From f90718f2f0bb43afb2394e4dc6a129b360d03826 Mon Sep 17 00:00:00 2001 From: e2002 Date: Tue, 17 Jan 2023 15:31:22 +0300 Subject: [PATCH] v0.8.920 --- README.md | 18 +- images/yopcb.jpg | Bin 0 -> 82178 bytes yoRadio/data/www/style.css.gz | Bin 7168 -> 7167 bytes .../src/AsyncWebServer/AsyncEventSource.cpp | 368 +++++ yoRadio/src/AsyncWebServer/AsyncEventSource.h | 133 ++ yoRadio/src/AsyncWebServer/AsyncJson.h | 254 +++ yoRadio/src/AsyncWebServer/AsyncTCP.cpp | 1357 +++++++++++++++++ yoRadio/src/AsyncWebServer/AsyncTCP.h | 219 +++ yoRadio/src/AsyncWebServer/AsyncWebSocket.cpp | 1294 ++++++++++++++++ yoRadio/src/AsyncWebServer/AsyncWebSocket.h | 350 +++++ .../AsyncWebServer/AsyncWebSynchronization.h | 87 ++ .../src/AsyncWebServer/ESPAsyncWebServer.h | 471 ++++++ yoRadio/src/AsyncWebServer/SPIFFSEditor.cpp | 544 +++++++ yoRadio/src/AsyncWebServer/SPIFFSEditor.h | 24 + yoRadio/src/AsyncWebServer/StringArray.h | 193 +++ .../src/AsyncWebServer/WebAuthentication.cpp | 235 +++ .../src/AsyncWebServer/WebAuthentication.h | 34 + yoRadio/src/AsyncWebServer/WebHandlerImpl.h | 151 ++ yoRadio/src/AsyncWebServer/WebHandlers.cpp | 220 +++ yoRadio/src/AsyncWebServer/WebRequest.cpp | 1008 ++++++++++++ yoRadio/src/AsyncWebServer/WebResponseImpl.h | 136 ++ yoRadio/src/AsyncWebServer/WebResponses.cpp | 699 +++++++++ yoRadio/src/AsyncWebServer/WebServer.cpp | 193 +++ yoRadio/src/AsyncWebServer/edit.htm | 627 ++++++++ .../src/async-mqtt-client/AsyncMqttClient.cpp | 755 +++++++++ .../src/async-mqtt-client/AsyncMqttClient.h | 6 + .../src/async-mqtt-client/AsyncMqttClient.hpp | 179 +++ .../AsyncMqttClient/Callbacks.hpp | 30 + .../AsyncMqttClient/DisconnectReasons.hpp | 15 + .../AsyncMqttClient/Errors.hpp | 6 + .../AsyncMqttClient/Flags.hpp | 57 + .../AsyncMqttClient/Helpers.hpp | 61 + .../AsyncMqttClient/MessageProperties.hpp | 7 + .../AsyncMqttClient/Packets/ConnAckPacket.cpp | 30 + .../AsyncMqttClient/Packets/ConnAckPacket.hpp | 25 + .../AsyncMqttClient/Packets/Out/Connect.cpp | 162 ++ .../AsyncMqttClient/Packets/Out/Connect.hpp | 29 + .../AsyncMqttClient/Packets/Out/Disconn.cpp | 18 + .../AsyncMqttClient/Packets/Out/Disconn.hpp | 17 + .../AsyncMqttClient/Packets/Out/OutPacket.cpp | 44 + .../AsyncMqttClient/Packets/Out/OutPacket.hpp | 35 + .../AsyncMqttClient/Packets/Out/PingReq.cpp | 18 + .../AsyncMqttClient/Packets/Out/PingReq.hpp | 17 + .../AsyncMqttClient/Packets/Out/PubAck.cpp | 25 + .../AsyncMqttClient/Packets/Out/PubAck.hpp | 18 + .../AsyncMqttClient/Packets/Out/Publish.cpp | 69 + .../AsyncMqttClient/Packets/Out/Publish.hpp | 23 + .../AsyncMqttClient/Packets/Out/Subscribe.cpp | 49 + .../AsyncMqttClient/Packets/Out/Subscribe.hpp | 21 + .../Packets/Out/Unsubscribe.cpp | 42 + .../Packets/Out/Unsubscribe.hpp | 21 + .../AsyncMqttClient/Packets/Packet.hpp | 11 + .../Packets/PingRespPacket.cpp | 21 + .../Packets/PingRespPacket.hpp | 21 + .../AsyncMqttClient/Packets/PubAckPacket.cpp | 30 + .../AsyncMqttClient/Packets/PubAckPacket.hpp | 25 + .../AsyncMqttClient/Packets/PubCompPacket.cpp | 30 + .../AsyncMqttClient/Packets/PubCompPacket.hpp | 25 + .../AsyncMqttClient/Packets/PubRecPacket.cpp | 30 + .../AsyncMqttClient/Packets/PubRecPacket.hpp | 25 + .../AsyncMqttClient/Packets/PubRelPacket.cpp | 30 + .../AsyncMqttClient/Packets/PubRelPacket.hpp | 25 + .../AsyncMqttClient/Packets/PublishPacket.cpp | 91 ++ .../AsyncMqttClient/Packets/PublishPacket.hpp | 38 + .../AsyncMqttClient/Packets/SubAckPacket.cpp | 46 + .../AsyncMqttClient/Packets/SubAckPacket.hpp | 25 + .../Packets/UnsubAckPacket.cpp | 30 + .../Packets/UnsubAckPacket.hpp | 25 + .../AsyncMqttClient/ParsingInformation.hpp | 21 + .../AsyncMqttClient/Storage.hpp | 13 + yoRadio/src/core/mqtt.h | 2 +- yoRadio/src/core/netserver.h | 4 +- yoRadio/src/core/options.h | 2 +- yoRadio/src/core/player.cpp | 3 +- yoRadio/src/displays/widgets/pages.h | 2 +- 75 files changed, 10961 insertions(+), 8 deletions(-) create mode 100644 images/yopcb.jpg create mode 100644 yoRadio/src/AsyncWebServer/AsyncEventSource.cpp create mode 100644 yoRadio/src/AsyncWebServer/AsyncEventSource.h create mode 100644 yoRadio/src/AsyncWebServer/AsyncJson.h create mode 100644 yoRadio/src/AsyncWebServer/AsyncTCP.cpp create mode 100644 yoRadio/src/AsyncWebServer/AsyncTCP.h create mode 100644 yoRadio/src/AsyncWebServer/AsyncWebSocket.cpp create mode 100644 yoRadio/src/AsyncWebServer/AsyncWebSocket.h create mode 100644 yoRadio/src/AsyncWebServer/AsyncWebSynchronization.h create mode 100644 yoRadio/src/AsyncWebServer/ESPAsyncWebServer.h create mode 100644 yoRadio/src/AsyncWebServer/SPIFFSEditor.cpp create mode 100644 yoRadio/src/AsyncWebServer/SPIFFSEditor.h create mode 100644 yoRadio/src/AsyncWebServer/StringArray.h create mode 100644 yoRadio/src/AsyncWebServer/WebAuthentication.cpp create mode 100644 yoRadio/src/AsyncWebServer/WebAuthentication.h create mode 100644 yoRadio/src/AsyncWebServer/WebHandlerImpl.h create mode 100644 yoRadio/src/AsyncWebServer/WebHandlers.cpp create mode 100644 yoRadio/src/AsyncWebServer/WebRequest.cpp create mode 100644 yoRadio/src/AsyncWebServer/WebResponseImpl.h create mode 100644 yoRadio/src/AsyncWebServer/WebResponses.cpp create mode 100644 yoRadio/src/AsyncWebServer/WebServer.cpp create mode 100644 yoRadio/src/AsyncWebServer/edit.htm create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient.h create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Callbacks.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/DisconnectReasons.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Errors.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Flags.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Helpers.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/MessageProperties.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Connect.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Connect.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Disconn.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Disconn.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/OutPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/OutPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PingReq.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PingReq.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PubAck.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PubAck.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Publish.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Publish.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Subscribe.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Subscribe.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Unsubscribe.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Unsubscribe.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Packet.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.cpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/ParsingInformation.hpp create mode 100644 yoRadio/src/async-mqtt-client/AsyncMqttClient/Storage.hpp diff --git a/README.md b/README.md index b6f4f15..b54ed16 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ ##### Web-radio based on [ESP32-audioI2S](https://github.com/schreibfaul1/ESP32-audioI2S) or/and [ESP32-vs1053_ext](https://github.com/schreibfaul1/ESP32-vs1053_ext) library +--- +#### NEW! +##### yoRadio Printed Circuit Boards repository: +[](https://github.com/e2002/yopcb) + +https://github.com/e2002/yopcb + --- - [Hardware](#hardware) - [Connection tables](#connection-tables) @@ -176,8 +183,7 @@ _\** GPIO 16 and 17 are used by PSRAM on the WROVER modules._ ## Dependencies #### Libraries: **Library Manager**: Adafruit_GFX, Adafruit_ST7735\*, Adafruit_SSD1306\*, Adafruit_PCD8544\*, Adafruit_SH110X\*, Adafruit_SSD1327\*, Adafruit_ILI9341\*, Adafruit_SSD1305\*, TFT_22_ILI9225\* (\* depending on display model), OneButton, IRremoteESP8266, XPT2046_Touchscreen \ -**Github**: [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer), [AsyncTCP](https://github.com/me-no-dev/AsyncTCP), [async-mqtt-client](https://github.com/marvinroger/async-mqtt-client)* \ -\* _if you need MQTT support_ +**Github**: ~~[ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer), [AsyncTCP](https://github.com/me-no-dev/AsyncTCP), [async-mqtt-client](https://github.com/marvinroger/async-mqtt-client) (if you need MQTT support)~~ <<< **starting with version 0.8.920, these libraries have been moved into the project, and there is no need to install them additionally.** #### Tool: [ESP32 Filesystem Uploader](https://randomnerdtutorials.com/install-esp32-filesystem-uploader-arduino-ide/) \ @@ -311,6 +317,14 @@ Work is in progress... --- ## Version history +#### v0.8.NEW +**!!! a [full update](#update-over-web-interface) with Sketch data upload is required. After updating please press CTRL+F5 in browser !!!** \ +**Please backup playlist.csv and wifi.csv before updating.** +- fixed bug with displaying horizontal scroll in playlist +- fixed compilation error with IR_PIN=255 +- libraries async-mqtt-client, AsyncTCP, ESPAsyncWebServer moved to the project +- new parameter #define XTASK_MEM_SIZE - buffer size for AsyncTCP task (4096 by default) + #### v0.8.901 **!!! a [full update](#update-over-web-interface) with Sketch data upload is required. After updating please press CTRL+F5 in browser !!!** \ **Please backup playlist.csv and wifi.csv before updating.** diff --git a/images/yopcb.jpg b/images/yopcb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0dfdcef18cd5afa615866c740cfd2f79df64fffd GIT binary patch literal 82178 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V`#l80z$m)g z=3uM>DZef{-a*8`(9`^5Kar!c{{R>!CPp^MUizLgM2c`NsCno0ji0Jc8f%P9!+Uq= z#K;Il<}yM|ZJ~a+!x9xud(q>(pi9hpIT~%W;KKN5QyiaVA--K>05iR-9t5Xoy0aeR zM3J8jr&xAvN(OC?ZTz@=jIXsG5leNzX3>du8V+zc4Y zV*5guFN;0q>pGId-^{suQGo4le;HV>6rb6Iok*tB#}2W!kfX`cmM!(3T`3Piy}e-| z(|H@``>YX*o`*b~=`)0u}4)8NjGTJ3g=!m=p56 z;uu3-R6k3E+#gd1{o+xX?o2A1sE==0+w5JBHwZuu3UzTJqd{C)N)isZxoHiRhL2Mz zotED7Sz;{bR#N%T!?S zQT%4)CM26(`Q9DHX2jzJOR?F-lPkrYh~>mU?&MD(}!;KtJ> zVd+kFJl@>VjKD-`Z;g*(=MUqq1I^^C!+PrlkD$sbQP>67FPAx6JrLgl#0)aZyk4x| zY(BAIFadeuq94$ z9I15ofviFog#6(isUlau-tY@#MJhL}b-MsKj~eR;iV>l6)~*(;1Vdd;-IyJR4v`mc z7+n!YnsGO={{UH_^F0np#SO2|P8Ax`#n94nKY5^}PeI|b@V^O{n5G^%1&9P$obcH4Vf8A=r$>6%xt0A9r0U-OOy;XE4jbP%#E2CF z^fTuDbBx{v9S^KVaTGyj$+v@?6>m#na7SZeeKX5<#m-A9MZJmNuR6oVFN4(Uul>zr zdk}NF{or9j{3}nmG~_w72Bv9uHuKR`~Ibq0?ijKb-T6DH~1?cPouF4Bjm!X^K>9^l%bI0_Bee z2Or(zwX%XB`Jdfl`)$Z`{V{+tsnf=?BLD>K`^KOG1V0~ml$J zCNsJqtqmXR9STXLqrJ79>i969KFRQmRxCOf9%q2Oyo-`Tt+ApAUCi3fLa=T87=DKJ zRdwZ^I(=j;*kHgb;A4n!&Err!ZwmN?CmF$Vy1akp1cecQ$R@V z(VrK1O>o%j_;>X(;y@e(?LX%W1%RcW*POKiHMJgn>mUV+LH?KxLv|y8$cEP(YhIiT z2Afsd7^q!!$*%K(QDa{?MTCYye{P&YOG+H7)bMeIvO#`L@sWvFL4+Ni@>FSiFW2Me zF0jfqYb4Tow}i9>3QZRNOx?G@ti3IUfK$*ZAiH@8IOsMsS!-rCw#zqY<^+68bM6 zt?L|DaBYhR2NemQUpRn~s4Sa0sU`v0Qarb??mtrx=MV?jU87ul_o$&-NMoieK;dE0&der=69pc}q&M>W`jo)(%$bxJ?-m$WjbwfqjSK%f! ztR)4iubebeR!$uS%2H1OLU4MxisYFN+rVAo_)#0N>&K_8AP6NW*r`4Di#K#2yI-gH z%Z7=XPdnMK?aOMSD;uVMU#u!cQ3aNB#w0#>5cKht$`PdBP(Hq}@h}Yk0EV#P#@K#O zeSPBxM-Za^ai<(pZ~&Zhghsj1D^J9juRN!tz~4kg35*7zm`$Z*yqBMhivIxXX6I|d z2#hXA@WU`4Vq77Lv@s=gLExg?HE=~utt-d#hU)$w0`%hdmP(XtJU<*{d33-*0=5IGl=Qepd}{~_DdRkRKJdaE(jYu>k9>it zLOoa~yjcVq%G7iOiA(h`v?(e99t+sVedV_s7SXePV>UT9(u>c-l^TJC7-@vVT0AlW$4`NSN-mAjIw>#(?ZI(EH`DYWbWZw73~ zZfz7IkG=yM-2@F0#^r}{`ozR2@VX1W_c{#JH84oMX?T41fU{aOs%M`wi8$US!ja<@|cV3KS6S_4)5MV$d+(#w)%8QP&=^ zkivE+pXrQjp;pdgJq!w5Gt{6Yuwv zE+qnA2lI#2sG`30u)u#}E0bzRU*k97Um05Y1rz>c zSp}A-k48jJ>{Cu&guIS?TxW1iA?4!%r7Wg{2Z(%3;64RSH}4`BM@_!(&S{z|$+t!K zm4fG>Ce|UQ@$+#gYEmKIs9oH$iE1Ka>W{`<2@97E-toGuAiBL~+0#HxYK?9VAcZt} z-#qX5!IqqGkIH=HNw6gM3%Xx@=MJTJ7hFtlyl56#@ASaPg9~Zp^`|M@=Qb5OpXUN1 z_QX-+=-#mllePgtZ(%evxW-tvLQ%2joj5RpN~t^dcfWWWR1JmY=it8>g&7mIRKfbq zxYt$9to;-knw?L?#@2T}yO;{*CGzud{x=MZTkyO0ePU9mweJ4_I1&z8+C87VS^{|` zS&i}fAT1Y_r2Wmf&VX(!;o;USxCXr#8wufDVTmJV95=*po-m)YS#T0so;V!ZD0UHl zInN{>msInCfT_r6X$_Dih362QI#}cY9f*Cy7FD-G-H$%lcZ|>y;G5!$8RILg# zOM>9iJQQ8=5#+xZ5GUw6lsFE*w;N=R6&}m4oP>B;DR{U<3vyNEc;ma!eBqWgfrp_u zAbR(k1@$1S%6z#cj|;G(a`nbD;CI@-&iwvzoJfMN{lemqgRh5=I8#ME6#3V9xkF2# zqwDy=FDA4Nw`h33SPBT}ns!mf7Gt;5RqgkTvIQ3KmioK+#Ad~!dy`*B82G=+QNmVV z^^21=A{;_Ry_1|HRC>3!dsmz#r%B%_Z7kj6Ua??Sc5|Lu4>qT zfwx>aw`T}@RQ~`O2QMN-L}V5OyH^0 zc6o-~bMf;*5{n&yt+OkX`5 z7=kuAVB=?UWc3QWk@bxq$_Vqk3mtDEE6yf?xuD0N7)WorLH7RuT&J-#W`g)3z5a8D zIdm8qI@j}*0~8dt2JVMmu^|@x3RDllzdd0N3F4MKS|d2dN>vJ7FH&4p-_9X9n$_#Y zeP=$A1@fK3d%(tK`*pYI@Z28vr;g}#{{S$+2y4y`rP7E1oU4yZ@r=eDi%}8d7FI|i z@?;L2olnOZp@aMpMvf5ig9d6q5Ggz7UUKNCagnJY1GWJTH&xN|`@pCST&L(4uWlU@ zDBiZ!*NjhURs`?w6HK+MI*&I;^~NKRLvQC;4O0c=poiD|!C_j6lrv%|=-PVk>v=J{ zs$)wSsL?yo#!y{qg9EZ$VQfgc)gC|(x9XaFLWF+kbN7(kccv6ONUpq0U9Z5&cXyo{ z+|_Z7xnR0j#S$aL#|#21r~E%IQ62g?!85hwPFLP40@;Z^@F2+v2l4AR^w>E3uo|bL$H5D1qkh zez55}da#$D7=*T0pv8vBAeTp2A`K?m77qUavlT0QtF!%OgG*a;CMN32))U{a?y|IZ zLbP-F{pRUFZ;4Ol;#%lY5u&p8oE}`7?V|+UUS?A^RtAHk_cL_FCWjYo4SzXX_{!{@ z1U_WnSesR})Pw~mKUj`1Ru=$uKz!F713^>r^Dpt6p{#UDh;=E=_}&@iq|RwxTUwRN zZ7+6#-=hjcgS zMZ52JjHy3iZ4g@ZImml$GzCD)%!o!>{m)rqL=9_=ZzA((N6sm;&IbLQ4;y&Ih?sdk zUwBfyIyuWRSxe#H-^LoMCG}2k&TEn(6>DvTZ}(W8gh*z<>|NQfj_&>jogQX1w_s2^ zQ^Af%^j#=%*0+ODax^k_jpMWD3jWesnhnBv!ph9BNmHZA(rvmGSs2pRNhmtXC?V7h z>jAHIXG?tLOiC5Ox1OgT7_o<FHTXoKfGcXM;@==Pa+qeup6 zb5TSMk7bh(X-zmq(q{z0O>(o({$Y4<8&!+OaOH@Cl7!%xBxy-&7&c=gP^0Vp=F=1* zZCqTbZP9n-IGI`bR8=(dPP;YE6)PfA9Vl!&ynD+-(hF~6#sd>%OJCRbSkyOcvGJn< zNkCBl068F_FlQCk4F(*(b+YFQ3u~KnN76jj+f{t|xvP*W$h_?0eIc)KdruTjxFea};s79}?0?)Ul+e`UyjQfPNip7jh(ofy z9W`@9j$3TXHesp8DP(J^Yo5#k5VNT9LI*qwyutc0M66Nn890p|p(?~b4glcv6^ z?<37OxqW8B)DXX)-b)3A6sUz@7|#M~=ZeaX5Dt~^STO*BfTYq;=X>53%mEQ2NEKS& zMms9T(rMC=@qxUc0n8&fP58pA=%@g9qKVEkh=ZbFxG98r)!wi(wnhPYXI$p4 zy=xV!9qh}4yrYg5k_F)#v&X#H@&|16bN9UNy+{d^Jchvnez?K1Xgndczb;Iwkb*a! zem|T5w~4aFjwZRnOkCFm%k!)20Z-;IB?CeV;A&+z)$BaQ-yJ>T)|!fveosF*`+@SH zHSyo)7{giVZ#z9^xKm0U^YL`6a(S*NzXnpyo99eglv!|VL@l&!`y-zsRBGgF3|JdH<<#KqSj z`P(*m_PB#oyL_#@C@%yoMvlxoRut3SO6&i_Btfm z>jkg@v`C4`zo5g*15m9zOSk>uxWa>>{{VH9Sqr-G<8gWyQ1$u#n2lv{IA3{o4X#@4 z_c7AL20lCI{^BJ$O?%@@;}#J_MB3{ar??akwG;}S=jP!n#=^_B?1*n+9oU1fj} ztK8eqjA3dS06$->6wtv}h39>yeBjCSX^+7lolbIv062*dL%q1*3rGy?-yS!Fvy{|m zlSYnZ)-br@CBBbGCh(R{z744J2?rqfn8`G4UUgwJaxI442XCA(14BnE&-0v|>vxH| zL&FKx6I53AQ2ziJMWGD@*MsHrm0B+7)3@J@-?STXPzALV(Z}(C8%3aNb*cNsVt^Wb zx#KTw(4blysb&;&r&vyk+quuia@Tq|hG}_Und1nx>^3s*diahxGZxr7f-Y}YX_c>L zWEx`z4mTsNH%t_xwaGfXf0B)>DiC1Gd-!880r3>;&?FOH$K}2PH$2BAzr69vmU7ppQc~ zo%qUB1$3eq4mCemHI)mDrR@WJa$%C-CkyY-@$-TvgJ95KIS=xxT2a;>#>5h85_{K= zd&UkiOOkvJx%$Ow!5t$~csuu=GMe!`23AeL)Fpqt;$s>}9UfEk&FkZ`9(vbVMlG^i zQ66`1pIBfzl?he3$>#U#HjcXZd*y&+7$h13;$y{^EU249KSn+NjG1SFKxP>3X4@W3 za=C1zfajU~{&1;UAa5aQD*!deIHCZJ5uEje03#r9d%0erZ3y_ll%S9D)-HmKUcUaZ z6a~;3Q+TRg(H&$FNZXe6Fs_y}r`}j$+i&mu!UCfw`~5OE$zKdBM`Tk&f+v_nb@<6- zf!Kce%iu=nZPv+C5kY7}t@O7b8J#F-KUvKx$cGOYN8e(N`j`Z!hC#gMxyl+|xWMkC zTsrZNf)*Tqpz(|-7Dr(GbBMBl(a`c}JIcUpTSvozpo2s!xq!$<&eOl!9nx9bqx9eq zDrz+NKfUBG3FI091BN`u1-e6l`!)?zpIqVW@hBB*MVoNC;SgeM@45RNfCIGte;Cwd zkppZ8SFYW8#Gt97;)3gtZ_W>7ciiYU*H!%C8v)@XAe=21Ek#p?c>%s4E(Eo~TYT?k z{NaHMMPs0cAAWL((j;9f(k9k=N?|OGLr61(k-{geCu+_pQJnl3b7nK30(gC3sTfJO zZ`t_Bu|zBl{_>`Q;L_b-6;>LJA6UChx)nG70CC3LcRif%ycgri1e{*6wR*@eS{{CR znY4$x@)!;2;K$)H(vS(-`=0U7){U58OH%cWQ2=GnUFC`7kPnP8&~PoFx;ZZ|l5N7% zIj(cgna>#I#n}4`6Fc0tYBz*V?u?#DwV0wRg(t?bw|RjE@KSNYX4Cjl`~o{Z@tG1e z4%&{lb-W=AYE(pc<=?X`gQa9(UjSgb9(!=)jS*epqSG55KUhY>oLSU6$Qo!+Q@i7? zvtdpssdsMw0N3XtrKAg{@qV$KY(y8z5B$ZIX@_T05$_o&D2o>qN7BC?N z-FdLxJ$7|*2!J5(hZJZ!920ntwN;@P_`)b$nuJBBz5Hdskz-`u8D}H5u}Wt9@MNl* zHlRfx4lUI9vD3Q&<6KN;7jT8ID59@Uq{c9}R&4Fr)Z?5%s2x!b{`AXbnl!kYRyq>;4;a<2?*YIj3Jy_@5?&=awYn$EnXr4_%*}Qiuq7d20o*07b*`k`XP_v%D&3 zTGnqp5W}a&1RXGRQh(MaY1Qa8<25V*JDuPcwP4XC;{zed0CkrJcd>VQ`Q~9dL`LuD z1PMK?F(s-B;|2{vz}jAYcZx$OM!HQQleTACEC7j-LqMePefu!PjR=cd?|tM>k~GVv zv1+Pf*Bh);R$ZYMumSr+IXRqAh8U<8i@x@jGx zez1Y?RSp$%Fk59=L@8CW+3y{3BW!HjMQ0T{Gs<&USC#VML2c3C7zNWy)`4(v?6(qh zVwA)H@0W4sAv1;2mWXwud`EblLIm*+{Qm&vLnA{Lxz;_AHh|yWE3r4mLV1|j2r(T# z{;)MYpasrOf#-?i5$IWA=_@<*oTl3aAwzrSZq}nwjw>EIDvCon1hp zs01&bFvO@tv~WB}S5+Iy*Z;L3+jTn{VcTQUAKzf!HcdCIN`jn(9T zy=I3(Cf*-d!HHQ(5$q>;M!@rtKqlvpyiy@qW>250H&y3hFB_=XR6l^o1VvNy2i`#w48KHw4oDV8tm*4&h7Vm2 zlup2lG*DWGE}glJlt<&QoD)pK_h>7`^_9^stDF?_ejJd0OTX@B?V&)s;&X>0Sb#)o zjIZOo2xgUTxpra!Cne(d_{Iu$fzX;i7`uGOm(#(B#twppg5THcBTq*f^oRP#iZ!ky zR|vQyM13Du*BG@FfkFeF%|G`jG=xBxT+CF|XygZ2Lu_#(asKmF4`*$DzusyX1fz$X zcCcU|n=KN~H-)I|xMwk&+2bU|>ZVY)Tbguplk&m>-hN&m3cV)Z}pF|Rj|8GFZY}aC(~E!&NC>q z0b2r~&}zmx^@(Vu6M-<8$vfSm+J?^`Id4v?MfrvQ0Ng@|X8!=g@q?3edt!@+L`Q?| z`@-Ef8VjC=hZ@&6j=Pc0IPl;V;9#{06d`CHzgSR^Q~@W+5B$I^0LE|*4Zm!nfHc_z z1GHf_%tRJVBu8aP_cw&VeM&u`EDVd7d>aG41NZA8v6@FUqPdlr1Ui+#froQjhF=R% z)f@zbI5OJ^2>=|5gnx4wW%ewWptP-qgu)SADm!cns9K?eh-Og=u#~IEvk5Z0Q#eAx z9bU4t#%Q)g0OM9>lr|TRMYyLs!v6rDu`q$JB3l8;7-bouKp z2hJM{^S-f)ZgGouUs283a?ZiV@UWc}^R7QeMaEoi!0$$v=N18I)Dw(>ic}@{JIJ9U z4DXlYG`RzKuE~eWsnC4>@I^x_p!2L4v2KZ?oS2X{E|IfcKi)D%FOmG7d?y%S_8Mh- zyJtM>9M-eI;>D)A9Dq24Vbk>%?5sHBihUDjYt5lWxWuXSf+zcsB3hJzj#mXGk zV?2pSwH@d3GHVRc=f*fIy4ey?y4kpeSPeIMPosMR`D)tS$gtqHxHk^v?t( zeSAQkW;D)BN(zGc+m34qEpz35Gq6)_4n9A#6miJ19t!13t7s!Vm;gZy^~EIgaA2Et zczWqK3@uKBTj0CmV^>vW$9yafc)?VcH~K!ZuzCbJ(TD=X3g654*RR1i%nCz?7%25q zsT~}#8ypSUD848hip5^G;4escrom6*;1rY)M$ZhHL~%e~{tw1-DiJ{A&3_n!X2noW za$`{HGyUPH3;|j|CoaPPLtRit9UiVAiI9`Ha5$CR#SO@^CD6hhA zyOMn9r}@huxPbxD=UF9UFc3hRx|_q?L=|h!;flA~!dWe=9^B&XI?Lm2h4aAz`BOD7wnnhE?cys~e5dPeV z6K4Ma8o`00ibz`DgKqIn1vv$K_cKw8xZ&^l z->impgf)4dFupZ5#{2PzMO;IcaC{~q$bwll&Jq)$EkD_S76XaP`^|0e(}Rx#$~S~V zjH$4qKRMa7>^(Zj)rGfw>)t^HI{ap`u}iRj<~7}$^YD4fVxXL>$Bc>-hKL;=Kc)`s z`sjY1@zOQgx7+;S_klZ(pNA{G4-vUN8rC$L4~v{HIT#2DwBJwj>lUC*KsUXAj`Db4 zkLkxe{;^6F2+`m9#YYMP`~l+PVsI89_?e_AO+4Y$3ax3!#lSeBfZ#MA-yX0abvYe+ zCIfq?bwcXmB4CPxh2NZRK>?utH-A_Pz*2*Q$IedCjFwsrqh(0wvuaat1ItmvapR zAxwyfjlFY%-ETFq!MoRQ!hjIlW6nYXaLN5|97>RKS^56}nIl(6zkL1q%CYM)#jB%m zu8ZGr8KM!Y$G``$;L6}iI>aRKItGNx{Z~}culO>Kn`n}!@&2$?DJ!-9F-X$R2cPwf z-K0d(tVIDyU3ec_%4Qh~P10-&#)F(fo%9VOx>Jll75@N+@;)7y9^Zw=4zhTjCm00S zI|hv%sQb}@*+#06J^OM@Wsoi4P=$BauuLvJi74`H+zj64<7!YmUOtQg8uWuc67}OB zqpkLE+vEVAJYmrw6$7AO1U|UR^1z=6lRgb+dRWeKbx-q^MZu7u3D@h!7dbJlw4ouw+x@A;GK4#9ATc}Pp0+9{^VKl zY65TfuXqhekV>#+a8U~NFdgh()2V;Haqnr%_~PqJa7MsUiTt>z)5nAZ=Xl};LjkEy z8h*IIFy+_bk0A-IZ21nkz-Yr{<_iA=r~!4R}305`AW zBa{F!DhNG)xr=fbzBWRY;{ZrUPUNb6U`(2z%7YvI)+Qv#q?~sH$IqMq+zIKf@r!ct z&^#h@fMgqUX5DIDO2!}@T#uHN`xcqPTh&vQ; z&yOGH5!6Fz*Po2E0G|Qw{LDcG(1|@}owRCa^1#-h8AEJtP`N#w*AYt(Oi|o^vIN4~ zZ=BI6(VdUajpG;>Md176Si~(6IDhu#DJXj$dmmqnGi7f11yFV^hVttmiHBjJDb;wy zOw_zf^c&X&=nyA(FOC6+Fx_ET>;mFhM+Hgi zjs40Koj=VvaboGN5e1ZwfU zPmHL6byR8p06N#!D%i16-Tgo75d|jJ`P=%y5*yj8^}jg1lO^4J&Zbd>55_27Ae-lj zfo0)QUOU4*wUQ!nL^f)W?Bfe!h%45>nC z6ZBd2j>^*_tp?<-S=LVJwNFnsd8{m*0R%@uX|U%FBe+!azB(*r%WTelBx#_L;`4_W zfk27TLqooDnn)pOdkzx*Frf-HP5ac0kORsfQZ7=3*~_$(;{)}j2IJ&!F#Aa=OUVFa)>hma6=^N_3qY9gp%3JvR?57UL&rtjLSrc=P0Q=TF!VM*@>j4 z;t#E|*o0KWo3rmFy>QZYSH0kZRG=L2&Uc!FkmB+Z97#JMY_k2*iR<7kw#i5%FknOTXT?5&gicf2OYHz(c; zJUCTX1_~B}>fzQwT3N2OlUBLaXt%BM>sK6+)$|WRo_l>6aKfD&(B@;z@W#&2_6cE> zrS>@dc!rc+0KUb?kI$ByI6|fS?*h31c}HGhj!pt4eh1IiAS42KQT&;7Zr`I21a|dt zDuLLfia8O}<1bJH0{aqk^@_0sfrLM<@xxYAT-u%Bg4lt2c=f!7u@SH=ua9^rFx_>& zJ>=_+Ygx+iukW2=@f8E6e6p3pfg?l*L!`f1wV(uyI0-ypZlIhA{;-blh@$;(`e8Gz zgTa4zn1_|n-#=KkKo)x+^Yeys5Mb?Q;<)pKtH4iHJSd`-Gp7uN1HR6l%a62$i>+Oe z1P#KjApAJQR8UAO+Sgcn(<3sWmv1c^TNh|Erzoc)uTCyi_HV zvBCv2H;LN8U*U}eL443~*tXbLSmc5#dO0rOUls1T+eMNlirNi1+v#Xm9(pJJz)_Mk z15fqN7X*L+fTj4rUCG!UUgiHpJOlW6vDseaS{Ny1;LZ>(Lo+IiAkzYA5fGe+Z%j_@;Ir$IC>mP?PfYZGX z_q>dJY)RvtWSgX_C)On4F5yvrG@oV=%K0=Xk7v(#siO)T9`E;gOa>{s5)+N&0k{(t z@xj&%PLXF&xxpfIHOb?~Q8n9VC!7+HHg?{OMAxtbe7EubFkwT9u1?QiI5b62?A-qP z##TsGv3L8&sJu#e8o0!Zhk%v zj;A~(UzS}1Y>91McX0c%oeTr;4tu!vm7)WzM6pwiZ^iE+bViZ;!iGZ>-qoTs0#UKn z7A_a>bS?;lUirwoO2I3^E(J(QG>TE&K`fvgi>f*h7KS6Hge6C+$8*Hv8Cz0r6~vmt zrTOO{x|#{1kcAMN)&xYd0J=fY*z<)OF7}iX1UQ@~Y++p6_kS6qA`V4kqvr*SQRs9Z z8p_&-s&&qB5#~KF{l!Uwl`HXoyx>1&B5o+w2p5;*C`GX=2HN-_I^4pgKow4fvBKb8 zWvk8LMy;`;_+?d1EVsl56tH$(p>BmzgrmJEP1<0?7-rH2iV5t>hD=_xMvultsU4qN zpiXm<0(+?~hixva=LJ(MxP%U88o5X%GrXZJZp(p20Jc{ONMFN@bD&1W<)NO^Z0`fEgX+Oj~ZfSI#YZ@j^CI?M|={Ca|tn+$pBp@90Wp&VQD47D3+dbsS z3IIq%oCdt@&5WgiXcXv!c}z{G2oP9kZCJ5$WfvN_37e!GIjyw8i)*N_lLasa(dTa; z7!#cuHShC+0%eMSv4>F85O2H$116w8%vMP%9%t_e2@xO>)IGDJ1ZIcBP4|d8pK--X zGj!v;i!A(B1c3wV78lw!x4mD^7t$0a6-^bi#3ExvOHnm|U3e=eC;7gz0pufbdufH5 zO%{PIq)Y%(y~JGu5{!v>CtbJCSahIR@5jWXzzb_-@3>?byHIUy+lQ!Ft&G29))K=D zBuo6{>C`pr`2M(?S5olEkRoZ$^^*z<%Xcwq1UVdNF_P#CZ-e-`bRZ2bzW)FiY0m>T z-gxnYKm-n$&}&p3p7s7QH8rD0?;pe?(t3NtoT}(P1yisW8AUTF>80px25`n|dnh*e zVfTm;LpwY7ygY*fzj(tkX*Ox+{b1l)BZucD{0QtGpNwoDhqLdj6mYHuW-u77fH8c3 zoN5guIPVUHBT2Gr@tVr)NFvk4HE}f+jod&2xJAvZHvFDXvQe7EWx#k^YI*_%3hcf= zP;VP+HSv@zxkX=k%N+vHJ8*yyL|yCn&30E3rrWt5`n0pcT;px^bn2DQD7g^$Hfxs7 zo_=v+HdGB$S%(5;0TtvYEpFcO!Ipr;eYqPKt)8;(EP$X~@qh%gI~oqy*AC18J36MK zMdyR9xqc+X8KD6KyEm*j@Br*$q5vt=@8=!pK}RO|eb3_}k*RM;p6~=Vlc{{``+VdH zPz`AW`@s|*!3$R_AU-!)wKB9+Hg9Bszl@h*GD7ddDjtplEupA@A+Ab87Q1$!txzff za895@vP7E|o!m9Spgarq{LDgs7(S@#g?FQ#ve4NtL>tx6&jud1*cd@#2P@|Q09#>G zSgkZ1xQqpi8w3+fSKezaS@y-zen5TTo#$9kqm>`9_k!k1NV2xZhhf%ChBick`!4&| zJI-3{Z0|SS>%4Lsc%F&I#{shCfg7uPD%2KT@t5CDoE{hbVMad(7nGm#8Y9c7YyCLd z%gh7{9t~V3dT8b(Pvi6JI>p5hBxBL*xs7#l7Mu~89Nor7&j_-sO)0B|k%vlEs!Y}A z)0>Up@YwEai8}qm3D^})GX>2A)e_Eg;exFuC%tQDrYGTgFCz~Q*`pcjHA}%3Zm356 z+zamXdC=< zta;QlX&_@-Fgpa})@dRL$~_&|`*1KXp&TmfjT{}~DQ3>$(ggLqMEZvAd2S9GDq-~h z09Y8IIuM@!0Kbemz!Czr(0JeLBM1SFLxCA(x^FyTD_l2ce)!0XP8ajyBj^hx*NiC^ z4G-CX2x+KpcyLQ7vgPx)`>Z=o%>g*b&_I*7SjJ8w8#xZu${{2Q5K)}Z!<2!7TYgU* zxx9x-aQ$JRCI-(xeth6U1h7Z#^Nav3v8Q^#v`UA;^Z1w+1ce}r918>rT$<;36a-Cv z#}rNE4#jXi<@tc%QqbXX{8n>!LMaL_o0Bab`2`C5bl+Q`&~6SN@sRpOLTvN>^N1DL zjiJzgc*E*ZeD{C7xDy1W0bl9HRX~lJyT3+JlA&E3B(-0xM!fq|n$Oer&7}sF=z*n9MG7))Anv)`L zC@=}avq^ShfPk=!G9#eb-x;y`w2mAjM8}7!#SpUaQ$bK#Wf2`CHA8bJ8!8+3@quEZ z{L+C!fE>ItAAk1=H}6M#MnX4hl84Jq3&Rtq)`qQ zg*qMka57u96lNL|9X!7u-DTlW9E(quykOb0_8`6uY}8jZN1hT2k2~LZPrkx!f{EBo zY8@HE5h=HZ7~x2m%9h=CIo26o-nnc_)RFteE$H9gN{`R^)c5xjf} zymkl>nC)2c$%MNkNw6-lNu+`+#~k=T2ruQ0ex4tnYC2j!km-x*TAs~i#05p#+Qvru3 zVT{xH?+s5{V{6zRZVh%&==-0%3QEK)U*9;=vhyo!@4aJPp_zrR>*EA0cttnCrN4}D zJya>)ou}8FborbbYTo(8B}j#qe|ydw;-`*wPZeF4h| zlG1ODaw*P<0+O;3vQI3Uu~H2%RcWxO>ks6is2j6yFMGI=>2D|^qROD4^U|r0R4UH;F z1vI6Qd`JgIR8{1~*~$}ituTs0_#I@p=*EkuNSqw`$Tm<4N4|=s?Ag)w8#SO{#s<U89JX?!VBwA*?8h9M$FVtiw(3xK<4wT@h@tU6`7N=*&@rB?T zjl2Lw7M|IX!`8?Ks%g`E3k=wjC1i6_Kxopg5N*VO1Rzo$)`o**&-gLA&avm>9MbfK$e(<|GvZsX-9?h73 zA}a@HF78M*NVzWYFQ#u7UN^iO?w|~T{{XBiu0S~cJ>Z@)1nfNj069@YvFD}#09cj< zSsv8q@sJKk=p%XlanuC1yo65}-i?>L$>%g)Fsg4~oGz3RWIcW0eqQbr^LIWQ%Poq> zIM+g0`EClBwHbJTpEdbp!-)<;y^sF@85dE#54UBLafr% zIb2t}oB>dFH8BJdBaR9kW#bNokwBp+0YpHq0Mr&CBt@tYE+I}W^UOi%Flj{FfWCaJ z;(TI%awLT<3fzjOA&^;Ff{si%e+zX8<)>9_xJvh{D4M<=TtU-b8$6u`ve_!zdB=Nc z>|y{@opZzU!5ZK&2%)l($HX}uWai_f2+ND@5pabU5aVlyt7X-!Pu87T;&F&bHXbes zmqkrt4_6Wwfi%ea#u)4F7xsvbNHQ!J+AwJff*f#;@X-?J?=xI&oKhY!4Ja^A#7cQvU#X#)g7W=e%Z#(sPSj91@32oA-}s+N*=4IXl)bQ+P$$wZmO+ z0AcrnHd_H+X9fg~iXH=eelZxx!hBwFezF6|yg09X7KvP;^o}KJLd6vzRtG&3j4m6n z5HNXlyWbHy=Gr1(0{zz;U{IiifbC5H04u%Q< literal 0 HcmV?d00001 diff --git a/yoRadio/data/www/style.css.gz b/yoRadio/data/www/style.css.gz index b3237b2f9a1a75dafe9c1eb8c6f9aaa9a33e1017..a6eb4183cd70ac1bbfd2f782de1d5fd2b5750ada 100644 GIT binary patch delta 2381 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 delta 2417 zcmV-%36A#vH-ICHotd z=UF{C^nvSz`_e_Eqw>IM0(Cg);RYhcKDoIPMs?;oHi?zP83z)ZpSqqfsVMxV(UTi` zYiOvWai64%fo)1_)N$V#Zd8*uU$t;@uJEsujb}O|-mB8UTj6S{x6*Fa-8K`*xh-^Y zW?>X*nwTw1gsHaq&W2-#D|@C_)LLMZ6cb7T%9CFcWPc~i$w>~GkGvB1oQX8x_5xBt zs4W|a#j;gD2mq#4fCG=HhO|%=M|?-r`H#PR+dBHXpKQ!hR4IWOs7Y$9qwRE zERX59EIB!|6QjTM*wOmXH3kg3s4gx;mcg(tGwxI{PGJZS7udZiAIby9Fm<E z7O(1T*-?rMX0_|gr^CHqla~oI@6=l(ej~d_pR%a7J@*B>UBgC8`AAp$y;Gf2hK;~< zHBWk@z2u5M*<;m#-y5>60maRTS!r`sOOnNm2Rl*UI0m=ANGe%o#w=HMPs(IkUh5ZX z=r-|T^CZxNojh~9nl^3oR+$zmf0%bEHjBJy`(;L2auuJe5XF(ylg@KNsqLxZeuDQ} zRG^wM?kc)%)v@(;)24+$^f!a1YIfZ2cy{enZN?b6JGCd89MP@XrRMq6!u8^zs`7iB zx^zh6I4P6IL7Q!E_ieSKlj3UF_nD^O-r?-oYD_oPokNbbvt2X>3aM~7f9_$cR#TEH zmR}xIE3D`xwn<}-RYsD$5xGnK?PVGrJLQjMvPYs1 z=oHa`B#cmfOcg*Ox{?$1gvoVd;FL2burHaG?$78lN+xL&k68h5=9H*|+HL?(<)_bW zQ=|m~Gk5D><<`HGV+EU|5uTe_W`d_i@e7jFY9@u6<3@GwX#vXylLI}`XXd=Y2myV)9 zQrOllEP`#}#k8}hz}jvXj1FK4(mFz5Co16?bw>|mJ_mLxC)kHD>7-d7gW6;c?r%86 zSY&cUw?!l+FbXpxe-arqawr)ITsamLbaEb#l|MO0y&4#@mmJnF&M$`rkY31+e1Id! znUR?RfEfwD-*+BLLU#Pog#HP=4#8Ld1d!^#hrRke-^GI#qaxq7ZZqo`RvLEzvrNnl zSs+4+2ST;dP9|u~-8#;3^&_Vv@-VFsFP^|v-bK%X7dM_?f85Wp?8!3-$^NGU&OuCs zm7?=w=1f|X$YZjyfb;sr_tvA(sNN)CbIItgSByL`7=3E!gPFKLuw!H}D@#E5Fhe1k z)z4;EJgN6xRE1d9LR#qix!nYe84uYq5NR&jNgg16(0~sVNQ?0aPPhyoHN4w zzR{`kxl^b#S|!{DX4)aA;C4X>#Gbu#Gr;tQP;unk*AV;=f_}%OC2H1lgVpOUtl<4Z z7UzQ$7oC83hVV}DWZpdK zNvbh3f4(6ciG%``Phf?2&IFZ^3LZj65$`Y7QPErcR{`QfAU z@}EpbhF5`5K<^M$MAQlUiLUPsjuCjS?pq4w4atBb4scwCdQSMsyOl}kDYBTdy-b~t z!HX4S_xVZd@O20}*^QbDkewL&h^f zf3AmQsTx!w%4b4){tHG6_$a8FGMdf$bWDGGdgP`75)elz&4jnz(GP#-)GgcUFfC; z;sk+BUC?a3@CCkZ>no@_z4NS)zat7%f1e2v<(6(f4Y>4on_QSBiO0dkv(c!~G6R|8 zfaqOA2zt}w+G>uOlX{prVtS464+ zal}GN$!L}|S`nKe8RpjQ$z9K@RJdgTp--=L`j)FUv(RZQsVF _len){ + // Return the number of extra bytes acked (they will be carried on to the next message) + const size_t extra = _acked + len - _len; + _acked = _len; + return extra; + } + // Return that no extra bytes left. + _acked += len; + return 0; +} + +size_t AsyncEventSourceMessage::send(AsyncClient *client) { + const size_t len = _len - _sent; + if(client->space() < len){ + return 0; + } + size_t sent = client->add((const char *)_data, len); + if(client->canSend()) + client->send(); + _sent += sent; + return sent; +} + +// Client + +AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server) +: _messageQueue(LinkedList([](AsyncEventSourceMessage *m){ delete m; })) +{ + _client = request->client(); + _server = server; + _lastId = 0; + if(request->hasHeader("Last-Event-ID")) + _lastId = atoi(request->getHeader("Last-Event-ID")->value().c_str()); + + _client->setRxTimeout(0); + _client->onError(NULL, NULL); + _client->onAck([](void *r, AsyncClient* c, size_t len, uint32_t time){ (void)c; ((AsyncEventSourceClient*)(r))->_onAck(len, time); }, this); + _client->onPoll([](void *r, AsyncClient* c){ (void)c; ((AsyncEventSourceClient*)(r))->_onPoll(); }, this); + _client->onData(NULL, NULL); + _client->onTimeout([this](void *r, AsyncClient* c __attribute__((unused)), uint32_t time){ ((AsyncEventSourceClient*)(r))->_onTimeout(time); }, this); + _client->onDisconnect([this](void *r, AsyncClient* c){ ((AsyncEventSourceClient*)(r))->_onDisconnect(); delete c; }, this); + + _server->_addClient(this); + delete request; +} + +AsyncEventSourceClient::~AsyncEventSourceClient(){ + _messageQueue.free(); + close(); +} + +void AsyncEventSourceClient::_queueMessage(AsyncEventSourceMessage *dataMessage){ + if(dataMessage == NULL) + return; + if(!connected()){ + delete dataMessage; + return; + } + if(_messageQueue.length() >= SSE_MAX_QUEUED_MESSAGES){ + ets_printf("ERROR: Too many messages queued\n"); + delete dataMessage; + } else { + _messageQueue.add(dataMessage); + } + if(_client->canSend()) + _runQueue(); +} + +void AsyncEventSourceClient::_onAck(size_t len, uint32_t time){ + while(len && !_messageQueue.isEmpty()){ + len = _messageQueue.front()->ack(len, time); + if(_messageQueue.front()->finished()) + _messageQueue.remove(_messageQueue.front()); + } + + _runQueue(); +} + +void AsyncEventSourceClient::_onPoll(){ + if(!_messageQueue.isEmpty()){ + _runQueue(); + } +} + + +void AsyncEventSourceClient::_onTimeout(uint32_t time __attribute__((unused))){ + _client->close(true); +} + +void AsyncEventSourceClient::_onDisconnect(){ + _client = NULL; + _server->_handleDisconnect(this); +} + +void AsyncEventSourceClient::close(){ + if(_client != NULL) + _client->close(); +} + +void AsyncEventSourceClient::write(const char * message, size_t len){ + _queueMessage(new AsyncEventSourceMessage(message, len)); +} + +void AsyncEventSourceClient::send(const char *message, const char *event, uint32_t id, uint32_t reconnect){ + String ev = generateEventMessage(message, event, id, reconnect); + _queueMessage(new AsyncEventSourceMessage(ev.c_str(), ev.length())); +} + +void AsyncEventSourceClient::_runQueue(){ + while(!_messageQueue.isEmpty() && _messageQueue.front()->finished()){ + _messageQueue.remove(_messageQueue.front()); + } + + for(auto i = _messageQueue.begin(); i != _messageQueue.end(); ++i) + { + if(!(*i)->sent()) + (*i)->send(_client); + } +} + + +// Handler + +AsyncEventSource::AsyncEventSource(const String& url) + : _url(url) + , _clients(LinkedList([](AsyncEventSourceClient *c){ delete c; })) + , _connectcb(NULL) +{} + +AsyncEventSource::~AsyncEventSource(){ + close(); +} + +void AsyncEventSource::onConnect(ArEventHandlerFunction cb){ + _connectcb = cb; +} + +void AsyncEventSource::_addClient(AsyncEventSourceClient * client){ + /*char * temp = (char *)malloc(2054); + if(temp != NULL){ + memset(temp+1,' ',2048); + temp[0] = ':'; + temp[2049] = '\r'; + temp[2050] = '\n'; + temp[2051] = '\r'; + temp[2052] = '\n'; + temp[2053] = 0; + client->write((const char *)temp, 2053); + free(temp); + }*/ + + _clients.add(client); + if(_connectcb) + _connectcb(client); +} + +void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient * client){ + _clients.remove(client); +} + +void AsyncEventSource::close(){ + for(const auto &c: _clients){ + if(c->connected()) + c->close(); + } +} + +// pmb fix +size_t AsyncEventSource::avgPacketsWaiting() const { + if(_clients.isEmpty()) + return 0; + + size_t aql=0; + uint32_t nConnectedClients=0; + + for(const auto &c: _clients){ + if(c->connected()) { + aql+=c->packetsWaiting(); + ++nConnectedClients; + } + } +// return aql / nConnectedClients; + return ((aql) + (nConnectedClients/2))/(nConnectedClients); // round up +} + +void AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect){ + + + String ev = generateEventMessage(message, event, id, reconnect); + for(const auto &c: _clients){ + if(c->connected()) { + c->write(ev.c_str(), ev.length()); + } + } +} + +size_t AsyncEventSource::count() const { + return _clients.count_if([](AsyncEventSourceClient *c){ + return c->connected(); + }); +} + +bool AsyncEventSource::canHandle(AsyncWebServerRequest *request){ + if(request->method() != HTTP_GET || !request->url().equals(_url)) { + return false; + } + request->addInterestingHeader("Last-Event-ID"); + return true; +} + +void AsyncEventSource::handleRequest(AsyncWebServerRequest *request){ + if((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) + return request->requestAuthentication(); + request->send(new AsyncEventSourceResponse(this)); +} + +// Response + +AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server){ + _server = server; + _code = 200; + _contentType = "text/event-stream"; + _sendContentLength = false; + addHeader("Cache-Control", "no-cache"); + addHeader("Connection","keep-alive"); +} + +void AsyncEventSourceResponse::_respond(AsyncWebServerRequest *request){ + String out = _assembleHead(request->version()); + request->client()->write(out.c_str(), _headLength); + _state = RESPONSE_WAIT_ACK; +} + +size_t AsyncEventSourceResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time __attribute__((unused))){ + if(len){ + new AsyncEventSourceClient(request, _server); + } + return 0; +} + diff --git a/yoRadio/src/AsyncWebServer/AsyncEventSource.h b/yoRadio/src/AsyncWebServer/AsyncEventSource.h new file mode 100644 index 0000000..4f5139a --- /dev/null +++ b/yoRadio/src/AsyncWebServer/AsyncEventSource.h @@ -0,0 +1,133 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef ASYNCEVENTSOURCE_H_ +#define ASYNCEVENTSOURCE_H_ + +#include +#ifdef ESP32 +#include "AsyncTCP.h" +#define SSE_MAX_QUEUED_MESSAGES 32 +#else +#include +#define SSE_MAX_QUEUED_MESSAGES 8 +#endif +#include "ESPAsyncWebServer.h" + +#include "AsyncWebSynchronization.h" + +#ifdef ESP8266 +#include +#ifdef CRYPTO_HASH_h // include Hash.h from espressif framework if the first include was from the crypto library +#include <../src/Hash.h> +#endif +#endif + +#ifdef ESP32 +#define DEFAULT_MAX_SSE_CLIENTS 8 +#else +#define DEFAULT_MAX_SSE_CLIENTS 4 +#endif + +class AsyncEventSource; +class AsyncEventSourceResponse; +class AsyncEventSourceClient; +typedef std::function ArEventHandlerFunction; + +class AsyncEventSourceMessage { + private: + uint8_t * _data; + size_t _len; + size_t _sent; + //size_t _ack; + size_t _acked; + public: + AsyncEventSourceMessage(const char * data, size_t len); + ~AsyncEventSourceMessage(); + size_t ack(size_t len, uint32_t time __attribute__((unused))); + size_t send(AsyncClient *client); + bool finished(){ return _acked == _len; } + bool sent() { return _sent == _len; } +}; + +class AsyncEventSourceClient { + private: + AsyncClient *_client; + AsyncEventSource *_server; + uint32_t _lastId; + LinkedList _messageQueue; + void _queueMessage(AsyncEventSourceMessage *dataMessage); + void _runQueue(); + + public: + + AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server); + ~AsyncEventSourceClient(); + + AsyncClient* client(){ return _client; } + void close(); + void write(const char * message, size_t len); + void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0); + bool connected() const { return (_client != NULL) && _client->connected(); } + uint32_t lastId() const { return _lastId; } + size_t packetsWaiting() const { return _messageQueue.length(); } + + //system callbacks (do not call) + void _onAck(size_t len, uint32_t time); + void _onPoll(); + void _onTimeout(uint32_t time); + void _onDisconnect(); +}; + +class AsyncEventSource: public AsyncWebHandler { + private: + String _url; + LinkedList _clients; + ArEventHandlerFunction _connectcb; + public: + AsyncEventSource(const String& url); + ~AsyncEventSource(); + + const char * url() const { return _url.c_str(); } + void close(); + void onConnect(ArEventHandlerFunction cb); + void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0); + size_t count() const; //number clinets connected + size_t avgPacketsWaiting() const; + + //system callbacks (do not call) + void _addClient(AsyncEventSourceClient * client); + void _handleDisconnect(AsyncEventSourceClient * client); + virtual bool canHandle(AsyncWebServerRequest *request) override final; + virtual void handleRequest(AsyncWebServerRequest *request) override final; +}; + +class AsyncEventSourceResponse: public AsyncWebServerResponse { + private: + String _content; + AsyncEventSource *_server; + public: + AsyncEventSourceResponse(AsyncEventSource *server); + void _respond(AsyncWebServerRequest *request); + size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); + bool _sourceValid() const { return true; } +}; + + +#endif /* ASYNCEVENTSOURCE_H_ */ diff --git a/yoRadio/src/AsyncWebServer/AsyncJson.h b/yoRadio/src/AsyncWebServer/AsyncJson.h new file mode 100644 index 0000000..131ad59 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/AsyncJson.h @@ -0,0 +1,254 @@ +// AsyncJson.h +/* + Async Response to use with ArduinoJson and AsyncWebServer + Written by Andrew Melvin (SticilFace) with help from me-no-dev and BBlanchon. + + Example of callback in use + + server.on("/json", HTTP_ANY, [](AsyncWebServerRequest * request) { + + AsyncJsonResponse * response = new AsyncJsonResponse(); + JsonObject& root = response->getRoot(); + root["key1"] = "key number one"; + JsonObject& nested = root.createNestedObject("nested"); + nested["key1"] = "key number one"; + + response->setLength(); + request->send(response); + }); + + -------------------- + + Async Request to use with ArduinoJson and AsyncWebServer + Written by Arsène von Wyss (avonwyss) + + Example + + AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler("/rest/endpoint"); + handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) { + JsonObject& jsonObj = json.as(); + // ... + }); + server.addHandler(handler); + +*/ +#ifndef ASYNC_JSON_H_ +#define ASYNC_JSON_H_ +#include +#include "ESPAsyncWebServer.h" +#include + +#if ARDUINOJSON_VERSION_MAJOR == 5 + #define ARDUINOJSON_5_COMPATIBILITY +#else + #ifndef DYNAMIC_JSON_DOCUMENT_SIZE + #define DYNAMIC_JSON_DOCUMENT_SIZE 1024 + #endif +#endif + +constexpr const char* JSON_MIMETYPE = "application/json"; + +/* + * Json Response + * */ + +class ChunkPrint : public Print { + private: + uint8_t* _destination; + size_t _to_skip; + size_t _to_write; + size_t _pos; + public: + ChunkPrint(uint8_t* destination, size_t from, size_t len) + : _destination(destination), _to_skip(from), _to_write(len), _pos{0} {} + virtual ~ChunkPrint(){} + size_t write(uint8_t c){ + if (_to_skip > 0) { + _to_skip--; + return 1; + } else if (_to_write > 0) { + _to_write--; + _destination[_pos++] = c; + return 1; + } + return 0; + } + size_t write(const uint8_t *buffer, size_t size) + { + return this->Print::write(buffer, size); + } +}; + +class AsyncJsonResponse: public AsyncAbstractResponse { + protected: + +#ifdef ARDUINOJSON_5_COMPATIBILITY + DynamicJsonBuffer _jsonBuffer; +#else + DynamicJsonDocument _jsonBuffer; +#endif + + JsonVariant _root; + bool _isValid; + + public: + +#ifdef ARDUINOJSON_5_COMPATIBILITY + AsyncJsonResponse(bool isArray=false): _isValid{false} { + _code = 200; + _contentType = JSON_MIMETYPE; + if(isArray) + _root = _jsonBuffer.createArray(); + else + _root = _jsonBuffer.createObject(); + } +#else + AsyncJsonResponse(bool isArray=false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : _jsonBuffer(maxJsonBufferSize), _isValid{false} { + _code = 200; + _contentType = JSON_MIMETYPE; + if(isArray) + _root = _jsonBuffer.createNestedArray(); + else + _root = _jsonBuffer.createNestedObject(); + } +#endif + + ~AsyncJsonResponse() {} + JsonVariant & getRoot() { return _root; } + bool _sourceValid() const { return _isValid; } + size_t setLength() { + +#ifdef ARDUINOJSON_5_COMPATIBILITY + _contentLength = _root.measureLength(); +#else + _contentLength = measureJson(_root); +#endif + + if (_contentLength) { _isValid = true; } + return _contentLength; + } + + size_t getSize() { return _jsonBuffer.size(); } + + size_t _fillBuffer(uint8_t *data, size_t len){ + ChunkPrint dest(data, _sentLength, len); + +#ifdef ARDUINOJSON_5_COMPATIBILITY + _root.printTo( dest ) ; +#else + serializeJson(_root, dest); +#endif + return len; + } +}; + +class PrettyAsyncJsonResponse: public AsyncJsonResponse { +public: +#ifdef ARDUINOJSON_5_COMPATIBILITY + PrettyAsyncJsonResponse (bool isArray=false) : AsyncJsonResponse{isArray} {} +#else + PrettyAsyncJsonResponse (bool isArray=false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : AsyncJsonResponse{isArray, maxJsonBufferSize} {} +#endif + size_t setLength () { +#ifdef ARDUINOJSON_5_COMPATIBILITY + _contentLength = _root.measurePrettyLength (); +#else + _contentLength = measureJsonPretty(_root); +#endif + if (_contentLength) {_isValid = true;} + return _contentLength; + } + size_t _fillBuffer (uint8_t *data, size_t len) { + ChunkPrint dest (data, _sentLength, len); +#ifdef ARDUINOJSON_5_COMPATIBILITY + _root.prettyPrintTo (dest); +#else + serializeJsonPretty(_root, dest); +#endif + return len; + } +}; + +typedef std::function ArJsonRequestHandlerFunction; + +class AsyncCallbackJsonWebHandler: public AsyncWebHandler { +private: +protected: + const String _uri; + WebRequestMethodComposite _method; + ArJsonRequestHandlerFunction _onRequest; + size_t _contentLength; +#ifndef ARDUINOJSON_5_COMPATIBILITY + const size_t maxJsonBufferSize; +#endif + size_t _maxContentLength; +public: +#ifdef ARDUINOJSON_5_COMPATIBILITY + AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest) + : _uri(uri), _method(HTTP_POST|HTTP_PUT|HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {} +#else + AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize=DYNAMIC_JSON_DOCUMENT_SIZE) + : _uri(uri), _method(HTTP_POST|HTTP_PUT|HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {} +#endif + + void setMethod(WebRequestMethodComposite method){ _method = method; } + void setMaxContentLength(int maxContentLength){ _maxContentLength = maxContentLength; } + void onRequest(ArJsonRequestHandlerFunction fn){ _onRequest = fn; } + + virtual bool canHandle(AsyncWebServerRequest *request) override final{ + if(!_onRequest) + return false; + + if(!(_method & request->method())) + return false; + + if(_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri+"/"))) + return false; + + if ( !request->contentType().equalsIgnoreCase(JSON_MIMETYPE) ) + return false; + + request->addInterestingHeader("ANY"); + return true; + } + + virtual void handleRequest(AsyncWebServerRequest *request) override final { + if(_onRequest) { + if (request->_tempObject != NULL) { + +#ifdef ARDUINOJSON_5_COMPATIBILITY + DynamicJsonBuffer jsonBuffer; + JsonVariant json = jsonBuffer.parse((uint8_t*)(request->_tempObject)); + if (json.success()) { +#else + DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); + DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject)); + if(!error) { + JsonVariant json = jsonBuffer.as(); +#endif + + _onRequest(request, json); + return; + } + } + request->send(_contentLength > _maxContentLength ? 413 : 400); + } else { + request->send(500); + } + } + virtual void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) override final { + } + virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final { + if (_onRequest) { + _contentLength = total; + if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { + request->_tempObject = malloc(total); + } + if (request->_tempObject != NULL) { + memcpy((uint8_t*)(request->_tempObject) + index, data, len); + } + } + } + virtual bool isRequestHandlerTrivial() override final {return _onRequest ? false : true;} +}; +#endif diff --git a/yoRadio/src/AsyncWebServer/AsyncTCP.cpp b/yoRadio/src/AsyncWebServer/AsyncTCP.cpp new file mode 100644 index 0000000..220b4de --- /dev/null +++ b/yoRadio/src/AsyncWebServer/AsyncTCP.cpp @@ -0,0 +1,1357 @@ +/* + Asynchronous TCP library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "Arduino.h" + +#include "AsyncTCP.h" +extern "C"{ +#include "lwip/opt.h" +#include "lwip/tcp.h" +#include "lwip/inet.h" +#include "lwip/dns.h" +#include "lwip/err.h" +} +#include "esp_task_wdt.h" + +/* + * TCP/IP Event Task + * */ + +typedef enum { + LWIP_TCP_SENT, LWIP_TCP_RECV, LWIP_TCP_FIN, LWIP_TCP_ERROR, LWIP_TCP_POLL, LWIP_TCP_CLEAR, LWIP_TCP_ACCEPT, LWIP_TCP_CONNECTED, LWIP_TCP_DNS +} lwip_event_t; + +typedef struct { + lwip_event_t event; + void *arg; + union { + struct { + void * pcb; + int8_t err; + } connected; + struct { + int8_t err; + } error; + struct { + tcp_pcb * pcb; + uint16_t len; + } sent; + struct { + tcp_pcb * pcb; + pbuf * pb; + int8_t err; + } recv; + struct { + tcp_pcb * pcb; + int8_t err; + } fin; + struct { + tcp_pcb * pcb; + } poll; + struct { + AsyncClient * client; + } accept; + struct { + const char * name; + ip_addr_t addr; + } dns; + }; +} lwip_event_packet_t; + +static xQueueHandle _async_queue; +static TaskHandle_t _async_service_task_handle = NULL; + + +SemaphoreHandle_t _slots_lock; +const int _number_of_closed_slots = CONFIG_LWIP_MAX_ACTIVE_TCP; +static uint32_t _closed_slots[_number_of_closed_slots]; +static uint32_t _closed_index = []() { + _slots_lock = xSemaphoreCreateBinary(); + xSemaphoreGive(_slots_lock); + for (int i = 0; i < _number_of_closed_slots; ++ i) { + _closed_slots[i] = 1; + } + return 1; +}(); + + +static inline bool _init_async_event_queue(){ + if(!_async_queue){ + _async_queue = xQueueCreate(32, sizeof(lwip_event_packet_t *)); + if(!_async_queue){ + return false; + } + } + return true; +} + +static inline bool _send_async_event(lwip_event_packet_t ** e){ + return _async_queue && xQueueSend(_async_queue, e, portMAX_DELAY) == pdPASS; +} + +static inline bool _prepend_async_event(lwip_event_packet_t ** e){ + return _async_queue && xQueueSendToFront(_async_queue, e, portMAX_DELAY) == pdPASS; +} + +static inline bool _get_async_event(lwip_event_packet_t ** e){ + return _async_queue && xQueueReceive(_async_queue, e, portMAX_DELAY) == pdPASS; +} + +static bool _remove_events_with_arg(void * arg){ + lwip_event_packet_t * first_packet = NULL; + lwip_event_packet_t * packet = NULL; + + if(!_async_queue){ + return false; + } + //figure out which is the first packet so we can keep the order + while(!first_packet){ + if(xQueueReceive(_async_queue, &first_packet, 0) != pdPASS){ + return false; + } + //discard packet if matching + if((int)first_packet->arg == (int)arg){ + free(first_packet); + first_packet = NULL; + //return first packet to the back of the queue + } else if(xQueueSend(_async_queue, &first_packet, portMAX_DELAY) != pdPASS){ + return false; + } + } + + while(xQueuePeek(_async_queue, &packet, 0) == pdPASS && packet != first_packet){ + if(xQueueReceive(_async_queue, &packet, 0) != pdPASS){ + return false; + } + if((int)packet->arg == (int)arg){ + free(packet); + packet = NULL; + } else if(xQueueSend(_async_queue, &packet, portMAX_DELAY) != pdPASS){ + return false; + } + } + return true; +} + +static void _handle_async_event(lwip_event_packet_t * e){ + if(e->arg == NULL){ + // do nothing when arg is NULL + //ets_printf("event arg == NULL: 0x%08x\n", e->recv.pcb); + } else if(e->event == LWIP_TCP_CLEAR){ + _remove_events_with_arg(e->arg); + } else if(e->event == LWIP_TCP_RECV){ + //ets_printf("-R: 0x%08x\n", e->recv.pcb); + AsyncClient::_s_recv(e->arg, e->recv.pcb, e->recv.pb, e->recv.err); + } else if(e->event == LWIP_TCP_FIN){ + //ets_printf("-F: 0x%08x\n", e->fin.pcb); + AsyncClient::_s_fin(e->arg, e->fin.pcb, e->fin.err); + } else if(e->event == LWIP_TCP_SENT){ + //ets_printf("-S: 0x%08x\n", e->sent.pcb); + AsyncClient::_s_sent(e->arg, e->sent.pcb, e->sent.len); + } else if(e->event == LWIP_TCP_POLL){ + //ets_printf("-P: 0x%08x\n", e->poll.pcb); + AsyncClient::_s_poll(e->arg, e->poll.pcb); + } else if(e->event == LWIP_TCP_ERROR){ + //ets_printf("-E: 0x%08x %d\n", e->arg, e->error.err); + AsyncClient::_s_error(e->arg, e->error.err); + } else if(e->event == LWIP_TCP_CONNECTED){ + //ets_printf("C: 0x%08x 0x%08x %d\n", e->arg, e->connected.pcb, e->connected.err); + AsyncClient::_s_connected(e->arg, e->connected.pcb, e->connected.err); + } else if(e->event == LWIP_TCP_ACCEPT){ + //ets_printf("A: 0x%08x 0x%08x\n", e->arg, e->accept.client); + AsyncServer::_s_accepted(e->arg, e->accept.client); + } else if(e->event == LWIP_TCP_DNS){ + //ets_printf("D: 0x%08x %s = %s\n", e->arg, e->dns.name, ipaddr_ntoa(&e->dns.addr)); + AsyncClient::_s_dns_found(e->dns.name, &e->dns.addr, e->arg); + } + free((void*)(e)); +} + +static void _async_service_task(void *pvParameters){ + lwip_event_packet_t * packet = NULL; + for (;;) { + if(_get_async_event(&packet)){ +#if CONFIG_ASYNC_TCP_USE_WDT + if(esp_task_wdt_add(NULL) != ESP_OK){ + log_e("Failed to add async task to WDT"); + } +#endif + _handle_async_event(packet); +#if CONFIG_ASYNC_TCP_USE_WDT + if(esp_task_wdt_delete(NULL) != ESP_OK){ + log_e("Failed to remove loop task from WDT"); + } +#endif + } + } + vTaskDelete(NULL); + _async_service_task_handle = NULL; +} +/* +static void _stop_async_task(){ + if(_async_service_task_handle){ + vTaskDelete(_async_service_task_handle); + _async_service_task_handle = NULL; + } +} +*/ +static bool _start_async_task(){ + if(!_init_async_event_queue()){ + return false; + } + if(!_async_service_task_handle){ + xTaskCreateUniversal(_async_service_task, "async_tcp", XTASK_MEM_SIZE, NULL, 3, &_async_service_task_handle, CONFIG_ASYNC_TCP_RUNNING_CORE); + if(!_async_service_task_handle){ + return false; + } + } + return true; +} + +/* + * LwIP Callbacks + * */ + +static int8_t _tcp_clear_events(void * arg) { + lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); + e->event = LWIP_TCP_CLEAR; + e->arg = arg; + if (!_prepend_async_event(&e)) { + free((void*)(e)); + } + return ERR_OK; +} + +static int8_t _tcp_connected(void * arg, tcp_pcb * pcb, int8_t err) { + //ets_printf("+C: 0x%08x\n", pcb); + lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); + e->event = LWIP_TCP_CONNECTED; + e->arg = arg; + e->connected.pcb = pcb; + e->connected.err = err; + if (!_prepend_async_event(&e)) { + free((void*)(e)); + } + return ERR_OK; +} + +static int8_t _tcp_poll(void * arg, struct tcp_pcb * pcb) { + //ets_printf("+P: 0x%08x\n", pcb); + lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); + e->event = LWIP_TCP_POLL; + e->arg = arg; + e->poll.pcb = pcb; + if (!_send_async_event(&e)) { + free((void*)(e)); + } + return ERR_OK; +} + +static int8_t _tcp_recv(void * arg, struct tcp_pcb * pcb, struct pbuf *pb, int8_t err) { + lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); + e->arg = arg; + if(pb){ + //ets_printf("+R: 0x%08x\n", pcb); + e->event = LWIP_TCP_RECV; + e->recv.pcb = pcb; + e->recv.pb = pb; + e->recv.err = err; + } else { + //ets_printf("+F: 0x%08x\n", pcb); + e->event = LWIP_TCP_FIN; + e->fin.pcb = pcb; + e->fin.err = err; + //close the PCB in LwIP thread + AsyncClient::_s_lwip_fin(e->arg, e->fin.pcb, e->fin.err); + } + if (!_send_async_event(&e)) { + free((void*)(e)); + } + return ERR_OK; +} + +static int8_t _tcp_sent(void * arg, struct tcp_pcb * pcb, uint16_t len) { + //ets_printf("+S: 0x%08x\n", pcb); + lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); + e->event = LWIP_TCP_SENT; + e->arg = arg; + e->sent.pcb = pcb; + e->sent.len = len; + if (!_send_async_event(&e)) { + free((void*)(e)); + } + return ERR_OK; +} + +static void _tcp_error(void * arg, int8_t err) { + //ets_printf("+E: 0x%08x\n", arg); + lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); + e->event = LWIP_TCP_ERROR; + e->arg = arg; + e->error.err = err; + if (!_send_async_event(&e)) { + free((void*)(e)); + } +} + +static void _tcp_dns_found(const char * name, struct ip_addr * ipaddr, void * arg) { + lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); + //ets_printf("+DNS: name=%s ipaddr=0x%08x arg=%x\n", name, ipaddr, arg); + e->event = LWIP_TCP_DNS; + e->arg = arg; + e->dns.name = name; + if (ipaddr) { + memcpy(&e->dns.addr, ipaddr, sizeof(struct ip_addr)); + } else { + memset(&e->dns.addr, 0, sizeof(e->dns.addr)); + } + if (!_send_async_event(&e)) { + free((void*)(e)); + } +} + +//Used to switch out from LwIP thread +static int8_t _tcp_accept(void * arg, AsyncClient * client) { + lwip_event_packet_t * e = (lwip_event_packet_t *)malloc(sizeof(lwip_event_packet_t)); + e->event = LWIP_TCP_ACCEPT; + e->arg = arg; + e->accept.client = client; + if (!_prepend_async_event(&e)) { + free((void*)(e)); + } + return ERR_OK; +} + +/* + * TCP/IP API Calls + * */ + +#include "lwip/priv/tcpip_priv.h" + +typedef struct { + struct tcpip_api_call_data call; + tcp_pcb * pcb; + int8_t closed_slot; + int8_t err; + union { + struct { + const char* data; + size_t size; + uint8_t apiflags; + } write; + size_t received; + struct { + ip_addr_t * addr; + uint16_t port; + tcp_connected_fn cb; + } connect; + struct { + ip_addr_t * addr; + uint16_t port; + } bind; + uint8_t backlog; + }; +} tcp_api_call_t; + +static err_t _tcp_output_api(struct tcpip_api_call_data *api_call_msg){ + tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if(msg->closed_slot == -1 || !_closed_slots[msg->closed_slot]) { + msg->err = tcp_output(msg->pcb); + } + return msg->err; +} + +static esp_err_t _tcp_output(tcp_pcb * pcb, int8_t closed_slot) { + if(!pcb){ + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + tcpip_api_call(_tcp_output_api, (struct tcpip_api_call_data*)&msg); + return msg.err; +} + +static err_t _tcp_write_api(struct tcpip_api_call_data *api_call_msg){ + tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if(msg->closed_slot == -1 || !_closed_slots[msg->closed_slot]) { + msg->err = tcp_write(msg->pcb, msg->write.data, msg->write.size, msg->write.apiflags); + } + return msg->err; +} + +static esp_err_t _tcp_write(tcp_pcb * pcb, int8_t closed_slot, const char* data, size_t size, uint8_t apiflags) { + if(!pcb){ + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + msg.write.data = data; + msg.write.size = size; + msg.write.apiflags = apiflags; + tcpip_api_call(_tcp_write_api, (struct tcpip_api_call_data*)&msg); + return msg.err; +} + +static err_t _tcp_recved_api(struct tcpip_api_call_data *api_call_msg){ + tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if(msg->closed_slot == -1 || !_closed_slots[msg->closed_slot]) { + msg->err = 0; + tcp_recved(msg->pcb, msg->received); + } + return msg->err; +} + +static esp_err_t _tcp_recved(tcp_pcb * pcb, int8_t closed_slot, size_t len) { + if(!pcb){ + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + msg.received = len; + tcpip_api_call(_tcp_recved_api, (struct tcpip_api_call_data*)&msg); + return msg.err; +} + +static err_t _tcp_close_api(struct tcpip_api_call_data *api_call_msg){ + tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if(msg->closed_slot == -1 || !_closed_slots[msg->closed_slot]) { + msg->err = tcp_close(msg->pcb); + } + return msg->err; +} + +static esp_err_t _tcp_close(tcp_pcb * pcb, int8_t closed_slot) { + if(!pcb){ + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + tcpip_api_call(_tcp_close_api, (struct tcpip_api_call_data*)&msg); + return msg.err; +} + +static err_t _tcp_abort_api(struct tcpip_api_call_data *api_call_msg){ + tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if(msg->closed_slot == -1 || !_closed_slots[msg->closed_slot]) { + tcp_abort(msg->pcb); + } + return msg->err; +} + +static esp_err_t _tcp_abort(tcp_pcb * pcb, int8_t closed_slot) { + if(!pcb){ + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + tcpip_api_call(_tcp_abort_api, (struct tcpip_api_call_data*)&msg); + return msg.err; +} + +static err_t _tcp_connect_api(struct tcpip_api_call_data *api_call_msg){ + tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; + msg->err = tcp_connect(msg->pcb, msg->connect.addr, msg->connect.port, msg->connect.cb); + return msg->err; +} + +static esp_err_t _tcp_connect(tcp_pcb * pcb, int8_t closed_slot, ip_addr_t * addr, uint16_t port, tcp_connected_fn cb) { + if(!pcb){ + return ESP_FAIL; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + msg.connect.addr = addr; + msg.connect.port = port; + msg.connect.cb = cb; + tcpip_api_call(_tcp_connect_api, (struct tcpip_api_call_data*)&msg); + return msg.err; +} + +static err_t _tcp_bind_api(struct tcpip_api_call_data *api_call_msg){ + tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; + msg->err = tcp_bind(msg->pcb, msg->bind.addr, msg->bind.port); + return msg->err; +} + +static esp_err_t _tcp_bind(tcp_pcb * pcb, ip_addr_t * addr, uint16_t port) { + if(!pcb){ + return ESP_FAIL; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = -1; + msg.bind.addr = addr; + msg.bind.port = port; + tcpip_api_call(_tcp_bind_api, (struct tcpip_api_call_data*)&msg); + return msg.err; +} + +static err_t _tcp_listen_api(struct tcpip_api_call_data *api_call_msg){ + tcp_api_call_t * msg = (tcp_api_call_t *)api_call_msg; + msg->err = 0; + msg->pcb = tcp_listen_with_backlog(msg->pcb, msg->backlog); + return msg->err; +} + +static tcp_pcb * _tcp_listen_with_backlog(tcp_pcb * pcb, uint8_t backlog) { + if(!pcb){ + return NULL; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = -1; + msg.backlog = backlog?backlog:0xFF; + tcpip_api_call(_tcp_listen_api, (struct tcpip_api_call_data*)&msg); + return msg.pcb; +} + + + +/* + Async TCP Client + */ + +AsyncClient::AsyncClient(tcp_pcb* pcb) +: _connect_cb(0) +, _connect_cb_arg(0) +, _discard_cb(0) +, _discard_cb_arg(0) +, _sent_cb(0) +, _sent_cb_arg(0) +, _error_cb(0) +, _error_cb_arg(0) +, _recv_cb(0) +, _recv_cb_arg(0) +, _pb_cb(0) +, _pb_cb_arg(0) +, _timeout_cb(0) +, _timeout_cb_arg(0) +, _pcb_busy(false) +, _pcb_sent_at(0) +, _ack_pcb(true) +, _rx_last_packet(0) +, _rx_since_timeout(0) +, _ack_timeout(ASYNC_MAX_ACK_TIME) +, _connect_port(0) +, prev(NULL) +, next(NULL) +{ + _pcb = pcb; + _closed_slot = -1; + if(_pcb){ + _allocate_closed_slot(); + _rx_last_packet = millis(); + tcp_arg(_pcb, this); + tcp_recv(_pcb, &_tcp_recv); + tcp_sent(_pcb, &_tcp_sent); + tcp_err(_pcb, &_tcp_error); + tcp_poll(_pcb, &_tcp_poll, 1); + } +} + +AsyncClient::~AsyncClient(){ + if(_pcb) { + _close(); + } + _free_closed_slot(); +} + +/* + * Operators + * */ + +AsyncClient& AsyncClient::operator=(const AsyncClient& other){ + if (_pcb) { + _close(); + } + + _pcb = other._pcb; + _closed_slot = other._closed_slot; + if (_pcb) { + _rx_last_packet = millis(); + tcp_arg(_pcb, this); + tcp_recv(_pcb, &_tcp_recv); + tcp_sent(_pcb, &_tcp_sent); + tcp_err(_pcb, &_tcp_error); + tcp_poll(_pcb, &_tcp_poll, 1); + } + return *this; +} + +bool AsyncClient::operator==(const AsyncClient &other) { + return _pcb == other._pcb; +} + +AsyncClient & AsyncClient::operator+=(const AsyncClient &other) { + if(next == NULL){ + next = (AsyncClient*)(&other); + next->prev = this; + } else { + AsyncClient *c = next; + while(c->next != NULL) { + c = c->next; + } + c->next =(AsyncClient*)(&other); + c->next->prev = c; + } + return *this; +} + +/* + * Callback Setters + * */ + +void AsyncClient::onConnect(AcConnectHandler cb, void* arg){ + _connect_cb = cb; + _connect_cb_arg = arg; +} + +void AsyncClient::onDisconnect(AcConnectHandler cb, void* arg){ + _discard_cb = cb; + _discard_cb_arg = arg; +} + +void AsyncClient::onAck(AcAckHandler cb, void* arg){ + _sent_cb = cb; + _sent_cb_arg = arg; +} + +void AsyncClient::onError(AcErrorHandler cb, void* arg){ + _error_cb = cb; + _error_cb_arg = arg; +} + +void AsyncClient::onData(AcDataHandler cb, void* arg){ + _recv_cb = cb; + _recv_cb_arg = arg; +} + +void AsyncClient::onPacket(AcPacketHandler cb, void* arg){ + _pb_cb = cb; + _pb_cb_arg = arg; +} + +void AsyncClient::onTimeout(AcTimeoutHandler cb, void* arg){ + _timeout_cb = cb; + _timeout_cb_arg = arg; +} + +void AsyncClient::onPoll(AcConnectHandler cb, void* arg){ + _poll_cb = cb; + _poll_cb_arg = arg; +} + +/* + * Main Public Methods + * */ + +bool AsyncClient::connect(IPAddress ip, uint16_t port){ + if (_pcb){ + log_w("already connected, state %d", _pcb->state); + return false; + } + if(!_start_async_task()){ + log_e("failed to start task"); + return false; + } + + ip_addr_t addr; + addr.type = IPADDR_TYPE_V4; + addr.u_addr.ip4.addr = ip; + + tcp_pcb* pcb = tcp_new_ip_type(IPADDR_TYPE_V4); + if (!pcb){ + log_e("pcb == NULL"); + return false; + } + + tcp_arg(pcb, this); + tcp_err(pcb, &_tcp_error); + tcp_recv(pcb, &_tcp_recv); + tcp_sent(pcb, &_tcp_sent); + tcp_poll(pcb, &_tcp_poll, 1); + //_tcp_connect(pcb, &addr, port,(tcp_connected_fn)&_s_connected); + _tcp_connect(pcb, _closed_slot, &addr, port,(tcp_connected_fn)&_tcp_connected); + return true; +} + +bool AsyncClient::connect(const char* host, uint16_t port){ + ip_addr_t addr; + + if(!_start_async_task()){ + log_e("failed to start task"); + return false; + } + + err_t err = dns_gethostbyname(host, &addr, (dns_found_callback)&_tcp_dns_found, this); + if(err == ERR_OK) { + return connect(IPAddress(addr.u_addr.ip4.addr), port); + } else if(err == ERR_INPROGRESS) { + _connect_port = port; + return true; + } + log_e("error: %d", err); + return false; +} + +void AsyncClient::close(bool now){ + if(_pcb){ + _tcp_recved(_pcb, _closed_slot, _rx_ack_len); + } + _close(); +} + +int8_t AsyncClient::abort(){ + if(_pcb) { + _tcp_abort(_pcb, _closed_slot ); + _pcb = NULL; + } + return ERR_ABRT; +} + +size_t AsyncClient::space(){ + if((_pcb != NULL) && (_pcb->state == 4)){ + return tcp_sndbuf(_pcb); + } + return 0; +} + +size_t AsyncClient::add(const char* data, size_t size, uint8_t apiflags) { + if(!_pcb || size == 0 || data == NULL) { + return 0; + } + size_t room = space(); + if(!room) { + return 0; + } + size_t will_send = (room < size) ? room : size; + int8_t err = ERR_OK; + err = _tcp_write(_pcb, _closed_slot, data, will_send, apiflags); + if(err != ERR_OK) { + return 0; + } + return will_send; +} + +bool AsyncClient::send(){ + int8_t err = ERR_OK; + err = _tcp_output(_pcb, _closed_slot); + if(err == ERR_OK){ + _pcb_busy = true; + _pcb_sent_at = millis(); + return true; + } + return false; +} + +size_t AsyncClient::ack(size_t len){ + if(len > _rx_ack_len) + len = _rx_ack_len; + if(len){ + _tcp_recved(_pcb, _closed_slot, len); + } + _rx_ack_len -= len; + return len; +} + +void AsyncClient::ackPacket(struct pbuf * pb){ + if(!pb){ + return; + } + _tcp_recved(_pcb, _closed_slot, pb->len); + pbuf_free(pb); +} + +/* + * Main Private Methods + * */ + +int8_t AsyncClient::_close(){ + //ets_printf("X: 0x%08x\n", (uint32_t)this); + int8_t err = ERR_OK; + if(_pcb) { + //log_i(""); + tcp_arg(_pcb, NULL); + tcp_sent(_pcb, NULL); + tcp_recv(_pcb, NULL); + tcp_err(_pcb, NULL); + tcp_poll(_pcb, NULL, 0); + _tcp_clear_events(this); + err = _tcp_close(_pcb, _closed_slot); + if(err != ERR_OK) { + err = abort(); + } + _pcb = NULL; + if(_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + } + return err; +} + +void AsyncClient::_allocate_closed_slot(){ + xSemaphoreTake(_slots_lock, portMAX_DELAY); + uint32_t closed_slot_min_index = 0; + for (int i = 0; i < _number_of_closed_slots; ++ i) { + if ((_closed_slot == -1 || _closed_slots[i] <= closed_slot_min_index) && _closed_slots[i] != 0) { + closed_slot_min_index = _closed_slots[i]; + _closed_slot = i; + } + } + if (_closed_slot != -1) { + _closed_slots[_closed_slot] = 0; + } + xSemaphoreGive(_slots_lock); +} + +void AsyncClient::_free_closed_slot(){ + if (_closed_slot != -1) { + _closed_slots[_closed_slot] = _closed_index; + _closed_slot = -1; + ++ _closed_index; + } +} + +/* + * Private Callbacks + * */ + +int8_t AsyncClient::_connected(void* pcb, int8_t err){ + _pcb = reinterpret_cast(pcb); + if(_pcb){ + _rx_last_packet = millis(); + _pcb_busy = false; +// tcp_recv(_pcb, &_tcp_recv); +// tcp_sent(_pcb, &_tcp_sent); +// tcp_poll(_pcb, &_tcp_poll, 1); + } + if(_connect_cb) { + _connect_cb(_connect_cb_arg, this); + } + return ERR_OK; +} + +void AsyncClient::_error(int8_t err) { + if(_pcb){ + tcp_arg(_pcb, NULL); + if(_pcb->state == LISTEN) { + tcp_sent(_pcb, NULL); + tcp_recv(_pcb, NULL); + tcp_err(_pcb, NULL); + tcp_poll(_pcb, NULL, 0); + } + _pcb = NULL; + } + if(_error_cb) { + _error_cb(_error_cb_arg, this, err); + } + if(_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } +} + +//In LwIP Thread +int8_t AsyncClient::_lwip_fin(tcp_pcb* pcb, int8_t err) { + if(!_pcb || pcb != _pcb){ + log_e("0x%08x != 0x%08x", (uint32_t)pcb, (uint32_t)_pcb); + return ERR_OK; + } + tcp_arg(_pcb, NULL); + if(_pcb->state == LISTEN) { + tcp_sent(_pcb, NULL); + tcp_recv(_pcb, NULL); + tcp_err(_pcb, NULL); + tcp_poll(_pcb, NULL, 0); + } + if(tcp_close(_pcb) != ERR_OK) { + tcp_abort(_pcb); + } + _free_closed_slot(); + _pcb = NULL; + return ERR_OK; +} + +//In Async Thread +int8_t AsyncClient::_fin(tcp_pcb* pcb, int8_t err) { + _tcp_clear_events(this); + if(_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + return ERR_OK; +} + +int8_t AsyncClient::_sent(tcp_pcb* pcb, uint16_t len) { + _rx_last_packet = millis(); + //log_i("%u", len); + _pcb_busy = false; + if(_sent_cb) { + _sent_cb(_sent_cb_arg, this, len, (millis() - _pcb_sent_at)); + } + return ERR_OK; +} + +int8_t AsyncClient::_recv(tcp_pcb* pcb, pbuf* pb, int8_t err) { + while(pb != NULL) { + _rx_last_packet = millis(); + //we should not ack before we assimilate the data + _ack_pcb = true; + pbuf *b = pb; + pb = b->next; + b->next = NULL; + if(_pb_cb){ + _pb_cb(_pb_cb_arg, this, b); + } else { + if(_recv_cb) { + _recv_cb(_recv_cb_arg, this, b->payload, b->len); + } + if(!_ack_pcb) { + _rx_ack_len += b->len; + } else if(_pcb) { + _tcp_recved(_pcb, _closed_slot, b->len); + } + pbuf_free(b); + } + } + return ERR_OK; +} + +int8_t AsyncClient::_poll(tcp_pcb* pcb){ + if(!_pcb){ + log_w("pcb is NULL"); + return ERR_OK; + } + if(pcb != _pcb){ + log_e("0x%08x != 0x%08x", (uint32_t)pcb, (uint32_t)_pcb); + return ERR_OK; + } + + uint32_t now = millis(); + + // ACK Timeout + if(_pcb_busy && _ack_timeout && (now - _pcb_sent_at) >= _ack_timeout){ + _pcb_busy = false; + log_w("ack timeout %d", pcb->state); + if(_timeout_cb) + _timeout_cb(_timeout_cb_arg, this, (now - _pcb_sent_at)); + return ERR_OK; + } + // RX Timeout + if(_rx_since_timeout && (now - _rx_last_packet) >= (_rx_since_timeout * 1000)){ + log_w("rx timeout %d", pcb->state); + _close(); + return ERR_OK; + } + // Everything is fine + if(_poll_cb) { + _poll_cb(_poll_cb_arg, this); + } + return ERR_OK; +} + +void AsyncClient::_dns_found(struct ip_addr *ipaddr){ + if(ipaddr && ipaddr->u_addr.ip4.addr){ + connect(IPAddress(ipaddr->u_addr.ip4.addr), _connect_port); + } else { + if(_error_cb) { + _error_cb(_error_cb_arg, this, -55); + } + if(_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + } +} + +/* + * Public Helper Methods + * */ + +void AsyncClient::stop() { + close(false); +} + +bool AsyncClient::free(){ + if(!_pcb) { + return true; + } + if(_pcb->state == 0 || _pcb->state > 4) { + return true; + } + return false; +} + +size_t AsyncClient::write(const char* data) { + if(data == NULL) { + return 0; + } + return write(data, strlen(data)); +} + +size_t AsyncClient::write(const char* data, size_t size, uint8_t apiflags) { + size_t will_send = add(data, size, apiflags); + if(!will_send || !send()) { + return 0; + } + return will_send; +} + +void AsyncClient::setRxTimeout(uint32_t timeout){ + _rx_since_timeout = timeout; +} + +uint32_t AsyncClient::getRxTimeout(){ + return _rx_since_timeout; +} + +uint32_t AsyncClient::getAckTimeout(){ + return _ack_timeout; +} + +void AsyncClient::setAckTimeout(uint32_t timeout){ + _ack_timeout = timeout; +} + +void AsyncClient::setNoDelay(bool nodelay){ + if(!_pcb) { + return; + } + if(nodelay) { + tcp_nagle_disable(_pcb); + } else { + tcp_nagle_enable(_pcb); + } +} + +bool AsyncClient::getNoDelay(){ + if(!_pcb) { + return false; + } + return tcp_nagle_disabled(_pcb); +} + +uint16_t AsyncClient::getMss(){ + if(!_pcb) { + return 0; + } + return tcp_mss(_pcb); +} + +uint32_t AsyncClient::getRemoteAddress() { + if(!_pcb) { + return 0; + } + return _pcb->remote_ip.u_addr.ip4.addr; +} + +uint16_t AsyncClient::getRemotePort() { + if(!_pcb) { + return 0; + } + return _pcb->remote_port; +} + +uint32_t AsyncClient::getLocalAddress() { + if(!_pcb) { + return 0; + } + return _pcb->local_ip.u_addr.ip4.addr; +} + +uint16_t AsyncClient::getLocalPort() { + if(!_pcb) { + return 0; + } + return _pcb->local_port; +} + +IPAddress AsyncClient::remoteIP() { + return IPAddress(getRemoteAddress()); +} + +uint16_t AsyncClient::remotePort() { + return getRemotePort(); +} + +IPAddress AsyncClient::localIP() { + return IPAddress(getLocalAddress()); +} + +uint16_t AsyncClient::localPort() { + return getLocalPort(); +} + +uint8_t AsyncClient::state() { + if(!_pcb) { + return 0; + } + return _pcb->state; +} + +bool AsyncClient::connected(){ + if (!_pcb) { + return false; + } + return _pcb->state == 4; +} + +bool AsyncClient::connecting(){ + if (!_pcb) { + return false; + } + return _pcb->state > 0 && _pcb->state < 4; +} + +bool AsyncClient::disconnecting(){ + if (!_pcb) { + return false; + } + return _pcb->state > 4 && _pcb->state < 10; +} + +bool AsyncClient::disconnected(){ + if (!_pcb) { + return true; + } + return _pcb->state == 0 || _pcb->state == 10; +} + +bool AsyncClient::freeable(){ + if (!_pcb) { + return true; + } + return _pcb->state == 0 || _pcb->state > 4; +} + +bool AsyncClient::canSend(){ + return space() > 0; +} + +const char * AsyncClient::errorToString(int8_t error){ + switch(error){ + case ERR_OK: return "OK"; + case ERR_MEM: return "Out of memory error"; + case ERR_BUF: return "Buffer error"; + case ERR_TIMEOUT: return "Timeout"; + case ERR_RTE: return "Routing problem"; + case ERR_INPROGRESS: return "Operation in progress"; + case ERR_VAL: return "Illegal value"; + case ERR_WOULDBLOCK: return "Operation would block"; + case ERR_USE: return "Address in use"; + case ERR_ALREADY: return "Already connected"; + case ERR_CONN: return "Not connected"; + case ERR_IF: return "Low-level netif error"; + case ERR_ABRT: return "Connection aborted"; + case ERR_RST: return "Connection reset"; + case ERR_CLSD: return "Connection closed"; + case ERR_ARG: return "Illegal argument"; + case -55: return "DNS failed"; + default: return "UNKNOWN"; + } +} + +const char * AsyncClient::stateToString(){ + switch(state()){ + case 0: return "Closed"; + case 1: return "Listen"; + case 2: return "SYN Sent"; + case 3: return "SYN Received"; + case 4: return "Established"; + case 5: return "FIN Wait 1"; + case 6: return "FIN Wait 2"; + case 7: return "Close Wait"; + case 8: return "Closing"; + case 9: return "Last ACK"; + case 10: return "Time Wait"; + default: return "UNKNOWN"; + } +} + +/* + * Static Callbacks (LwIP C2C++ interconnect) + * */ + +void AsyncClient::_s_dns_found(const char * name, struct ip_addr * ipaddr, void * arg){ + reinterpret_cast(arg)->_dns_found(ipaddr); +} + +int8_t AsyncClient::_s_poll(void * arg, struct tcp_pcb * pcb) { + return reinterpret_cast(arg)->_poll(pcb); +} + +int8_t AsyncClient::_s_recv(void * arg, struct tcp_pcb * pcb, struct pbuf *pb, int8_t err) { + return reinterpret_cast(arg)->_recv(pcb, pb, err); +} + +int8_t AsyncClient::_s_fin(void * arg, struct tcp_pcb * pcb, int8_t err) { + return reinterpret_cast(arg)->_fin(pcb, err); +} + +int8_t AsyncClient::_s_lwip_fin(void * arg, struct tcp_pcb * pcb, int8_t err) { + return reinterpret_cast(arg)->_lwip_fin(pcb, err); +} + +int8_t AsyncClient::_s_sent(void * arg, struct tcp_pcb * pcb, uint16_t len) { + return reinterpret_cast(arg)->_sent(pcb, len); +} + +void AsyncClient::_s_error(void * arg, int8_t err) { + reinterpret_cast(arg)->_error(err); +} + +int8_t AsyncClient::_s_connected(void * arg, void * pcb, int8_t err){ + return reinterpret_cast(arg)->_connected(pcb, err); +} + +/* + Async TCP Server + */ + +AsyncServer::AsyncServer(IPAddress addr, uint16_t port) +: _port(port) +, _addr(addr) +, _noDelay(false) +, _pcb(0) +, _connect_cb(0) +, _connect_cb_arg(0) +{} + +AsyncServer::AsyncServer(uint16_t port) +: _port(port) +, _addr((uint32_t) IPADDR_ANY) +, _noDelay(false) +, _pcb(0) +, _connect_cb(0) +, _connect_cb_arg(0) +{} + +AsyncServer::~AsyncServer(){ + end(); +} + +void AsyncServer::onClient(AcConnectHandler cb, void* arg){ + _connect_cb = cb; + _connect_cb_arg = arg; +} + +void AsyncServer::begin(){ + if(_pcb) { + return; + } + + if(!_start_async_task()){ + log_e("failed to start task"); + return; + } + int8_t err; + _pcb = tcp_new_ip_type(IPADDR_TYPE_V4); + if (!_pcb){ + log_e("_pcb == NULL"); + return; + } + + ip_addr_t local_addr; + local_addr.type = IPADDR_TYPE_V4; + local_addr.u_addr.ip4.addr = (uint32_t) _addr; + err = _tcp_bind(_pcb, &local_addr, _port); + + if (err != ERR_OK) { + _tcp_close(_pcb, -1); + log_e("bind error: %d", err); + return; + } + + static uint8_t backlog = 5; + _pcb = _tcp_listen_with_backlog(_pcb, backlog); + if (!_pcb) { + log_e("listen_pcb == NULL"); + return; + } + tcp_arg(_pcb, (void*) this); + tcp_accept(_pcb, &_s_accept); +} + +void AsyncServer::end(){ + if(_pcb){ + tcp_arg(_pcb, NULL); + tcp_accept(_pcb, NULL); + if(tcp_close(_pcb) != ERR_OK){ + _tcp_abort(_pcb, -1); + } + _pcb = NULL; + } +} + +//runs on LwIP thread +int8_t AsyncServer::_accept(tcp_pcb* pcb, int8_t err){ + //ets_printf("+A: 0x%08x\n", pcb); + if(_connect_cb){ + AsyncClient *c = new AsyncClient(pcb); + if(c){ + c->setNoDelay(_noDelay); + return _tcp_accept(this, c); + } + } + if(tcp_close(pcb) != ERR_OK){ + tcp_abort(pcb); + } + log_e("FAIL"); + return ERR_OK; +} + +int8_t AsyncServer::_accepted(AsyncClient* client){ + if(_connect_cb){ + _connect_cb(_connect_cb_arg, client); + } + return ERR_OK; +} + +void AsyncServer::setNoDelay(bool nodelay){ + _noDelay = nodelay; +} + +bool AsyncServer::getNoDelay(){ + return _noDelay; +} + +uint8_t AsyncServer::status(){ + if (!_pcb) { + return 0; + } + return _pcb->state; +} + +int8_t AsyncServer::_s_accept(void * arg, tcp_pcb * pcb, int8_t err){ + return reinterpret_cast(arg)->_accept(pcb, err); +} + +int8_t AsyncServer::_s_accepted(void *arg, AsyncClient* client){ + return reinterpret_cast(arg)->_accepted(client); +} diff --git a/yoRadio/src/AsyncWebServer/AsyncTCP.h b/yoRadio/src/AsyncWebServer/AsyncTCP.h new file mode 100644 index 0000000..28d6568 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/AsyncTCP.h @@ -0,0 +1,219 @@ +/* + Asynchronous TCP library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef ASYNCTCP_H_ +#define ASYNCTCP_H_ +#include "../core/options.h" +#include "IPAddress.h" +#include "sdkconfig.h" +#include +extern "C" { + #include "freertos/semphr.h" + #include "lwip/pbuf.h" +} + +//If core is not defined, then we are running in Arduino or PIO +#ifndef CONFIG_ASYNC_TCP_RUNNING_CORE +#define CONFIG_ASYNC_TCP_RUNNING_CORE -1 //any available core +#define CONFIG_ASYNC_TCP_USE_WDT 1 //if enabled, adds between 33us and 200us per event +#endif +#ifndef XTASK_MEM_SIZE + #define XTASK_MEM_SIZE 8192 / 2 +#endif +class AsyncClient; + +#define ASYNC_MAX_ACK_TIME 5000 +#define ASYNC_WRITE_FLAG_COPY 0x01 //will allocate new buffer to hold the data while sending (else will hold reference to the data given) +#define ASYNC_WRITE_FLAG_MORE 0x02 //will not send PSH flag, meaning that there should be more data to be sent before the application should react. + +typedef std::function AcConnectHandler; +typedef std::function AcAckHandler; +typedef std::function AcErrorHandler; +typedef std::function AcDataHandler; +typedef std::function AcPacketHandler; +typedef std::function AcTimeoutHandler; + +struct tcp_pcb; +struct ip_addr; + +class AsyncClient { + public: + AsyncClient(tcp_pcb* pcb = 0); + ~AsyncClient(); + + AsyncClient & operator=(const AsyncClient &other); + AsyncClient & operator+=(const AsyncClient &other); + + bool operator==(const AsyncClient &other); + + bool operator!=(const AsyncClient &other) { + return !(*this == other); + } + bool connect(IPAddress ip, uint16_t port); + bool connect(const char* host, uint16_t port); + void close(bool now = false); + void stop(); + int8_t abort(); + bool free(); + + bool canSend();//ack is not pending + size_t space();//space available in the TCP window + size_t add(const char* data, size_t size, uint8_t apiflags=ASYNC_WRITE_FLAG_COPY);//add for sending + bool send();//send all data added with the method above + + //write equals add()+send() + size_t write(const char* data); + size_t write(const char* data, size_t size, uint8_t apiflags=ASYNC_WRITE_FLAG_COPY); //only when canSend() == true + + uint8_t state(); + bool connecting(); + bool connected(); + bool disconnecting(); + bool disconnected(); + bool freeable();//disconnected or disconnecting + + uint16_t getMss(); + + uint32_t getRxTimeout(); + void setRxTimeout(uint32_t timeout);//no RX data timeout for the connection in seconds + + uint32_t getAckTimeout(); + void setAckTimeout(uint32_t timeout);//no ACK timeout for the last sent packet in milliseconds + + void setNoDelay(bool nodelay); + bool getNoDelay(); + + uint32_t getRemoteAddress(); + uint16_t getRemotePort(); + uint32_t getLocalAddress(); + uint16_t getLocalPort(); + + //compatibility + IPAddress remoteIP(); + uint16_t remotePort(); + IPAddress localIP(); + uint16_t localPort(); + + void onConnect(AcConnectHandler cb, void* arg = 0); //on successful connect + void onDisconnect(AcConnectHandler cb, void* arg = 0); //disconnected + void onAck(AcAckHandler cb, void* arg = 0); //ack received + void onError(AcErrorHandler cb, void* arg = 0); //unsuccessful connect or error + void onData(AcDataHandler cb, void* arg = 0); //data received (called if onPacket is not used) + void onPacket(AcPacketHandler cb, void* arg = 0); //data received + void onTimeout(AcTimeoutHandler cb, void* arg = 0); //ack timeout + void onPoll(AcConnectHandler cb, void* arg = 0); //every 125ms when connected + + void ackPacket(struct pbuf * pb);//ack pbuf from onPacket + size_t ack(size_t len); //ack data that you have not acked using the method below + void ackLater(){ _ack_pcb = false; } //will not ack the current packet. Call from onData + + const char * errorToString(int8_t error); + const char * stateToString(); + + //Do not use any of the functions below! + static int8_t _s_poll(void *arg, struct tcp_pcb *tpcb); + static int8_t _s_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *pb, int8_t err); + static int8_t _s_fin(void *arg, struct tcp_pcb *tpcb, int8_t err); + static int8_t _s_lwip_fin(void *arg, struct tcp_pcb *tpcb, int8_t err); + static void _s_error(void *arg, int8_t err); + static int8_t _s_sent(void *arg, struct tcp_pcb *tpcb, uint16_t len); + static int8_t _s_connected(void* arg, void* tpcb, int8_t err); + static void _s_dns_found(const char *name, struct ip_addr *ipaddr, void *arg); + + int8_t _recv(tcp_pcb* pcb, pbuf* pb, int8_t err); + tcp_pcb * pcb(){ return _pcb; } + + protected: + tcp_pcb* _pcb; + int8_t _closed_slot; + + AcConnectHandler _connect_cb; + void* _connect_cb_arg; + AcConnectHandler _discard_cb; + void* _discard_cb_arg; + AcAckHandler _sent_cb; + void* _sent_cb_arg; + AcErrorHandler _error_cb; + void* _error_cb_arg; + AcDataHandler _recv_cb; + void* _recv_cb_arg; + AcPacketHandler _pb_cb; + void* _pb_cb_arg; + AcTimeoutHandler _timeout_cb; + void* _timeout_cb_arg; + AcConnectHandler _poll_cb; + void* _poll_cb_arg; + + bool _pcb_busy; + uint32_t _pcb_sent_at; + bool _ack_pcb; + uint32_t _rx_ack_len; + uint32_t _rx_last_packet; + uint32_t _rx_since_timeout; + uint32_t _ack_timeout; + uint16_t _connect_port; + + int8_t _close(); + void _free_closed_slot(); + void _allocate_closed_slot(); + int8_t _connected(void* pcb, int8_t err); + void _error(int8_t err); + int8_t _poll(tcp_pcb* pcb); + int8_t _sent(tcp_pcb* pcb, uint16_t len); + int8_t _fin(tcp_pcb* pcb, int8_t err); + int8_t _lwip_fin(tcp_pcb* pcb, int8_t err); + void _dns_found(struct ip_addr *ipaddr); + + public: + AsyncClient* prev; + AsyncClient* next; +}; + +class AsyncServer { + public: + AsyncServer(IPAddress addr, uint16_t port); + AsyncServer(uint16_t port); + ~AsyncServer(); + void onClient(AcConnectHandler cb, void* arg); + void begin(); + void end(); + void setNoDelay(bool nodelay); + bool getNoDelay(); + uint8_t status(); + + //Do not use any of the functions below! + static int8_t _s_accept(void *arg, tcp_pcb* newpcb, int8_t err); + static int8_t _s_accepted(void *arg, AsyncClient* client); + + protected: + uint16_t _port; + IPAddress _addr; + bool _noDelay; + tcp_pcb* _pcb; + AcConnectHandler _connect_cb; + void* _connect_cb_arg; + + int8_t _accept(tcp_pcb* newpcb, int8_t err); + int8_t _accepted(AsyncClient* client); +}; + + +#endif /* ASYNCTCP_H_ */ diff --git a/yoRadio/src/AsyncWebServer/AsyncWebSocket.cpp b/yoRadio/src/AsyncWebServer/AsyncWebSocket.cpp new file mode 100644 index 0000000..f76f2fc --- /dev/null +++ b/yoRadio/src/AsyncWebServer/AsyncWebSocket.cpp @@ -0,0 +1,1294 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "Arduino.h" +#include "AsyncWebSocket.h" + +#include + +#ifndef ESP8266 +#include "mbedtls/sha1.h" +#else +#include +#endif + +#define MAX_PRINTF_LEN 64 + +size_t webSocketSendFrameWindow(AsyncClient *client){ + if(!client->canSend()) + return 0; + size_t space = client->space(); + if(space < 9) + return 0; + return space - 8; +} + +size_t webSocketSendFrame(AsyncClient *client, bool final, uint8_t opcode, bool mask, uint8_t *data, size_t len){ + if(!client->canSend()) + return 0; + size_t space = client->space(); + if(space < 2) + return 0; + uint8_t mbuf[4] = {0,0,0,0}; + uint8_t headLen = 2; + if(len && mask){ + headLen += 4; + mbuf[0] = rand() % 0xFF; + mbuf[1] = rand() % 0xFF; + mbuf[2] = rand() % 0xFF; + mbuf[3] = rand() % 0xFF; + } + if(len > 125) + headLen += 2; + if(space < headLen) + return 0; + space -= headLen; + + if(len > space) len = space; + + uint8_t *buf = (uint8_t*)malloc(headLen); + if(buf == NULL){ + //os_printf("could not malloc %u bytes for frame header\n", headLen); + return 0; + } + + buf[0] = opcode & 0x0F; + if(final) + buf[0] |= 0x80; + if(len < 126) + buf[1] = len & 0x7F; + else { + buf[1] = 126; + buf[2] = (uint8_t)((len >> 8) & 0xFF); + buf[3] = (uint8_t)(len & 0xFF); + } + if(len && mask){ + buf[1] |= 0x80; + memcpy(buf + (headLen - 4), mbuf, 4); + } + if(client->add((const char *)buf, headLen) != headLen){ + //os_printf("error adding %lu header bytes\n", headLen); + free(buf); + return 0; + } + free(buf); + + if(len){ + if(len && mask){ + size_t i; + for(i=0;iadd((const char *)data, len) != len){ + //os_printf("error adding %lu data bytes\n", len); + return 0; + } + } + if(!client->send()){ + //os_printf("error sending frame: %lu\n", headLen+len); + return 0; + } + return len; +} + + +/* + * AsyncWebSocketMessageBuffer + */ + + + +AsyncWebSocketMessageBuffer::AsyncWebSocketMessageBuffer() + :_data(nullptr) + ,_len(0) + ,_lock(false) + ,_count(0) +{ + +} + +AsyncWebSocketMessageBuffer::AsyncWebSocketMessageBuffer(uint8_t * data, size_t size) + :_data(nullptr) + ,_len(size) + ,_lock(false) + ,_count(0) +{ + + if (!data) { + return; + } + + _data = new uint8_t[_len + 1]; + + if (_data) { + memcpy(_data, data, _len); + _data[_len] = 0; + } +} + + +AsyncWebSocketMessageBuffer::AsyncWebSocketMessageBuffer(size_t size) + :_data(nullptr) + ,_len(size) + ,_lock(false) + ,_count(0) +{ + _data = new uint8_t[_len + 1]; + + if (_data) { + _data[_len] = 0; + } + +} + +AsyncWebSocketMessageBuffer::AsyncWebSocketMessageBuffer(const AsyncWebSocketMessageBuffer & copy) + :_data(nullptr) + ,_len(0) + ,_lock(false) + ,_count(0) +{ + _len = copy._len; + _lock = copy._lock; + _count = 0; + + if (_len) { + _data = new uint8_t[_len + 1]; + _data[_len] = 0; + } + + if (_data) { + memcpy(_data, copy._data, _len); + _data[_len] = 0; + } + +} + +AsyncWebSocketMessageBuffer::AsyncWebSocketMessageBuffer(AsyncWebSocketMessageBuffer && copy) + :_data(nullptr) + ,_len(0) + ,_lock(false) + ,_count(0) +{ + _len = copy._len; + _lock = copy._lock; + _count = 0; + + if (copy._data) { + _data = copy._data; + copy._data = nullptr; + } + +} + +AsyncWebSocketMessageBuffer::~AsyncWebSocketMessageBuffer() +{ + if (_data) { + delete[] _data; + } +} + +bool AsyncWebSocketMessageBuffer::reserve(size_t size) +{ + _len = size; + + if (_data) { + delete[] _data; + _data = nullptr; + } + + _data = new uint8_t[_len + 1]; + + if (_data) { + _data[_len] = 0; + return true; + } else { + return false; + } + +} + + + +/* + * Control Frame + */ + +class AsyncWebSocketControl { + private: + uint8_t _opcode; + uint8_t *_data; + size_t _len; + bool _mask; + bool _finished; + public: + AsyncWebSocketControl(uint8_t opcode, uint8_t *data=NULL, size_t len=0, bool mask=false) + :_opcode(opcode) + ,_len(len) + ,_mask(len && mask) + ,_finished(false) + { + if(data == NULL) + _len = 0; + if(_len){ + if(_len > 125) + _len = 125; + _data = (uint8_t*)malloc(_len); + if(_data == NULL) + _len = 0; + else memcpy(_data, data, len); + } else _data = NULL; + } + virtual ~AsyncWebSocketControl(){ + if(_data != NULL) + free(_data); + } + virtual bool finished() const { return _finished; } + uint8_t opcode(){ return _opcode; } + uint8_t len(){ return _len + 2; } + size_t send(AsyncClient *client){ + _finished = true; + return webSocketSendFrame(client, true, _opcode & 0x0F, _mask, _data, _len); + } +}; + +/* + * Basic Buffered Message + */ + + +AsyncWebSocketBasicMessage::AsyncWebSocketBasicMessage(const char * data, size_t len, uint8_t opcode, bool mask) + :_len(len) + ,_sent(0) + ,_ack(0) + ,_acked(0) +{ + _opcode = opcode & 0x07; + _mask = mask; + _data = (uint8_t*)malloc(_len+1); + if(_data == NULL){ + _len = 0; + _status = WS_MSG_ERROR; + } else { + _status = WS_MSG_SENDING; + memcpy(_data, data, _len); + _data[_len] = 0; + } +} +AsyncWebSocketBasicMessage::AsyncWebSocketBasicMessage(uint8_t opcode, bool mask) + :_len(0) + ,_sent(0) + ,_ack(0) + ,_acked(0) + ,_data(NULL) +{ + _opcode = opcode & 0x07; + _mask = mask; + +} + + +AsyncWebSocketBasicMessage::~AsyncWebSocketBasicMessage() { + if(_data != NULL) + free(_data); +} + + void AsyncWebSocketBasicMessage::ack(size_t len, uint32_t time) { + (void)time; + _acked += len; + if(_sent == _len && _acked == _ack){ + _status = WS_MSG_SENT; + } +} + size_t AsyncWebSocketBasicMessage::send(AsyncClient *client) { + if(_status != WS_MSG_SENDING) + return 0; + if(_acked < _ack){ + return 0; + } + if(_sent == _len){ + if(_acked == _ack) + _status = WS_MSG_SENT; + return 0; + } + if(_sent > _len){ + _status = WS_MSG_ERROR; + return 0; + } + + size_t toSend = _len - _sent; + size_t window = webSocketSendFrameWindow(client); + + if(window < toSend) { + toSend = window; + } + + _sent += toSend; + _ack += toSend + ((toSend < 126)?2:4) + (_mask * 4); + + bool final = (_sent == _len); + uint8_t* dPtr = (uint8_t*)(_data + (_sent - toSend)); + uint8_t opCode = (toSend && _sent == toSend)?_opcode:(uint8_t)WS_CONTINUATION; + + size_t sent = webSocketSendFrame(client, final, opCode, _mask, dPtr, toSend); + _status = WS_MSG_SENDING; + if(toSend && sent != toSend){ + _sent -= (toSend - sent); + _ack -= (toSend - sent); + } + return sent; +} + +// bool AsyncWebSocketBasicMessage::reserve(size_t size) { +// if (size) { +// _data = (uint8_t*)malloc(size +1); +// if (_data) { +// memset(_data, 0, size); +// _len = size; +// _status = WS_MSG_SENDING; +// return true; +// } +// } +// return false; +// } + + +/* + * AsyncWebSocketMultiMessage Message + */ + + +AsyncWebSocketMultiMessage::AsyncWebSocketMultiMessage(AsyncWebSocketMessageBuffer * buffer, uint8_t opcode, bool mask) + :_len(0) + ,_sent(0) + ,_ack(0) + ,_acked(0) + ,_WSbuffer(nullptr) +{ + + _opcode = opcode & 0x07; + _mask = mask; + + if (buffer) { + _WSbuffer = buffer; + (*_WSbuffer)++; + _data = buffer->get(); + _len = buffer->length(); + _status = WS_MSG_SENDING; + //ets_printf("M: %u\n", _len); + } else { + _status = WS_MSG_ERROR; + } + +} + + +AsyncWebSocketMultiMessage::~AsyncWebSocketMultiMessage() { + if (_WSbuffer) { + (*_WSbuffer)--; // decreases the counter. + } +} + + void AsyncWebSocketMultiMessage::ack(size_t len, uint32_t time) { + (void)time; + _acked += len; + if(_sent >= _len && _acked >= _ack){ + _status = WS_MSG_SENT; + } + //ets_printf("A: %u\n", len); +} + size_t AsyncWebSocketMultiMessage::send(AsyncClient *client) { + if(_status != WS_MSG_SENDING) + return 0; + if(_acked < _ack){ + return 0; + } + if(_sent == _len){ + _status = WS_MSG_SENT; + return 0; + } + if(_sent > _len){ + _status = WS_MSG_ERROR; + //ets_printf("E: %u > %u\n", _sent, _len); + return 0; + } + + size_t toSend = _len - _sent; + size_t window = webSocketSendFrameWindow(client); + + if(window < toSend) { + toSend = window; + } + + _sent += toSend; + _ack += toSend + ((toSend < 126)?2:4) + (_mask * 4); + + //ets_printf("W: %u %u\n", _sent - toSend, toSend); + + bool final = (_sent == _len); + uint8_t* dPtr = (uint8_t*)(_data + (_sent - toSend)); + uint8_t opCode = (toSend && _sent == toSend)?_opcode:(uint8_t)WS_CONTINUATION; + + size_t sent = webSocketSendFrame(client, final, opCode, _mask, dPtr, toSend); + _status = WS_MSG_SENDING; + if(toSend && sent != toSend){ + //ets_printf("E: %u != %u\n", toSend, sent); + _sent -= (toSend - sent); + _ack -= (toSend - sent); + } + //ets_printf("S: %u %u\n", _sent, sent); + return sent; +} + + +/* + * Async WebSocket Client + */ + const char * AWSC_PING_PAYLOAD = "ESPAsyncWebServer-PING"; + const size_t AWSC_PING_PAYLOAD_LEN = 22; + +AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server) + : _controlQueue(LinkedList([](AsyncWebSocketControl *c){ delete c; })) + , _messageQueue(LinkedList([](AsyncWebSocketMessage *m){ delete m; })) + , _tempObject(NULL) +{ + _client = request->client(); + _server = server; + _clientId = _server->_getNextId(); + _status = WS_CONNECTED; + _pstate = 0; + _lastMessageTime = millis(); + _keepAlivePeriod = 0; + _client->setRxTimeout(0); + _client->onError([](void *r, AsyncClient* c, int8_t error){ (void)c; ((AsyncWebSocketClient*)(r))->_onError(error); }, this); + _client->onAck([](void *r, AsyncClient* c, size_t len, uint32_t time){ (void)c; ((AsyncWebSocketClient*)(r))->_onAck(len, time); }, this); + _client->onDisconnect([](void *r, AsyncClient* c){ ((AsyncWebSocketClient*)(r))->_onDisconnect(); delete c; }, this); + _client->onTimeout([](void *r, AsyncClient* c, uint32_t time){ (void)c; ((AsyncWebSocketClient*)(r))->_onTimeout(time); }, this); + _client->onData([](void *r, AsyncClient* c, void *buf, size_t len){ (void)c; ((AsyncWebSocketClient*)(r))->_onData(buf, len); }, this); + _client->onPoll([](void *r, AsyncClient* c){ (void)c; ((AsyncWebSocketClient*)(r))->_onPoll(); }, this); + _server->_addClient(this); + _server->_handleEvent(this, WS_EVT_CONNECT, request, NULL, 0); + delete request; +} + +AsyncWebSocketClient::~AsyncWebSocketClient(){ + _messageQueue.free(); + _controlQueue.free(); + _server->_handleEvent(this, WS_EVT_DISCONNECT, NULL, NULL, 0); +} + +void AsyncWebSocketClient::_onAck(size_t len, uint32_t time){ + _lastMessageTime = millis(); + if(!_controlQueue.isEmpty()){ + auto head = _controlQueue.front(); + if(head->finished()){ + len -= head->len(); + if(_status == WS_DISCONNECTING && head->opcode() == WS_DISCONNECT){ + _controlQueue.remove(head); + _status = WS_DISCONNECTED; + _client->close(true); + return; + } + _controlQueue.remove(head); + } + } + if(len && !_messageQueue.isEmpty()){ + _messageQueue.front()->ack(len, time); + } + _server->_cleanBuffers(); + _runQueue(); +} + +void AsyncWebSocketClient::_onPoll(){ + if(_client->canSend() && (!_controlQueue.isEmpty() || !_messageQueue.isEmpty())){ + _runQueue(); + } else if(_keepAlivePeriod > 0 && _controlQueue.isEmpty() && _messageQueue.isEmpty() && (millis() - _lastMessageTime) >= _keepAlivePeriod){ + ping((uint8_t *)AWSC_PING_PAYLOAD, AWSC_PING_PAYLOAD_LEN); + } +} + +void AsyncWebSocketClient::_runQueue(){ + while(!_messageQueue.isEmpty() && _messageQueue.front()->finished()){ + _messageQueue.remove(_messageQueue.front()); + } + + if(!_controlQueue.isEmpty() && (_messageQueue.isEmpty() || _messageQueue.front()->betweenFrames()) && webSocketSendFrameWindow(_client) > (size_t)(_controlQueue.front()->len() - 1)){ + _controlQueue.front()->send(_client); + } else if(!_messageQueue.isEmpty() && _messageQueue.front()->betweenFrames() && webSocketSendFrameWindow(_client)){ + _messageQueue.front()->send(_client); + } +} + +bool AsyncWebSocketClient::queueIsFull(){ + if((_messageQueue.length() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED) ) return true; + return false; +} + +void AsyncWebSocketClient::_queueMessage(AsyncWebSocketMessage *dataMessage){ + if(dataMessage == NULL) + return; + if(_status != WS_CONNECTED){ + delete dataMessage; + return; + } + if(_messageQueue.length() >= WS_MAX_QUEUED_MESSAGES){ + ets_printf("ERROR: Too many messages queued\n"); + delete dataMessage; + } else { + _messageQueue.add(dataMessage); + } + if(_client->canSend()) + _runQueue(); +} + +void AsyncWebSocketClient::_queueControl(AsyncWebSocketControl *controlMessage){ + if(controlMessage == NULL) + return; + _controlQueue.add(controlMessage); + if(_client->canSend()) + _runQueue(); +} + +void AsyncWebSocketClient::close(uint16_t code, const char * message){ + if(_status != WS_CONNECTED) + return; + if(code){ + uint8_t packetLen = 2; + if(message != NULL){ + size_t mlen = strlen(message); + if(mlen > 123) mlen = 123; + packetLen += mlen; + } + char * buf = (char*)malloc(packetLen); + if(buf != NULL){ + buf[0] = (uint8_t)(code >> 8); + buf[1] = (uint8_t)(code & 0xFF); + if(message != NULL){ + memcpy(buf+2, message, packetLen -2); + } + _queueControl(new AsyncWebSocketControl(WS_DISCONNECT,(uint8_t*)buf,packetLen)); + free(buf); + return; + } + } + _queueControl(new AsyncWebSocketControl(WS_DISCONNECT)); +} + +void AsyncWebSocketClient::ping(uint8_t *data, size_t len){ + if(_status == WS_CONNECTED) + _queueControl(new AsyncWebSocketControl(WS_PING, data, len)); +} + +void AsyncWebSocketClient::_onError(int8_t){} + +void AsyncWebSocketClient::_onTimeout(uint32_t time){ + (void)time; + _client->close(true); +} + +void AsyncWebSocketClient::_onDisconnect(){ + _client = NULL; + _server->_handleDisconnect(this); +} + +void AsyncWebSocketClient::_onData(void *pbuf, size_t plen){ + _lastMessageTime = millis(); + uint8_t *data = (uint8_t*)pbuf; + while(plen > 0){ + if(!_pstate){ + const uint8_t *fdata = data; + _pinfo.index = 0; + _pinfo.final = (fdata[0] & 0x80) != 0; + _pinfo.opcode = fdata[0] & 0x0F; + _pinfo.masked = (fdata[1] & 0x80) != 0; + _pinfo.len = fdata[1] & 0x7F; + data += 2; + plen -= 2; + if(_pinfo.len == 126){ + _pinfo.len = fdata[3] | (uint16_t)(fdata[2]) << 8; + data += 2; + plen -= 2; + } else if(_pinfo.len == 127){ + _pinfo.len = fdata[9] | (uint16_t)(fdata[8]) << 8 | (uint32_t)(fdata[7]) << 16 | (uint32_t)(fdata[6]) << 24 | (uint64_t)(fdata[5]) << 32 | (uint64_t)(fdata[4]) << 40 | (uint64_t)(fdata[3]) << 48 | (uint64_t)(fdata[2]) << 56; + data += 8; + plen -= 8; + } + + if(_pinfo.masked){ + memcpy(_pinfo.mask, data, 4); + data += 4; + plen -= 4; + } + } + + const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen); + const auto datalast = data[datalen]; + + if(_pinfo.masked){ + for(size_t i=0;i_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, (uint8_t*)data, datalen); + + _pinfo.index += datalen; + } else if((datalen + _pinfo.index) == _pinfo.len){ + _pstate = 0; + if(_pinfo.opcode == WS_DISCONNECT){ + if(datalen){ + uint16_t reasonCode = (uint16_t)(data[0] << 8) + data[1]; + char * reasonString = (char*)(data+2); + if(reasonCode > 1001){ + _server->_handleEvent(this, WS_EVT_ERROR, (void *)&reasonCode, (uint8_t*)reasonString, strlen(reasonString)); + } + } + if(_status == WS_DISCONNECTING){ + _status = WS_DISCONNECTED; + _client->close(true); + } else { + _status = WS_DISCONNECTING; + _client->ackLater(); + _queueControl(new AsyncWebSocketControl(WS_DISCONNECT, data, datalen)); + } + } else if(_pinfo.opcode == WS_PING){ + _queueControl(new AsyncWebSocketControl(WS_PONG, data, datalen)); + } else if(_pinfo.opcode == WS_PONG){ + if(datalen != AWSC_PING_PAYLOAD_LEN || memcmp(AWSC_PING_PAYLOAD, data, AWSC_PING_PAYLOAD_LEN) != 0) + _server->_handleEvent(this, WS_EVT_PONG, NULL, data, datalen); + } else if(_pinfo.opcode < 8){//continuation or text/binary frame + _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, data, datalen); + } + } else { + //os_printf("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len); + //what should we do? + break; + } + + // restore byte as _handleEvent may have added a null terminator i.e., data[len] = 0; + if (datalen > 0) + data[datalen] = datalast; + + data += datalen; + plen -= datalen; + } +} + +size_t AsyncWebSocketClient::printf(const char *format, ...) { + va_list arg; + va_start(arg, format); + char* temp = new char[MAX_PRINTF_LEN]; + if(!temp){ + va_end(arg); + return 0; + } + char* buffer = temp; + size_t len = vsnprintf(temp, MAX_PRINTF_LEN, format, arg); + va_end(arg); + + if (len > (MAX_PRINTF_LEN - 1)) { + buffer = new char[len + 1]; + if (!buffer) { + delete[] temp; + return 0; + } + va_start(arg, format); + vsnprintf(buffer, len + 1, format, arg); + va_end(arg); + } + text(buffer, len); + if (buffer != temp) { + delete[] buffer; + } + delete[] temp; + return len; +} + +#ifndef ESP32 +size_t AsyncWebSocketClient::printf_P(PGM_P formatP, ...) { + va_list arg; + va_start(arg, formatP); + char* temp = new char[MAX_PRINTF_LEN]; + if(!temp){ + va_end(arg); + return 0; + } + char* buffer = temp; + size_t len = vsnprintf_P(temp, MAX_PRINTF_LEN, formatP, arg); + va_end(arg); + + if (len > (MAX_PRINTF_LEN - 1)) { + buffer = new char[len + 1]; + if (!buffer) { + delete[] temp; + return 0; + } + va_start(arg, formatP); + vsnprintf_P(buffer, len + 1, formatP, arg); + va_end(arg); + } + text(buffer, len); + if (buffer != temp) { + delete[] buffer; + } + delete[] temp; + return len; +} +#endif + +void AsyncWebSocketClient::text(const char * message, size_t len){ + _queueMessage(new AsyncWebSocketBasicMessage(message, len)); +} +void AsyncWebSocketClient::text(const char * message){ + text(message, strlen(message)); +} +void AsyncWebSocketClient::text(uint8_t * message, size_t len){ + text((const char *)message, len); +} +void AsyncWebSocketClient::text(char * message){ + text(message, strlen(message)); +} +void AsyncWebSocketClient::text(const String &message){ + text(message.c_str(), message.length()); +} +void AsyncWebSocketClient::text(const __FlashStringHelper *data){ + PGM_P p = reinterpret_cast(data); + size_t n = 0; + while (1) { + if (pgm_read_byte(p+n) == 0) break; + n += 1; + } + char * message = (char*) malloc(n+1); + if(message){ + for(size_t b=0; b(data); + char * message = (char*) malloc(len); + if(message){ + for(size_t b=0; bremoteIP(); +} + +uint16_t AsyncWebSocketClient::remotePort() { + if(!_client) { + return 0; + } + return _client->remotePort(); +} + + + +/* + * Async Web Socket - Each separate socket location + */ + +AsyncWebSocket::AsyncWebSocket(const String& url) + :_url(url) + ,_clients(LinkedList([](AsyncWebSocketClient *c){ delete c; })) + ,_cNextId(1) + ,_enabled(true) + ,_buffers(LinkedList([](AsyncWebSocketMessageBuffer *b){ delete b; })) +{ + _eventHandler = NULL; +} + +AsyncWebSocket::~AsyncWebSocket(){} + +void AsyncWebSocket::_handleEvent(AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ + if(_eventHandler != NULL){ + _eventHandler(this, client, type, arg, data, len); + } +} + +void AsyncWebSocket::_addClient(AsyncWebSocketClient * client){ + _clients.add(client); +} + +void AsyncWebSocket::_handleDisconnect(AsyncWebSocketClient * client){ + + _clients.remove_first([=](AsyncWebSocketClient * c){ + return c->id() == client->id(); + }); +} + +bool AsyncWebSocket::availableForWriteAll(){ + for(const auto& c: _clients){ + if(c->queueIsFull()) return false; + } + return true; +} + +bool AsyncWebSocket::availableForWrite(uint32_t id){ + for(const auto& c: _clients){ + if(c->queueIsFull() && (c->id() == id )) return false; + } + return true; +} + +size_t AsyncWebSocket::count() const { + return _clients.count_if([](AsyncWebSocketClient * c){ + return c->status() == WS_CONNECTED; + }); +} + +AsyncWebSocketClient * AsyncWebSocket::client(uint32_t id){ + for(const auto &c: _clients){ + if(c->id() == id && c->status() == WS_CONNECTED){ + return c; + } + } + return nullptr; +} + + +void AsyncWebSocket::close(uint32_t id, uint16_t code, const char * message){ + AsyncWebSocketClient * c = client(id); + if(c) + c->close(code, message); +} + +void AsyncWebSocket::closeAll(uint16_t code, const char * message){ + for(const auto& c: _clients){ + if(c->status() == WS_CONNECTED) + c->close(code, message); + } +} + +void AsyncWebSocket::cleanupClients(uint16_t maxClients) +{ + if (count() > maxClients){ + _clients.front()->close(); + } +} + +void AsyncWebSocket::ping(uint32_t id, uint8_t *data, size_t len){ + AsyncWebSocketClient * c = client(id); + if(c) + c->ping(data, len); +} + +void AsyncWebSocket::pingAll(uint8_t *data, size_t len){ + for(const auto& c: _clients){ + if(c->status() == WS_CONNECTED) + c->ping(data, len); + } +} + +void AsyncWebSocket::text(uint32_t id, const char * message, size_t len){ + AsyncWebSocketClient * c = client(id); + if(c) + c->text(message, len); +} + +void AsyncWebSocket::textAll(AsyncWebSocketMessageBuffer * buffer){ + if (!buffer) return; + buffer->lock(); + for(const auto& c: _clients){ + if(c->status() == WS_CONNECTED){ + c->text(buffer); + } + } + buffer->unlock(); + _cleanBuffers(); +} + + +void AsyncWebSocket::textAll(const char * message, size_t len){ + AsyncWebSocketMessageBuffer * WSBuffer = makeBuffer((uint8_t *)message, len); + textAll(WSBuffer); +} + +void AsyncWebSocket::binary(uint32_t id, const char * message, size_t len){ + AsyncWebSocketClient * c = client(id); + if(c) + c->binary(message, len); +} + +void AsyncWebSocket::binaryAll(const char * message, size_t len){ + AsyncWebSocketMessageBuffer * buffer = makeBuffer((uint8_t *)message, len); + binaryAll(buffer); +} + +void AsyncWebSocket::binaryAll(AsyncWebSocketMessageBuffer * buffer) +{ + if (!buffer) return; + buffer->lock(); + for(const auto& c: _clients){ + if(c->status() == WS_CONNECTED) + c->binary(buffer); + } + buffer->unlock(); + _cleanBuffers(); +} + +void AsyncWebSocket::message(uint32_t id, AsyncWebSocketMessage *message){ + AsyncWebSocketClient * c = client(id); + if(c) + c->message(message); +} + +void AsyncWebSocket::messageAll(AsyncWebSocketMultiMessage *message){ + for(const auto& c: _clients){ + if(c->status() == WS_CONNECTED) + c->message(message); + } + _cleanBuffers(); +} + +size_t AsyncWebSocket::printf(uint32_t id, const char *format, ...){ + AsyncWebSocketClient * c = client(id); + if(c){ + va_list arg; + va_start(arg, format); + size_t len = c->printf(format, arg); + va_end(arg); + return len; + } + return 0; +} + +size_t AsyncWebSocket::printfAll(const char *format, ...) { + va_list arg; + char* temp = new char[MAX_PRINTF_LEN]; + if(!temp){ + return 0; + } + va_start(arg, format); + size_t len = vsnprintf(temp, MAX_PRINTF_LEN, format, arg); + va_end(arg); + delete[] temp; + + AsyncWebSocketMessageBuffer * buffer = makeBuffer(len); + if (!buffer) { + return 0; + } + + va_start(arg, format); + vsnprintf( (char *)buffer->get(), len + 1, format, arg); + va_end(arg); + + textAll(buffer); + return len; +} + +#ifndef ESP32 +size_t AsyncWebSocket::printf_P(uint32_t id, PGM_P formatP, ...){ + AsyncWebSocketClient * c = client(id); + if(c != NULL){ + va_list arg; + va_start(arg, formatP); + size_t len = c->printf_P(formatP, arg); + va_end(arg); + return len; + } + return 0; +} +#endif + +size_t AsyncWebSocket::printfAll_P(PGM_P formatP, ...) { + va_list arg; + char* temp = new char[MAX_PRINTF_LEN]; + if(!temp){ + return 0; + } + va_start(arg, formatP); + size_t len = vsnprintf_P(temp, MAX_PRINTF_LEN, formatP, arg); + va_end(arg); + delete[] temp; + + AsyncWebSocketMessageBuffer * buffer = makeBuffer(len + 1); + if (!buffer) { + return 0; + } + + va_start(arg, formatP); + vsnprintf_P((char *)buffer->get(), len + 1, formatP, arg); + va_end(arg); + + textAll(buffer); + return len; +} + +void AsyncWebSocket::text(uint32_t id, const char * message){ + text(id, message, strlen(message)); +} +void AsyncWebSocket::text(uint32_t id, uint8_t * message, size_t len){ + text(id, (const char *)message, len); +} +void AsyncWebSocket::text(uint32_t id, char * message){ + text(id, message, strlen(message)); +} +void AsyncWebSocket::text(uint32_t id, const String &message){ + text(id, message.c_str(), message.length()); +} +void AsyncWebSocket::text(uint32_t id, const __FlashStringHelper *message){ + AsyncWebSocketClient * c = client(id); + if(c != NULL) + c->text(message); +} +void AsyncWebSocket::textAll(const char * message){ + textAll(message, strlen(message)); +} +void AsyncWebSocket::textAll(uint8_t * message, size_t len){ + textAll((const char *)message, len); +} +void AsyncWebSocket::textAll(char * message){ + textAll(message, strlen(message)); +} +void AsyncWebSocket::textAll(const String &message){ + textAll(message.c_str(), message.length()); +} +void AsyncWebSocket::textAll(const __FlashStringHelper *message){ + for(const auto& c: _clients){ + if(c->status() == WS_CONNECTED) + c->text(message); + } +} +void AsyncWebSocket::binary(uint32_t id, const char * message){ + binary(id, message, strlen(message)); +} +void AsyncWebSocket::binary(uint32_t id, uint8_t * message, size_t len){ + binary(id, (const char *)message, len); +} +void AsyncWebSocket::binary(uint32_t id, char * message){ + binary(id, message, strlen(message)); +} +void AsyncWebSocket::binary(uint32_t id, const String &message){ + binary(id, message.c_str(), message.length()); +} +void AsyncWebSocket::binary(uint32_t id, const __FlashStringHelper *message, size_t len){ + AsyncWebSocketClient * c = client(id); + if(c != NULL) + c-> binary(message, len); +} +void AsyncWebSocket::binaryAll(const char * message){ + binaryAll(message, strlen(message)); +} +void AsyncWebSocket::binaryAll(uint8_t * message, size_t len){ + binaryAll((const char *)message, len); +} +void AsyncWebSocket::binaryAll(char * message){ + binaryAll(message, strlen(message)); +} +void AsyncWebSocket::binaryAll(const String &message){ + binaryAll(message.c_str(), message.length()); +} +void AsyncWebSocket::binaryAll(const __FlashStringHelper *message, size_t len){ + for(const auto& c: _clients){ + if(c->status() == WS_CONNECTED) + c-> binary(message, len); + } + } + +const char * WS_STR_CONNECTION = "Connection"; +const char * WS_STR_UPGRADE = "Upgrade"; +const char * WS_STR_ORIGIN = "Origin"; +const char * WS_STR_VERSION = "Sec-WebSocket-Version"; +const char * WS_STR_KEY = "Sec-WebSocket-Key"; +const char * WS_STR_PROTOCOL = "Sec-WebSocket-Protocol"; +const char * WS_STR_ACCEPT = "Sec-WebSocket-Accept"; +const char * WS_STR_UUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +bool AsyncWebSocket::canHandle(AsyncWebServerRequest *request){ + if(!_enabled) + return false; + + if(request->method() != HTTP_GET || !request->url().equals(_url) || !request->isExpectedRequestedConnType(RCT_WS)) + return false; + + request->addInterestingHeader(WS_STR_CONNECTION); + request->addInterestingHeader(WS_STR_UPGRADE); + request->addInterestingHeader(WS_STR_ORIGIN); + request->addInterestingHeader(WS_STR_VERSION); + request->addInterestingHeader(WS_STR_KEY); + request->addInterestingHeader(WS_STR_PROTOCOL); + return true; +} + +void AsyncWebSocket::handleRequest(AsyncWebServerRequest *request){ + if(!request->hasHeader(WS_STR_VERSION) || !request->hasHeader(WS_STR_KEY)){ + request->send(400); + return; + } + if((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())){ + return request->requestAuthentication(); + } + AsyncWebHeader* version = request->getHeader(WS_STR_VERSION); + if(version->value().toInt() != 13){ + AsyncWebServerResponse *response = request->beginResponse(400); + response->addHeader(WS_STR_VERSION,"13"); + request->send(response); + return; + } + AsyncWebHeader* key = request->getHeader(WS_STR_KEY); + AsyncWebServerResponse *response = new AsyncWebSocketResponse(key->value(), this); + if(request->hasHeader(WS_STR_PROTOCOL)){ + AsyncWebHeader* protocol = request->getHeader(WS_STR_PROTOCOL); + //ToDo: check protocol + response->addHeader(WS_STR_PROTOCOL, protocol->value()); + } + request->send(response); +} + +AsyncWebSocketMessageBuffer * AsyncWebSocket::makeBuffer(size_t size) +{ + AsyncWebSocketMessageBuffer * buffer = new AsyncWebSocketMessageBuffer(size); + if (buffer) { + AsyncWebLockGuard l(_lock); + _buffers.add(buffer); + } + return buffer; +} + +AsyncWebSocketMessageBuffer * AsyncWebSocket::makeBuffer(uint8_t * data, size_t size) +{ + AsyncWebSocketMessageBuffer * buffer = new AsyncWebSocketMessageBuffer(data, size); + + if (buffer) { + AsyncWebLockGuard l(_lock); + _buffers.add(buffer); + } + + return buffer; +} + +void AsyncWebSocket::_cleanBuffers() +{ + AsyncWebLockGuard l(_lock); + + for(AsyncWebSocketMessageBuffer * c: _buffers){ + if(c && c->canDelete()){ + _buffers.remove(c); + } + } +} + +AsyncWebSocket::AsyncWebSocketClientLinkedList AsyncWebSocket::getClients() const { + return _clients; +} + +/* + * Response to Web Socket request - sends the authorization and detaches the TCP Client from the web server + * Authentication code from https://github.com/Links2004/arduinoWebSockets/blob/master/src/WebSockets.cpp#L480 + */ + +AsyncWebSocketResponse::AsyncWebSocketResponse(const String& key, AsyncWebSocket *server){ + _server = server; + _code = 101; + _sendContentLength = false; + + uint8_t * hash = (uint8_t*)malloc(20); + if(hash == NULL){ + _state = RESPONSE_FAILED; + return; + } + char * buffer = (char *) malloc(33); + if(buffer == NULL){ + free(hash); + _state = RESPONSE_FAILED; + return; + } +#ifdef ESP8266 + sha1(key + WS_STR_UUID, hash); +#else + (String&)key += WS_STR_UUID; + mbedtls_sha1_context ctx; + mbedtls_sha1_init(&ctx); + mbedtls_sha1_starts_ret(&ctx); + mbedtls_sha1_update_ret(&ctx, (const unsigned char*)key.c_str(), key.length()); + mbedtls_sha1_finish_ret(&ctx, hash); + mbedtls_sha1_free(&ctx); +#endif + base64_encodestate _state; + base64_init_encodestate(&_state); + int len = base64_encode_block((const char *) hash, 20, buffer, &_state); + len = base64_encode_blockend((buffer + len), &_state); + addHeader(WS_STR_CONNECTION, WS_STR_UPGRADE); + addHeader(WS_STR_UPGRADE, "websocket"); + addHeader(WS_STR_ACCEPT,buffer); + free(buffer); + free(hash); +} + +void AsyncWebSocketResponse::_respond(AsyncWebServerRequest *request){ + if(_state == RESPONSE_FAILED){ + request->client()->close(true); + return; + } + String out = _assembleHead(request->version()); + request->client()->write(out.c_str(), _headLength); + _state = RESPONSE_WAIT_ACK; +} + +size_t AsyncWebSocketResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ + (void)time; + if(len){ + new AsyncWebSocketClient(request, _server); + } + return 0; +} diff --git a/yoRadio/src/AsyncWebServer/AsyncWebSocket.h b/yoRadio/src/AsyncWebServer/AsyncWebSocket.h new file mode 100644 index 0000000..d40641a --- /dev/null +++ b/yoRadio/src/AsyncWebServer/AsyncWebSocket.h @@ -0,0 +1,350 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef ASYNCWEBSOCKET_H_ +#define ASYNCWEBSOCKET_H_ + +#include +#ifdef ESP32 +#include "AsyncTCP.h" +#define WS_MAX_QUEUED_MESSAGES 32 +#else +#include +#define WS_MAX_QUEUED_MESSAGES 8 +#endif +#include "ESPAsyncWebServer.h" + +#include "AsyncWebSynchronization.h" + +#ifdef ESP8266 +#include +#ifdef CRYPTO_HASH_h // include Hash.h from espressif framework if the first include was from the crypto library +#include <../src/Hash.h> +#endif +#endif + +#ifdef ESP32 +#define DEFAULT_MAX_WS_CLIENTS 8 +#else +#define DEFAULT_MAX_WS_CLIENTS 4 +#endif + +class AsyncWebSocket; +class AsyncWebSocketResponse; +class AsyncWebSocketClient; +class AsyncWebSocketControl; + +typedef struct { + /** Message type as defined by enum AwsFrameType. + * Note: Applications will only see WS_TEXT and WS_BINARY. + * All other types are handled by the library. */ + uint8_t message_opcode; + /** Frame number of a fragmented message. */ + uint32_t num; + /** Is this the last frame in a fragmented message ?*/ + uint8_t final; + /** Is this frame masked? */ + uint8_t masked; + /** Message type as defined by enum AwsFrameType. + * This value is the same as message_opcode for non-fragmented + * messages, but may also be WS_CONTINUATION in a fragmented message. */ + uint8_t opcode; + /** Length of the current frame. + * This equals the total length of the message if num == 0 && final == true */ + uint64_t len; + /** Mask key */ + uint8_t mask[4]; + /** Offset of the data inside the current frame. */ + uint64_t index; +} AwsFrameInfo; + +typedef enum { WS_DISCONNECTED, WS_CONNECTED, WS_DISCONNECTING } AwsClientStatus; +typedef enum { WS_CONTINUATION, WS_TEXT, WS_BINARY, WS_DISCONNECT = 0x08, WS_PING, WS_PONG } AwsFrameType; +typedef enum { WS_MSG_SENDING, WS_MSG_SENT, WS_MSG_ERROR } AwsMessageStatus; +typedef enum { WS_EVT_CONNECT, WS_EVT_DISCONNECT, WS_EVT_PONG, WS_EVT_ERROR, WS_EVT_DATA } AwsEventType; + +class AsyncWebSocketMessageBuffer { + private: + uint8_t * _data; + size_t _len; + bool _lock; + uint32_t _count; + + public: + AsyncWebSocketMessageBuffer(); + AsyncWebSocketMessageBuffer(size_t size); + AsyncWebSocketMessageBuffer(uint8_t * data, size_t size); + AsyncWebSocketMessageBuffer(const AsyncWebSocketMessageBuffer &); + AsyncWebSocketMessageBuffer(AsyncWebSocketMessageBuffer &&); + ~AsyncWebSocketMessageBuffer(); + void operator ++(int i) { (void)i; _count++; } + void operator --(int i) { (void)i; if (_count > 0) { _count--; } ; } + bool reserve(size_t size); + void lock() { _lock = true; } + void unlock() { _lock = false; } + uint8_t * get() { return _data; } + size_t length() { return _len; } + uint32_t count() { return _count; } + bool canDelete() { return (!_count && !_lock); } + + friend AsyncWebSocket; + +}; + +class AsyncWebSocketMessage { + protected: + uint8_t _opcode; + bool _mask; + AwsMessageStatus _status; + public: + AsyncWebSocketMessage():_opcode(WS_TEXT),_mask(false),_status(WS_MSG_ERROR){} + virtual ~AsyncWebSocketMessage(){} + virtual void ack(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))){} + virtual size_t send(AsyncClient *client __attribute__((unused))){ return 0; } + virtual bool finished(){ return _status != WS_MSG_SENDING; } + virtual bool betweenFrames() const { return false; } +}; + +class AsyncWebSocketBasicMessage: public AsyncWebSocketMessage { + private: + size_t _len; + size_t _sent; + size_t _ack; + size_t _acked; + uint8_t * _data; +public: + AsyncWebSocketBasicMessage(const char * data, size_t len, uint8_t opcode=WS_TEXT, bool mask=false); + AsyncWebSocketBasicMessage(uint8_t opcode=WS_TEXT, bool mask=false); + virtual ~AsyncWebSocketBasicMessage() override; + virtual bool betweenFrames() const override { return _acked == _ack; } + virtual void ack(size_t len, uint32_t time) override ; + virtual size_t send(AsyncClient *client) override ; +}; + +class AsyncWebSocketMultiMessage: public AsyncWebSocketMessage { + private: + uint8_t * _data; + size_t _len; + size_t _sent; + size_t _ack; + size_t _acked; + AsyncWebSocketMessageBuffer * _WSbuffer; +public: + AsyncWebSocketMultiMessage(AsyncWebSocketMessageBuffer * buffer, uint8_t opcode=WS_TEXT, bool mask=false); + virtual ~AsyncWebSocketMultiMessage() override; + virtual bool betweenFrames() const override { return _acked == _ack; } + virtual void ack(size_t len, uint32_t time) override ; + virtual size_t send(AsyncClient *client) override ; +}; + +class AsyncWebSocketClient { + private: + AsyncClient *_client; + AsyncWebSocket *_server; + uint32_t _clientId; + AwsClientStatus _status; + + LinkedList _controlQueue; + LinkedList _messageQueue; + + uint8_t _pstate; + AwsFrameInfo _pinfo; + + uint32_t _lastMessageTime; + uint32_t _keepAlivePeriod; + + void _queueMessage(AsyncWebSocketMessage *dataMessage); + void _queueControl(AsyncWebSocketControl *controlMessage); + void _runQueue(); + + public: + void *_tempObject; + + AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server); + ~AsyncWebSocketClient(); + + //client id increments for the given server + uint32_t id(){ return _clientId; } + AwsClientStatus status(){ return _status; } + AsyncClient* client(){ return _client; } + AsyncWebSocket *server(){ return _server; } + AwsFrameInfo const &pinfo() const { return _pinfo; } + + IPAddress remoteIP(); + uint16_t remotePort(); + + //control frames + void close(uint16_t code=0, const char * message=NULL); + void ping(uint8_t *data=NULL, size_t len=0); + + //set auto-ping period in seconds. disabled if zero (default) + void keepAlivePeriod(uint16_t seconds){ + _keepAlivePeriod = seconds * 1000; + } + uint16_t keepAlivePeriod(){ + return (uint16_t)(_keepAlivePeriod / 1000); + } + + //data packets + void message(AsyncWebSocketMessage *message){ _queueMessage(message); } + bool queueIsFull(); + + size_t printf(const char *format, ...) __attribute__ ((format (printf, 2, 3))); +#ifndef ESP32 + size_t printf_P(PGM_P formatP, ...) __attribute__ ((format (printf, 2, 3))); +#endif + void text(const char * message, size_t len); + void text(const char * message); + void text(uint8_t * message, size_t len); + void text(char * message); + void text(const String &message); + void text(const __FlashStringHelper *data); + void text(AsyncWebSocketMessageBuffer *buffer); + + void binary(const char * message, size_t len); + void binary(const char * message); + void binary(uint8_t * message, size_t len); + void binary(char * message); + void binary(const String &message); + void binary(const __FlashStringHelper *data, size_t len); + void binary(AsyncWebSocketMessageBuffer *buffer); + + bool canSend() { return _messageQueue.length() < WS_MAX_QUEUED_MESSAGES; } + + //system callbacks (do not call) + void _onAck(size_t len, uint32_t time); + void _onError(int8_t); + void _onPoll(); + void _onTimeout(uint32_t time); + void _onDisconnect(); + void _onData(void *pbuf, size_t plen); +}; + +typedef std::function AwsEventHandler; + +//WebServer Handler implementation that plays the role of a socket server +class AsyncWebSocket: public AsyncWebHandler { + public: + typedef LinkedList AsyncWebSocketClientLinkedList; + private: + String _url; + AsyncWebSocketClientLinkedList _clients; + uint32_t _cNextId; + AwsEventHandler _eventHandler; + bool _enabled; + AsyncWebLock _lock; + + public: + AsyncWebSocket(const String& url); + ~AsyncWebSocket(); + const char * url() const { return _url.c_str(); } + void enable(bool e){ _enabled = e; } + bool enabled() const { return _enabled; } + bool availableForWriteAll(); + bool availableForWrite(uint32_t id); + + size_t count() const; + AsyncWebSocketClient * client(uint32_t id); + bool hasClient(uint32_t id){ return client(id) != NULL; } + + void close(uint32_t id, uint16_t code=0, const char * message=NULL); + void closeAll(uint16_t code=0, const char * message=NULL); + void cleanupClients(uint16_t maxClients = DEFAULT_MAX_WS_CLIENTS); + + void ping(uint32_t id, uint8_t *data=NULL, size_t len=0); + void pingAll(uint8_t *data=NULL, size_t len=0); // done + + void text(uint32_t id, const char * message, size_t len); + void text(uint32_t id, const char * message); + void text(uint32_t id, uint8_t * message, size_t len); + void text(uint32_t id, char * message); + void text(uint32_t id, const String &message); + void text(uint32_t id, const __FlashStringHelper *message); + + void textAll(const char * message, size_t len); + void textAll(const char * message); + void textAll(uint8_t * message, size_t len); + void textAll(char * message); + void textAll(const String &message); + void textAll(const __FlashStringHelper *message); // need to convert + void textAll(AsyncWebSocketMessageBuffer * buffer); + + void binary(uint32_t id, const char * message, size_t len); + void binary(uint32_t id, const char * message); + void binary(uint32_t id, uint8_t * message, size_t len); + void binary(uint32_t id, char * message); + void binary(uint32_t id, const String &message); + void binary(uint32_t id, const __FlashStringHelper *message, size_t len); + + void binaryAll(const char * message, size_t len); + void binaryAll(const char * message); + void binaryAll(uint8_t * message, size_t len); + void binaryAll(char * message); + void binaryAll(const String &message); + void binaryAll(const __FlashStringHelper *message, size_t len); + void binaryAll(AsyncWebSocketMessageBuffer * buffer); + + void message(uint32_t id, AsyncWebSocketMessage *message); + void messageAll(AsyncWebSocketMultiMessage *message); + + size_t printf(uint32_t id, const char *format, ...) __attribute__ ((format (printf, 3, 4))); + size_t printfAll(const char *format, ...) __attribute__ ((format (printf, 2, 3))); +#ifndef ESP32 + size_t printf_P(uint32_t id, PGM_P formatP, ...) __attribute__ ((format (printf, 3, 4))); +#endif + size_t printfAll_P(PGM_P formatP, ...) __attribute__ ((format (printf, 2, 3))); + + //event listener + void onEvent(AwsEventHandler handler){ + _eventHandler = handler; + } + + //system callbacks (do not call) + uint32_t _getNextId(){ return _cNextId++; } + void _addClient(AsyncWebSocketClient * client); + void _handleDisconnect(AsyncWebSocketClient * client); + void _handleEvent(AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len); + virtual bool canHandle(AsyncWebServerRequest *request) override final; + virtual void handleRequest(AsyncWebServerRequest *request) override final; + + + // messagebuffer functions/objects. + AsyncWebSocketMessageBuffer * makeBuffer(size_t size = 0); + AsyncWebSocketMessageBuffer * makeBuffer(uint8_t * data, size_t size); + LinkedList _buffers; + void _cleanBuffers(); + + AsyncWebSocketClientLinkedList getClients() const; +}; + +//WebServer response to authenticate the socket and detach the tcp client from the web server request +class AsyncWebSocketResponse: public AsyncWebServerResponse { + private: + String _content; + AsyncWebSocket *_server; + public: + AsyncWebSocketResponse(const String& key, AsyncWebSocket *server); + void _respond(AsyncWebServerRequest *request); + size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); + bool _sourceValid() const { return true; } +}; + + +#endif /* ASYNCWEBSOCKET_H_ */ diff --git a/yoRadio/src/AsyncWebServer/AsyncWebSynchronization.h b/yoRadio/src/AsyncWebServer/AsyncWebSynchronization.h new file mode 100644 index 0000000..238847c --- /dev/null +++ b/yoRadio/src/AsyncWebServer/AsyncWebSynchronization.h @@ -0,0 +1,87 @@ +#ifndef ASYNCWEBSYNCHRONIZATION_H_ +#define ASYNCWEBSYNCHRONIZATION_H_ + +// Synchronisation is only available on ESP32, as the ESP8266 isn't using FreeRTOS by default + +#include "ESPAsyncWebServer.h" + +#ifdef ESP32 + +// This is the ESP32 version of the Sync Lock, using the FreeRTOS Semaphore +class AsyncWebLock +{ +private: + SemaphoreHandle_t _lock; + mutable void *_lockedBy; + +public: + AsyncWebLock() { + _lock = xSemaphoreCreateBinary(); + _lockedBy = NULL; + xSemaphoreGive(_lock); + } + + ~AsyncWebLock() { + vSemaphoreDelete(_lock); + } + + bool lock() const { + extern void *pxCurrentTCB; + if (_lockedBy != pxCurrentTCB) { + xSemaphoreTake(_lock, portMAX_DELAY); + _lockedBy = pxCurrentTCB; + return true; + } + return false; + } + + void unlock() const { + _lockedBy = NULL; + xSemaphoreGive(_lock); + } +}; + +#else + +// This is the 8266 version of the Sync Lock which is currently unimplemented +class AsyncWebLock +{ + +public: + AsyncWebLock() { + } + + ~AsyncWebLock() { + } + + bool lock() const { + return false; + } + + void unlock() const { + } +}; +#endif + +class AsyncWebLockGuard +{ +private: + const AsyncWebLock *_lock; + +public: + AsyncWebLockGuard(const AsyncWebLock &l) { + if (l.lock()) { + _lock = &l; + } else { + _lock = NULL; + } + } + + ~AsyncWebLockGuard() { + if (_lock) { + _lock->unlock(); + } + } +}; + +#endif // ASYNCWEBSYNCHRONIZATION_H_ diff --git a/yoRadio/src/AsyncWebServer/ESPAsyncWebServer.h b/yoRadio/src/AsyncWebServer/ESPAsyncWebServer.h new file mode 100644 index 0000000..448d6eb --- /dev/null +++ b/yoRadio/src/AsyncWebServer/ESPAsyncWebServer.h @@ -0,0 +1,471 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef _ESPAsyncWebServer_H_ +#define _ESPAsyncWebServer_H_ + +#include "Arduino.h" + +#include +#include "FS.h" + +#include "StringArray.h" + +#ifdef ESP32 +#include +#include "AsyncTCP.h" +#elif defined(ESP8266) +#include +#include +#else +#error Platform not supported +#endif + +#ifdef ASYNCWEBSERVER_REGEX +#define ASYNCWEBSERVER_REGEX_ATTRIBUTE +#else +#define ASYNCWEBSERVER_REGEX_ATTRIBUTE __attribute__((warning("ASYNCWEBSERVER_REGEX not defined"))) +#endif + +#define DEBUGF(...) //Serial.printf(__VA_ARGS__) + +class AsyncWebServer; +class AsyncWebServerRequest; +class AsyncWebServerResponse; +class AsyncWebHeader; +class AsyncWebParameter; +class AsyncWebRewrite; +class AsyncWebHandler; +class AsyncStaticWebHandler; +class AsyncCallbackWebHandler; +class AsyncResponseStream; + +#ifndef WEBSERVER_H +typedef enum { + HTTP_GET = 0b00000001, + HTTP_POST = 0b00000010, + HTTP_DELETE = 0b00000100, + HTTP_PUT = 0b00001000, + HTTP_PATCH = 0b00010000, + HTTP_HEAD = 0b00100000, + HTTP_OPTIONS = 0b01000000, + HTTP_ANY = 0b01111111, +} WebRequestMethod; +#endif + +//if this value is returned when asked for data, packet will not be sent and you will be asked for data again +#define RESPONSE_TRY_AGAIN 0xFFFFFFFF + +typedef uint8_t WebRequestMethodComposite; +typedef std::function ArDisconnectHandler; + +/* + * PARAMETER :: Chainable object to hold GET/POST and FILE parameters + * */ + +class AsyncWebParameter { + private: + String _name; + String _value; + size_t _size; + bool _isForm; + bool _isFile; + + public: + + AsyncWebParameter(const String& name, const String& value, bool form=false, bool file=false, size_t size=0): _name(name), _value(value), _size(size), _isForm(form), _isFile(file){} + const String& name() const { return _name; } + const String& value() const { return _value; } + size_t size() const { return _size; } + bool isPost() const { return _isForm; } + bool isFile() const { return _isFile; } +}; + +/* + * HEADER :: Chainable object to hold the headers + * */ + +class AsyncWebHeader { + private: + String _name; + String _value; + + public: + AsyncWebHeader(const String& name, const String& value): _name(name), _value(value){} + AsyncWebHeader(const String& data): _name(), _value(){ + if(!data) return; + int index = data.indexOf(':'); + if (index < 0) return; + _name = data.substring(0, index); + _value = data.substring(index + 2); + } + ~AsyncWebHeader(){} + const String& name() const { return _name; } + const String& value() const { return _value; } + String toString() const { return String(_name+": "+_value+"\r\n"); } +}; + +/* + * REQUEST :: Each incoming Client is wrapped inside a Request and both live together until disconnect + * */ + +typedef enum { RCT_NOT_USED = -1, RCT_DEFAULT = 0, RCT_HTTP, RCT_WS, RCT_EVENT, RCT_MAX } RequestedConnectionType; + +typedef std::function AwsResponseFiller; +typedef std::function AwsTemplateProcessor; + +class AsyncWebServerRequest { + using File = fs::File; + using FS = fs::FS; + friend class AsyncWebServer; + friend class AsyncCallbackWebHandler; + private: + AsyncClient* _client; + AsyncWebServer* _server; + AsyncWebHandler* _handler; + AsyncWebServerResponse* _response; + StringArray _interestingHeaders; + ArDisconnectHandler _onDisconnectfn; + + String _temp; + uint8_t _parseState; + + uint8_t _version; + WebRequestMethodComposite _method; + String _url; + String _host; + String _contentType; + String _boundary; + String _authorization; + RequestedConnectionType _reqconntype; + void _removeNotInterestingHeaders(); + bool _isDigest; + bool _isMultipart; + bool _isPlainPost; + bool _expectingContinue; + size_t _contentLength; + size_t _parsedLength; + + LinkedList _headers; + LinkedList _params; + LinkedList _pathParams; + + uint8_t _multiParseState; + uint8_t _boundaryPosition; + size_t _itemStartIndex; + size_t _itemSize; + String _itemName; + String _itemFilename; + String _itemType; + String _itemValue; + uint8_t *_itemBuffer; + size_t _itemBufferIndex; + bool _itemIsFile; + + void _onPoll(); + void _onAck(size_t len, uint32_t time); + void _onError(int8_t error); + void _onTimeout(uint32_t time); + void _onDisconnect(); + void _onData(void *buf, size_t len); + + void _addParam(AsyncWebParameter*); + void _addPathParam(const char *param); + + bool _parseReqHead(); + bool _parseReqHeader(); + void _parseLine(); + void _parsePlainPostChar(uint8_t data); + void _parseMultipartPostByte(uint8_t data, bool last); + void _addGetParams(const String& params); + + void _handleUploadStart(); + void _handleUploadByte(uint8_t data, bool last); + void _handleUploadEnd(); + + public: + File _tempFile; + void *_tempObject; + + AsyncWebServerRequest(AsyncWebServer*, AsyncClient*); + ~AsyncWebServerRequest(); + + AsyncClient* client(){ return _client; } + uint8_t version() const { return _version; } + WebRequestMethodComposite method() const { return _method; } + const String& url() const { return _url; } + const String& host() const { return _host; } + const String& contentType() const { return _contentType; } + size_t contentLength() const { return _contentLength; } + bool multipart() const { return _isMultipart; } + const char * methodToString() const; + const char * requestedConnTypeToString() const; + RequestedConnectionType requestedConnType() const { return _reqconntype; } + bool isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2 = RCT_NOT_USED, RequestedConnectionType erct3 = RCT_NOT_USED); + void onDisconnect (ArDisconnectHandler fn); + + //hash is the string representation of: + // base64(user:pass) for basic or + // user:realm:md5(user:realm:pass) for digest + bool authenticate(const char * hash); + bool authenticate(const char * username, const char * password, const char * realm = NULL, bool passwordIsHash = false); + void requestAuthentication(const char * realm = NULL, bool isDigest = true); + + void setHandler(AsyncWebHandler *handler){ _handler = handler; } + void addInterestingHeader(const String& name); + + void redirect(const String& url); + + void send(AsyncWebServerResponse *response); + void send(int code, const String& contentType=String(), const String& content=String()); + void send(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + void send(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + void send(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); + void send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + void sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + void send_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); + void send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback=nullptr); + + AsyncWebServerResponse *beginResponse(int code, const String& contentType=String(), const String& content=String()); + AsyncWebServerResponse *beginResponse(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + AsyncWebServerResponse *beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + AsyncResponseStream *beginResponseStream(const String& contentType, size_t bufferSize=1460); + AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); + AsyncWebServerResponse *beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback=nullptr); + + size_t headers() const; // get header count + bool hasHeader(const String& name) const; // check if header exists + bool hasHeader(const __FlashStringHelper * data) const; // check if header exists + + AsyncWebHeader* getHeader(const String& name) const; + AsyncWebHeader* getHeader(const __FlashStringHelper * data) const; + AsyncWebHeader* getHeader(size_t num) const; + + size_t params() const; // get arguments count + bool hasParam(const String& name, bool post=false, bool file=false) const; + bool hasParam(const __FlashStringHelper * data, bool post=false, bool file=false) const; + + AsyncWebParameter* getParam(const String& name, bool post=false, bool file=false) const; + AsyncWebParameter* getParam(const __FlashStringHelper * data, bool post, bool file) const; + AsyncWebParameter* getParam(size_t num) const; + + size_t args() const { return params(); } // get arguments count + const String& arg(const String& name) const; // get request argument value by name + const String& arg(const __FlashStringHelper * data) const; // get request argument value by F(name) + const String& arg(size_t i) const; // get request argument value by number + const String& argName(size_t i) const; // get request argument name by number + bool hasArg(const char* name) const; // check if argument exists + bool hasArg(const __FlashStringHelper * data) const; // check if F(argument) exists + + const String& ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const; + + const String& header(const char* name) const;// get request header value by name + const String& header(const __FlashStringHelper * data) const;// get request header value by F(name) + const String& header(size_t i) const; // get request header value by number + const String& headerName(size_t i) const; // get request header name by number + String urlDecode(const String& text) const; +}; + +/* + * FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server) + * */ + +typedef std::function ArRequestFilterFunction; + +bool ON_STA_FILTER(AsyncWebServerRequest *request); + +bool ON_AP_FILTER(AsyncWebServerRequest *request); + +/* + * REWRITE :: One instance can be handle any Request (done by the Server) + * */ + +class AsyncWebRewrite { + protected: + String _from; + String _toUrl; + String _params; + ArRequestFilterFunction _filter; + public: + AsyncWebRewrite(const char* from, const char* to): _from(from), _toUrl(to), _params(String()), _filter(NULL){ + int index = _toUrl.indexOf('?'); + if (index > 0) { + _params = _toUrl.substring(index +1); + _toUrl = _toUrl.substring(0, index); + } + } + virtual ~AsyncWebRewrite(){} + AsyncWebRewrite& setFilter(ArRequestFilterFunction fn) { _filter = fn; return *this; } + bool filter(AsyncWebServerRequest *request) const { return _filter == NULL || _filter(request); } + const String& from(void) const { return _from; } + const String& toUrl(void) const { return _toUrl; } + const String& params(void) const { return _params; } + virtual bool match(AsyncWebServerRequest *request) { return from() == request->url() && filter(request); } +}; + +/* + * HANDLER :: One instance can be attached to any Request (done by the Server) + * */ + +class AsyncWebHandler { + protected: + ArRequestFilterFunction _filter; + String _username; + String _password; + public: + AsyncWebHandler():_username(""), _password(""){} + AsyncWebHandler& setFilter(ArRequestFilterFunction fn) { _filter = fn; return *this; } + AsyncWebHandler& setAuthentication(const char *username, const char *password){ _username = String(username);_password = String(password); return *this; }; + bool filter(AsyncWebServerRequest *request){ return _filter == NULL || _filter(request); } + virtual ~AsyncWebHandler(){} + virtual bool canHandle(AsyncWebServerRequest *request __attribute__((unused))){ + return false; + } + virtual void handleRequest(AsyncWebServerRequest *request __attribute__((unused))){} + virtual void handleUpload(AsyncWebServerRequest *request __attribute__((unused)), const String& filename __attribute__((unused)), size_t index __attribute__((unused)), uint8_t *data __attribute__((unused)), size_t len __attribute__((unused)), bool final __attribute__((unused))){} + virtual void handleBody(AsyncWebServerRequest *request __attribute__((unused)), uint8_t *data __attribute__((unused)), size_t len __attribute__((unused)), size_t index __attribute__((unused)), size_t total __attribute__((unused))){} + virtual bool isRequestHandlerTrivial(){return true;} +}; + +/* + * RESPONSE :: One instance is created for each Request (attached by the Handler) + * */ + +typedef enum { + RESPONSE_SETUP, RESPONSE_HEADERS, RESPONSE_CONTENT, RESPONSE_WAIT_ACK, RESPONSE_END, RESPONSE_FAILED +} WebResponseState; + +class AsyncWebServerResponse { + protected: + int _code; + LinkedList _headers; + String _contentType; + size_t _contentLength; + bool _sendContentLength; + bool _chunked; + size_t _headLength; + size_t _sentLength; + size_t _ackedLength; + size_t _writtenLength; + WebResponseState _state; + const char* _responseCodeToString(int code); + + public: + AsyncWebServerResponse(); + virtual ~AsyncWebServerResponse(); + virtual void setCode(int code); + virtual void setContentLength(size_t len); + virtual void setContentType(const String& type); + virtual void addHeader(const String& name, const String& value); + virtual String _assembleHead(uint8_t version); + virtual bool _started() const; + virtual bool _finished() const; + virtual bool _failed() const; + virtual bool _sourceValid() const; + virtual void _respond(AsyncWebServerRequest *request); + virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); +}; + +/* + * SERVER :: One instance + * */ + +typedef std::function ArRequestHandlerFunction; +typedef std::function ArUploadHandlerFunction; +typedef std::function ArBodyHandlerFunction; + +class AsyncWebServer { + protected: + AsyncServer _server; + LinkedList _rewrites; + LinkedList _handlers; + AsyncCallbackWebHandler* _catchAllHandler; + + public: + AsyncWebServer(uint16_t port); + ~AsyncWebServer(); + + void begin(); + void end(); + +#if ASYNC_TCP_SSL_ENABLED + void onSslFileRequest(AcSSlFileHandler cb, void* arg); + void beginSecure(const char *cert, const char *private_key_file, const char *password); +#endif + + AsyncWebRewrite& addRewrite(AsyncWebRewrite* rewrite); + bool removeRewrite(AsyncWebRewrite* rewrite); + AsyncWebRewrite& rewrite(const char* from, const char* to); + + AsyncWebHandler& addHandler(AsyncWebHandler* handler); + bool removeHandler(AsyncWebHandler* handler); + + AsyncCallbackWebHandler& on(const char* uri, ArRequestHandlerFunction onRequest); + AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest); + AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload); + AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody); + + AsyncStaticWebHandler& serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL); + + void onNotFound(ArRequestHandlerFunction fn); //called when handler is not assigned + void onFileUpload(ArUploadHandlerFunction fn); //handle file uploads + void onRequestBody(ArBodyHandlerFunction fn); //handle posts with plain body content (JSON often transmitted this way as a request) + + void reset(); //remove all writers and handlers, with onNotFound/onFileUpload/onRequestBody + + void _handleDisconnect(AsyncWebServerRequest *request); + void _attachHandler(AsyncWebServerRequest *request); + void _rewriteRequest(AsyncWebServerRequest *request); +}; + +class DefaultHeaders { + using headers_t = LinkedList; + headers_t _headers; + + DefaultHeaders() + :_headers(headers_t([](AsyncWebHeader *h){ delete h; })) + {} +public: + using ConstIterator = headers_t::ConstIterator; + + void addHeader(const String& name, const String& value){ + _headers.add(new AsyncWebHeader(name, value)); + } + + ConstIterator begin() const { return _headers.begin(); } + ConstIterator end() const { return _headers.end(); } + + DefaultHeaders(DefaultHeaders const &) = delete; + DefaultHeaders &operator=(DefaultHeaders const &) = delete; + static DefaultHeaders &Instance() { + static DefaultHeaders instance; + return instance; + } +}; + +#include "WebResponseImpl.h" +#include "WebHandlerImpl.h" +#include "AsyncWebSocket.h" +#include "AsyncEventSource.h" + +#endif /* _AsyncWebServer_H_ */ diff --git a/yoRadio/src/AsyncWebServer/SPIFFSEditor.cpp b/yoRadio/src/AsyncWebServer/SPIFFSEditor.cpp new file mode 100644 index 0000000..a84fa87 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/SPIFFSEditor.cpp @@ -0,0 +1,544 @@ +#include "SPIFFSEditor.h" +#include + +//File: edit.htm.gz, Size: 4151 +#define edit_htm_gz_len 4151 +const uint8_t edit_htm_gz[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0xB8, 0x94, 0xB1, 0x59, 0x00, 0x03, 0x65, 0x64, 0x69, 0x74, 0x2E, 0x68, + 0x74, 0x6D, 0x00, 0xB5, 0x3A, 0x0B, 0x7B, 0xDA, 0xB8, 0xB2, 0x7F, 0xC5, 0x71, 0xCF, 0x66, 0xED, + 0x83, 0x31, 0x90, 0xA4, 0xD9, 0xD6, 0xC4, 0xC9, 0x42, 0x92, 0x36, 0x6D, 0xF3, 0x6A, 0x80, 0xB6, + 0x69, 0x4F, 0xEE, 0x7E, 0xC2, 0x16, 0xA0, 0xC6, 0x96, 0x5D, 0x5B, 0x0E, 0x49, 0x59, 0xFE, 0xFB, + 0x9D, 0x91, 0x6C, 0xB0, 0x09, 0x69, 0x77, 0xCF, 0xBD, 0xBB, 0xDD, 0x2D, 0x92, 0x46, 0x33, 0x9A, + 0x19, 0xCD, 0x53, 0xDE, 0xBD, 0x8D, 0xA3, 0x8B, 0xC3, 0xFE, 0xF5, 0xE5, 0xB1, 0x36, 0x11, 0x61, + 0xB0, 0xBF, 0x87, 0x7F, 0x6B, 0x01, 0xE1, 0x63, 0x97, 0xF2, 0xFD, 0x3D, 0xC1, 0x44, 0x40, 0xF7, + 0x8F, 0x7B, 0x97, 0xDA, 0xB1, 0xCF, 0x44, 0x94, 0xEC, 0x35, 0xD4, 0xCA, 0x5E, 0x2A, 0x1E, 0x02, + 0xAA, 0x85, 0xD4, 0x67, 0xC4, 0x4D, 0xBD, 0x84, 0xC2, 0x66, 0xDB, 0x0B, 0x67, 0xDF, 0xEB, 0x8C, + 0xFB, 0xF4, 0xDE, 0xD9, 0x6E, 0x36, 0xDB, 0x71, 0x94, 0x32, 0xC1, 0x22, 0xEE, 0x90, 0x61, 0x1A, + 0x05, 0x99, 0xA0, 0xED, 0x80, 0x8E, 0x84, 0xF3, 0x3C, 0xBE, 0x6F, 0x0F, 0xA3, 0xC4, 0xA7, 0x89, + 0xD3, 0x8A, 0xEF, 0x35, 0x00, 0x31, 0x5F, 0x7B, 0xB6, 0xB3, 0xB3, 0xD3, 0x1E, 0x12, 0xEF, 0x76, + 0x9C, 0x44, 0x19, 0xF7, 0xEB, 0x5E, 0x14, 0x44, 0x89, 0xF3, 0x6C, 0xF4, 0x1C, 0xFF, 0xB4, 0x7D, + 0x96, 0xC6, 0x01, 0x79, 0x70, 0x78, 0xC4, 0x29, 0xE0, 0xDE, 0xD7, 0xD3, 0x09, 0xF1, 0xA3, 0xA9, + 0xD3, 0xD4, 0x9A, 0x5A, 0xAB, 0x09, 0x44, 0x92, 0xF1, 0x90, 0x18, 0x4D, 0x0B, 0xFF, 0xD8, 0x3B, + 0x66, 0x7B, 0x14, 0x71, 0x51, 0x4F, 0xD9, 0x77, 0xEA, 0xB4, 0xB6, 0xE0, 0x34, 0x39, 0x1D, 0x91, + 0x90, 0x05, 0x0F, 0x4E, 0x4A, 0x78, 0x5A, 0x4F, 0x69, 0xC2, 0x46, 0x6A, 0x79, 0x4A, 0xD9, 0x78, + 0x22, 0x9C, 0xDF, 0x9A, 0xCD, 0x39, 0xF0, 0xAF, 0x65, 0xC1, 0x2C, 0x60, 0x29, 0x20, 0xA3, 0x78, + 0xEA, 0x3C, 0x11, 0xC5, 0x4E, 0x53, 0xB1, 0xDE, 0x6C, 0x87, 0x24, 0x19, 0x33, 0x0E, 0x83, 0x98, + 0xF8, 0x3E, 0xE3, 0x63, 0x47, 0xA1, 0x05, 0x6C, 0xB6, 0x90, 0x36, 0xA1, 0x01, 0x11, 0xEC, 0x8E, + 0xB6, 0x43, 0xC6, 0xEB, 0x53, 0xE6, 0x8B, 0x89, 0xB3, 0x0B, 0x3C, 0xB6, 0xBD, 0x2C, 0x49, 0x41, + 0xA6, 0x38, 0x62, 0x5C, 0xD0, 0x44, 0xA2, 0xA5, 0x31, 0xE1, 0xB3, 0x5C, 0x54, 0x54, 0x40, 0x21, + 0x27, 0xE3, 0x01, 0xE3, 0xB4, 0x3E, 0x0C, 0x22, 0xEF, 0x76, 0x71, 0xD2, 0x6E, 0x7C, 0x9F, 0x9F, + 0xE5, 0x4C, 0xA2, 0x3B, 0x9A, 0xCC, 0x96, 0xEA, 0x92, 0xD8, 0x15, 0x60, 0x85, 0x34, 0xA5, 0x74, + 0x6E, 0x8B, 0xBB, 0x0C, 0xA0, 0x96, 0xFC, 0x05, 0x29, 0x17, 0xFC, 0x2F, 0x45, 0x5A, 0x11, 0x5C, + 0xA1, 0x30, 0x1E, 0x67, 0x62, 0xF6, 0xF8, 0x2A, 0xA3, 0x98, 0x78, 0x4C, 0x3C, 0xA0, 0xFC, 0xB0, + 0x6D, 0x86, 0xBA, 0x04, 0xAC, 0x24, 0x24, 0x81, 0x86, 0x3A, 0xD7, 0x3E, 0xD0, 0xC4, 0x27, 0x9C, + 0x58, 0x9D, 0x84, 0x91, 0xC0, 0xEA, 0x2D, 0xB5, 0x5E, 0x0F, 0xA3, 0xEF, 0xF5, 0x0C, 0xC6, 0x30, + 0x0F, 0xA8, 0x27, 0x94, 0x92, 0xE1, 0x1E, 0x86, 0xB7, 0x4C, 0x3C, 0x06, 0x3C, 0x5A, 0x28, 0xA9, + 0x4B, 0x2A, 0x69, 0xA2, 0x2E, 0xB0, 0x25, 0xD5, 0x83, 0x1C, 0x4B, 0xC9, 0x95, 0x50, 0xF5, 0x61, + 0x24, 0x44, 0x14, 0x4A, 0x93, 0x5B, 0x08, 0xAC, 0x49, 0xAB, 0x79, 0xF1, 0xE8, 0x46, 0xD6, 0x6B, + 0xBF, 0x44, 0xBE, 0x0D, 0x7A, 0x15, 0xCC, 0x23, 0x41, 0x9D, 0x04, 0x6C, 0xCC, 0x9D, 0x90, 0xF9, + 0x7E, 0x40, 0x4B, 0x56, 0xEB, 0x64, 0x49, 0x60, 0xF8, 0x44, 0x10, 0x87, 0x85, 0x64, 0x4C, 0x1B, + 0x31, 0x1F, 0x03, 0x34, 0xA5, 0xBB, 0x3B, 0x16, 0xFB, 0xD0, 0xBD, 0xB8, 0x9A, 0x36, 0xDF, 0xBD, + 0x1E, 0x47, 0x1D, 0xF8, 0xE7, 0xBC, 0x37, 0x98, 0x1C, 0x0F, 0xC6, 0x30, 0xEA, 0xE2, 0xB4, 0xF3, + 0xFE, 0xB0, 0xF3, 0x1E, 0x7E, 0x0E, 0x5B, 0xB5, 0xAF, 0xA3, 0x6F, 0xB8, 0xD0, 0x7D, 0xED, 0x77, + 0xFB, 0x83, 0xE3, 0x4E, 0xE7, 0x5D, 0xE3, 0xCD, 0xF9, 0xF4, 0xE3, 0xBB, 0x5D, 0x04, 0x77, 0x83, + 0xE6, 0xD5, 0x87, 0x49, 0x73, 0xB0, 0xF5, 0x32, 0xF4, 0x4F, 0xFC, 0x89, 0x17, 0x0E, 0x3A, 0xEF, + 0x3F, 0x5E, 0xDD, 0x5D, 0x87, 0x83, 0x71, 0xEF, 0x63, 0x6B, 0xF2, 0x79, 0xEB, 0x43, 0xEF, 0xF3, + 0xC7, 0x57, 0xB7, 0xF4, 0xD3, 0xC9, 0xDB, 0xCF, 0xFD, 0x29, 0x20, 0x1C, 0x45, 0xBD, 0xC1, 0x55, + 0xF7, 0x43, 0x77, 0xFC, 0xB9, 0xEB, 0x1D, 0xDF, 0x0F, 0x83, 0xF3, 0xEE, 0xEB, 0xCE, 0xB0, 0xB3, + 0xE5, 0x51, 0x3A, 0xEE, 0x5F, 0x75, 0xB3, 0x37, 0xEF, 0x2E, 0xC6, 0x8C, 0x4D, 0x7A, 0x9F, 0xCF, + 0xFB, 0xDE, 0xE1, 0xF3, 0xD3, 0xC1, 0x49, 0x87, 0x4D, 0xCE, 0xDF, 0x5E, 0x35, 0x6F, 0x5F, 0xBF, + 0x3B, 0x3C, 0xF2, 0xAE, 0xDF, 0x5E, 0xEF, 0x1E, 0x6D, 0x37, 0x7E, 0xFB, 0xED, 0xCC, 0xBF, 0x60, + 0xBC, 0x7F, 0xF7, 0xBD, 0x33, 0x3E, 0x9C, 0xBE, 0x78, 0x48, 0xFB, 0x93, 0x37, 0x77, 0xBC, 0xF1, + 0x21, 0xFA, 0xFA, 0xE6, 0xE1, 0x0C, 0xFE, 0xBB, 0xBC, 0xAC, 0x0D, 0x7B, 0xAD, 0x74, 0xF0, 0xFE, + 0xCD, 0x87, 0xAD, 0xF4, 0xE5, 0xF3, 0xB8, 0x7B, 0x74, 0x74, 0x17, 0x0E, 0x2F, 0x1B, 0xA1, 0x7F, + 0x3B, 0x12, 0x2F, 0xB6, 0x45, 0x7C, 0x3D, 0xCE, 0x3E, 0x7F, 0x7B, 0xFE, 0x76, 0xD2, 0xB8, 0xA0, + 0xE4, 0x7A, 0x52, 0x7B, 0xF8, 0xFE, 0xF0, 0x62, 0xD2, 0x3F, 0xB9, 0x3B, 0x0F, 0xC8, 0xFD, 0xF9, + 0xB9, 0xF7, 0x3D, 0xAC, 0x05, 0xE4, 0xE5, 0x45, 0x3F, 0x20, 0x49, 0x6B, 0xE0, 0x77, 0x1A, 0xB5, + 0xC3, 0xAD, 0xCE, 0x8E, 0x48, 0xAE, 0x0E, 0xF9, 0xD1, 0xF6, 0xD7, 0xDE, 0x8B, 0x6E, 0xB7, 0x15, + 0x0D, 0xBF, 0x6D, 0xBD, 0xBE, 0xDD, 0x7D, 0x3D, 0xD8, 0x7D, 0x3F, 0x7C, 0xDF, 0xE9, 0xED, 0x74, + 0x07, 0xE4, 0xBA, 0xF7, 0xBE, 0x33, 0xDA, 0x19, 0x4E, 0x26, 0xEF, 0xDE, 0xF5, 0x5F, 0xF9, 0x9D, + 0xEF, 0x49, 0xE7, 0x62, 0xDA, 0xB9, 0x3F, 0x1E, 0x74, 0x4E, 0x6A, 0xEF, 0x8E, 0xCF, 0x9A, 0xAD, + 0xDE, 0xF5, 0xF6, 0xF8, 0x6C, 0x77, 0xDA, 0x4D, 0x8F, 0x3B, 0xEF, 0xBB, 0xCD, 0xF1, 0xDB, 0x5A, + 0x48, 0x3E, 0x47, 0x87, 0xDB, 0xE3, 0x37, 0xBB, 0xEC, 0xF2, 0x9A, 0x74, 0xDE, 0x74, 0xDF, 0xA6, + 0xEC, 0x2A, 0x3C, 0x19, 0x34, 0x3B, 0x9D, 0xD3, 0x0B, 0xFA, 0xEA, 0x70, 0x9B, 0xBC, 0xDB, 0xF2, + 0x3E, 0x82, 0xFE, 0x07, 0x9F, 0xE8, 0x6F, 0xB5, 0xCE, 0xF4, 0xA2, 0x19, 0x78, 0x2F, 0x69, 0xFF, + 0xE4, 0xBA, 0x2F, 0x6F, 0xE7, 0x38, 0x78, 0xD5, 0xBF, 0xED, 0x65, 0xEF, 0xC3, 0xC3, 0x43, 0x53, + 0xE3, 0x51, 0x3D, 0xA1, 0x31, 0x25, 0xA2, 0x1C, 0xAE, 0x16, 0xFE, 0x01, 0xB6, 0xB5, 0xB4, 0xC2, + 0xDC, 0x4F, 0x05, 0xBD, 0x17, 0x75, 0x9F, 0x7A, 0x51, 0x42, 0xE4, 0x1E, 0x40, 0xA0, 0x09, 0x9A, + 0xD8, 0xFC, 0x77, 0x19, 0x3F, 0x35, 0x15, 0x3F, 0x35, 0xC2, 0x7D, 0xCD, 0x28, 0x1C, 0x01, 0x83, + 0x87, 0x4F, 0xEF, 0x98, 0x47, 0xEB, 0x31, 0xBB, 0xA7, 0x41, 0x5D, 0x22, 0x3B, 0x4D, 0x73, 0x26, + 0xFD, 0xAD, 0xD8, 0x46, 0x38, 0x98, 0x9A, 0xA4, 0x5A, 0x2C, 0xF8, 0x5F, 0x89, 0x47, 0x21, 0xB0, + 0x81, 0xCB, 0x84, 0xF8, 0xAB, 0x7C, 0x27, 0x4A, 0xEA, 0xC3, 0x6C, 0x3C, 0x62, 0xF7, 0xE0, 0xD0, + 0x23, 0xC6, 0x99, 0xA0, 0x5A, 0x2B, 0x9D, 0xFF, 0x5E, 0x90, 0xB9, 0xA5, 0x0F, 0xA3, 0x84, 0x84, + 0x34, 0xD5, 0xFE, 0x22, 0x99, 0xD9, 0x28, 0x89, 0xC2, 0x65, 0x10, 0x99, 0x8B, 0xA8, 0x34, 0x99, + 0xCF, 0x9F, 0x65, 0x71, 0x10, 0x11, 0x10, 0x73, 0x4D, 0xE4, 0x50, 0xF1, 0x34, 0x91, 0x6E, 0xB5, + 0x88, 0xAB, 0xB9, 0x9B, 0x6D, 0xA1, 0x5B, 0x96, 0xDD, 0x7A, 0x6B, 0x67, 0xE9, 0xBA, 0x75, 0xB9, + 0x17, 0xE3, 0xFD, 0x9A, 0x4C, 0x81, 0xF1, 0xA0, 0x14, 0xEE, 0x9E, 0x09, 0x50, 0xE9, 0x13, 0x87, + 0xCB, 0x43, 0xF2, 0xC8, 0xB0, 0x60, 0x40, 0x05, 0xEA, 0x96, 0x8C, 0xD4, 0x85, 0x24, 0xB0, 0x6F, + 0xFE, 0x8C, 0xCA, 0xBC, 0x67, 0x3D, 0x8B, 0x13, 0xB8, 0x0D, 0x3A, 0xFD, 0x11, 0xCD, 0x42, 0xA6, + 0x2A, 0x6D, 0x45, 0x53, 0x65, 0xBC, 0x5C, 0x84, 0x65, 0xDA, 0x93, 0xBC, 0x16, 0xA4, 0x1F, 0x4B, + 0x05, 0xE0, 0x05, 0x37, 0xCF, 0x91, 0x9B, 0x1F, 0x6A, 0x75, 0x7B, 0xF7, 0x97, 0x9C, 0x87, 0x9D, + 0xE6, 0x2F, 0x73, 0x3B, 0xDF, 0x5B, 0xA4, 0xE4, 0x56, 0x13, 0xFE, 0x29, 0x32, 0xEF, 0x8B, 0x25, + 0x0B, 0xC3, 0xE7, 0xF8, 0xA7, 0x60, 0x10, 0xE9, 0x94, 0x80, 0xDB, 0x3B, 0x2F, 0x5F, 0xF8, 0xC3, + 0x02, 0x98, 0x0B, 0xF6, 0x24, 0x3C, 0x21, 0x3E, 0xCB, 0x52, 0xE7, 0x79, 0xF3, 0x97, 0x5C, 0x9F, + 0x5B, 0x3B, 0x28, 0xFB, 0xE2, 0x2E, 0x71, 0xB2, 0xB4, 0xD8, 0x34, 0x66, 0x5C, 0xDB, 0x4A, 0x35, + 0xBC, 0x6F, 0x92, 0x2C, 0x0C, 0xB3, 0x92, 0xED, 0xE7, 0xBF, 0x2F, 0x4D, 0x13, 0xF7, 0xCF, 0x9A, + 0xBF, 0xCC, 0x44, 0x02, 0xD9, 0x64, 0x04, 0xB9, 0xC6, 0x49, 0x22, 0x41, 0x04, 0x35, 0x9A, 0xE6, + 0x1C, 0x84, 0x5B, 0x03, 0xD8, 0xDE, 0x6D, 0xFA, 0x74, 0x6C, 0xCE, 0xE7, 0x7B, 0x0D, 0x99, 0xD7, + 0xA0, 0x6C, 0xF1, 0x12, 0x16, 0x8B, 0xFD, 0x51, 0xC6, 0x3D, 0xE4, 0x41, 0x1B, 0x53, 0x83, 0x9A, + 0xB3, 0x84, 0x8A, 0x2C, 0xE1, 0x9A, 0x1F, 0x79, 0x19, 0x1A, 0xBB, 0x3D, 0xA6, 0xE2, 0x58, 0xD9, + 0x7D, 0xF7, 0xE1, 0x8D, 0x0F, 0x3B, 0xE6, 0x0B, 0x04, 0x6F, 0x2D, 0x02, 0x38, 0x30, 0x9C, 0x97, + 0xE3, 0x54, 0xF6, 0x43, 0x82, 0x01, 0x22, 0xEF, 0xE8, 0x83, 0x41, 0x2D, 0xB1, 0x40, 0xA4, 0x36, + 0xAE, 0x1B, 0xC5, 0x2E, 0x80, 0x71, 0x73, 0x76, 0x07, 0x4A, 0x20, 0x2E, 0xFD, 0x22, 0x6E, 0x2C, + 0xE6, 0x72, 0xF8, 0x69, 0xE7, 0xBB, 0xC9, 0x1E, 0x3B, 0xA8, 0xB7, 0x1C, 0xB2, 0xCF, 0x0E, 0x5A, + 0xE0, 0x5E, 0x65, 0x6E, 0xE4, 0xB9, 0xAF, 0x58, 0x40, 0x07, 0xB9, 0xC3, 0xE1, 0x31, 0x48, 0x6C, + 0xB1, 0x85, 0x28, 0xE2, 0x5B, 0xCD, 0xE6, 0x86, 0x4B, 0x0F, 0x48, 0x00, 0x39, 0xCC, 0xD0, 0x8F, + 0xAF, 0xAE, 0x2E, 0xAE, 0xBE, 0xE8, 0x35, 0x5A, 0xD3, 0x6F, 0x1C, 0x4D, 0xAF, 0x71, 0xD3, 0x11, + 0x76, 0x42, 0x47, 0x09, 0x4D, 0x27, 0x97, 0x44, 0x4C, 0x8C, 0xD4, 0xBE, 0x23, 0x41, 0x56, 0x16, + 0x84, 0xA1, 0xDC, 0xC8, 0xA2, 0x70, 0x39, 0x9D, 0x6A, 0xAF, 0x40, 0xCD, 0x47, 0x90, 0xEA, 0xDA, + 0xC2, 0x26, 0x71, 0x4C, 0xB9, 0x6F, 0xE8, 0x31, 0x20, 0xEA, 0x16, 0x35, 0xAD, 0x84, 0x7E, 0xCB, + 0x68, 0x2A, 0x52, 0x1B, 0x2C, 0xD7, 0xD0, 0x2F, 0x07, 0x7D, 0xDD, 0xD2, 0x1B, 0xE8, 0x47, 0x3A, + 0xF0, 0x46, 0xCC, 0x39, 0x52, 0x89, 0x5C, 0xD0, 0xA4, 0x3E, 0xCC, 0xC0, 0xA0, 0xB8, 0x6E, 0xB6, + 0x23, 0x9B, 0x71, 0x4E, 0x93, 0x93, 0xFE, 0xD9, 0xA9, 0xAB, 0x5F, 0x29, 0x46, 0xB4, 0x53, 0x28, + 0x48, 0x74, 0x4B, 0x5E, 0x51, 0x7E, 0xC8, 0xE1, 0x84, 0x05, 0xBE, 0x11, 0x99, 0x6D, 0x24, 0xE1, + 0x49, 0x12, 0xB2, 0x40, 0x01, 0x0A, 0x9E, 0x2D, 0x1E, 0x62, 0xEA, 0xEA, 0x23, 0x50, 0x86, 0x6E, + 0x79, 0x76, 0x98, 0x05, 0x82, 0xC5, 0x01, 0x75, 0x37, 0x5A, 0x30, 0xE3, 0x60, 0x41, 0xAE, 0x8E, + 0xB9, 0x19, 0x61, 0xCC, 0x77, 0x75, 0x15, 0xA1, 0xF2, 0xB8, 0xB6, 0xEE, 0x14, 0x4F, 0x9D, 0x92, + 0x56, 0x4E, 0x49, 0xCB, 0xB8, 0x4A, 0xE0, 0x34, 0x3F, 0x18, 0xC3, 0x3C, 0xCE, 0xD4, 0x51, 0x05, + 0xCC, 0xA7, 0x23, 0x02, 0x9C, 0x7C, 0x40, 0x6D, 0xBA, 0x7A, 0x63, 0xDD, 0x41, 0xA9, 0x3A, 0xC8, + 0xAF, 0x6A, 0xC4, 0x2F, 0x6B, 0x44, 0xDD, 0xEE, 0x3A, 0x64, 0x5F, 0x21, 0x07, 0x55, 0xE4, 0xA0, + 0x8C, 0x7C, 0x28, 0x8D, 0x64, 0x1D, 0x72, 0xA0, 0x90, 0x93, 0x8A, 0x88, 0x89, 0x14, 0x51, 0x85, + 0xBD, 0x3A, 0x6A, 0x13, 0x05, 0xD2, 0xAD, 0xA4, 0x22, 0x66, 0x62, 0x83, 0x97, 0x92, 0x61, 0x40, + 0x7D, 0x77, 0xA3, 0x09, 0x33, 0x2C, 0xB6, 0xDD, 0xAD, 0xE6, 0x9A, 0x33, 0x12, 0x75, 0x46, 0x56, + 0x65, 0x30, 0x2B, 0x33, 0xA8, 0xF5, 0xC8, 0x1D, 0xD5, 0xD6, 0x31, 0x98, 0x99, 0x56, 0x60, 0x47, + 0xDC, 0x0B, 0x98, 0x77, 0xEB, 0x2E, 0xBD, 0xC5, 0x9C, 0xB1, 0x85, 0x85, 0x5A, 0x5C, 0x06, 0xBA, + 0x01, 0x94, 0x5E, 0x8B, 0xA5, 0x7C, 0x80, 0xFA, 0x9E, 0x5B, 0xD9, 0x5A, 0x02, 0xDC, 0xA6, 0xF7, + 0xD4, 0x3B, 0x8C, 0xC2, 0x90, 0xA0, 0xED, 0xA6, 0xC0, 0x41, 0x3E, 0xD1, 0xCD, 0xB9, 0x15, 0xAD, + 0xC5, 0x79, 0xC2, 0x45, 0x2C, 0x7F, 0x3D, 0x8B, 0x23, 0x03, 0x5C, 0xCE, 0xF5, 0x6C, 0xD4, 0x61, + 0x6A, 0x83, 0x1E, 0xC7, 0x62, 0xF2, 0x13, 0x17, 0x2A, 0x0C, 0x54, 0xA2, 0x7C, 0x69, 0xDE, 0x58, + 0x0B, 0x91, 0x56, 0x7C, 0xEA, 0xA2, 0xB7, 0xE2, 0x54, 0xA8, 0xBC, 0x8A, 0x5D, 0x9A, 0x4B, 0x1D, + 0x94, 0x61, 0xB9, 0xBD, 0x2F, 0xA0, 0xFA, 0x7C, 0x0E, 0xE7, 0x01, 0xFF, 0x13, 0x68, 0xF9, 0xE8, + 0x5F, 0x17, 0x60, 0xC9, 0xA3, 0x34, 0x78, 0x8B, 0xBB, 0x0D, 0xE3, 0xC0, 0xF9, 0x8F, 0x6D, 0x7C, + 0xF9, 0x1F, 0xFB, 0xA6, 0x66, 0x9A, 0x07, 0xFF, 0x6A, 0x48, 0x0D, 0x1B, 0xC2, 0xFC, 0xD2, 0xBA, + 0xB1, 0x08, 0x80, 0xED, 0x7F, 0x9B, 0xFF, 0xB1, 0x25, 0xB8, 0x02, 0x6B, 0xDF, 0x45, 0x90, 0x49, + 0xF0, 0x24, 0x34, 0xB0, 0x68, 0xA4, 0x91, 0xCD, 0x4D, 0x43, 0xB8, 0xA4, 0x72, 0x8D, 0x35, 0x51, + 0xD3, 0x6D, 0x88, 0x53, 0x50, 0x5B, 0xAC, 0x04, 0xBF, 0x3E, 0x24, 0x7A, 0x15, 0x5B, 0x17, 0x00, + 0xC9, 0x3D, 0xCA, 0x0C, 0x3D, 0x22, 0x97, 0x52, 0xCB, 0x0C, 0x02, 0x42, 0xA7, 0x89, 0xE7, 0x2A, + 0xAD, 0x1D, 0x14, 0x30, 0x17, 0xA2, 0xE0, 0xBC, 0x1C, 0x2D, 0x15, 0xEA, 0xAA, 0xFD, 0x17, 0x0A, + 0xA3, 0xD6, 0x12, 0x8A, 0x04, 0x31, 0xAD, 0xD8, 0x79, 0xC6, 0x72, 0x75, 0x4C, 0x59, 0xBA, 0x35, + 0x59, 0x5D, 0x96, 0xAD, 0x04, 0xAE, 0x2F, 0x8D, 0xFE, 0xD7, 0x3D, 0x16, 0x8E, 0xB5, 0x12, 0x3F, + 0xF8, 0x97, 0xFB, 0x2B, 0x46, 0xE4, 0xCD, 0x3F, 0xBC, 0x21, 0x70, 0x05, 0xA6, 0x41, 0x6D, 0x1E, + 0x4D, 0x0D, 0xB3, 0xF6, 0xAB, 0xAE, 0x49, 0x8A, 0xAE, 0x1E, 0x92, 0xFB, 0xBC, 0xA7, 0xC4, 0x8C, + 0xD7, 0xD6, 0x70, 0x5E, 0xB4, 0x28, 0xF9, 0x82, 0xEC, 0xE6, 0x48, 0x26, 0xA2, 0xB6, 0x56, 0x64, + 0x52, 0xD5, 0xCA, 0xE8, 0x5A, 0x63, 0xFF, 0xD7, 0x4A, 0x40, 0xB7, 0x98, 0xBA, 0x4E, 0x15, 0x8C, + 0xB3, 0x00, 0x1C, 0x93, 0x3E, 0x1D, 0x69, 0x03, 0x26, 0x03, 0x75, 0x35, 0x46, 0x5A, 0x81, 0xC1, + 0xCC, 0x03, 0xC3, 0x2B, 0xFB, 0xF3, 0x1E, 0x16, 0xBF, 0xFB, 0x97, 0xAA, 0xAA, 0x81, 0xD4, 0x8B, + 0x33, 0x5D, 0x59, 0x59, 0xD5, 0x4B, 0xE0, 0xD2, 0x08, 0xA0, 0x5B, 0x8B, 0x3C, 0x3A, 0x8C, 0xFC, + 0x87, 0x52, 0xF6, 0x4D, 0xBB, 0x0F, 0x87, 0x01, 0x49, 0xD3, 0x73, 0xB8, 0x01, 0x43, 0xF7, 0x42, + 0x50, 0xB8, 0xB2, 0xC2, 0xFD, 0xE6, 0xE6, 0x66, 0x15, 0x29, 0xA1, 0x21, 0x14, 0xDB, 0x8A, 0x2B, + 0xF0, 0x49, 0xD3, 0xF1, 0x81, 0x30, 0x18, 0xD2, 0x1A, 0xC6, 0xF0, 0x25, 0xE3, 0x47, 0x5C, 0x71, + 0xF4, 0xF4, 0x22, 0xA6, 0xFC, 0x33, 0xDC, 0x95, 0x32, 0xCB, 0x1A, 0xAD, 0xA6, 0x68, 0xFA, 0x8F, + 0xD8, 0x3E, 0xCA, 0x0D, 0x76, 0xC1, 0x7A, 0xBA, 0x56, 0xA1, 0xFC, 0x9F, 0x61, 0xB9, 0x94, 0x28, + 0xD6, 0x70, 0x9C, 0x40, 0x80, 0x5A, 0xC3, 0x31, 0xC4, 0x1A, 0x41, 0x17, 0xFC, 0x26, 0x6B, 0xF9, + 0xCD, 0xFE, 0x19, 0x7E, 0x97, 0x76, 0x1E, 0x15, 0x25, 0x91, 0xAA, 0xAF, 0x50, 0x02, 0x9F, 0xDD, + 0xE9, 0xA6, 0x15, 0xB9, 0x55, 0x0A, 0x50, 0x1B, 0x46, 0x41, 0xD0, 0x8F, 0xE2, 0x83, 0x27, 0xD6, + 0x9D, 0xC5, 0x7A, 0x31, 0xC8, 0xD9, 0x5C, 0x6E, 0xB1, 0xBC, 0xB5, 0x44, 0x4F, 0xA1, 0xEC, 0x5F, + 0x4B, 0x15, 0x01, 0x3F, 0x23, 0x8B, 0x7B, 0xAC, 0xD4, 0xA5, 0x36, 0x28, 0x0F, 0x56, 0x3F, 0xD5, + 0x3C, 0xCB, 0x5F, 0xCC, 0xAE, 0x6B, 0x51, 0x9B, 0xC0, 0x38, 0x57, 0x92, 0x8B, 0x4A, 0xB2, 0xC8, + 0x13, 0x01, 0xA8, 0x58, 0xC7, 0x2E, 0xC4, 0x4D, 0x6B, 0x7A, 0x7C, 0xBF, 0x5C, 0x83, 0xC2, 0xDF, + 0xF5, 0xD5, 0x12, 0x33, 0x08, 0xC4, 0xD3, 0x95, 0x4B, 0x29, 0x5F, 0x37, 0x29, 0x8A, 0x0E, 0x62, + 0x47, 0xA3, 0x51, 0x4A, 0xC5, 0x47, 0x0C, 0x49, 0x56, 0xB2, 0x98, 0x9F, 0xC8, 0x90, 0x04, 0x8C, + 0x45, 0x3C, 0x8C, 0xB2, 0x94, 0x46, 0x99, 0xA8, 0xA4, 0x16, 0x63, 0x21, 0xCC, 0x5E, 0xFA, 0xE7, + 0x9F, 0x8B, 0xC9, 0x7E, 0x5A, 0x0B, 0x96, 0xD3, 0xEB, 0x3D, 0xBF, 0x34, 0xD9, 0xF7, 0x6B, 0x89, + 0xB9, 0x7A, 0xE9, 0xFF, 0x67, 0x4B, 0x21, 0x65, 0x4B, 0xF1, 0xB0, 0x54, 0x2E, 0x62, 0x62, 0x29, + 0xE6, 0xC9, 0x82, 0x91, 0x97, 0x7C, 0x16, 0x0D, 0x1A, 0x2B, 0x25, 0x55, 0x9E, 0x97, 0x7D, 0x95, + 0x43, 0x40, 0x59, 0x71, 0xE5, 0x35, 0x11, 0x06, 0x34, 0xE0, 0x63, 0x64, 0xF2, 0x41, 0xEB, 0xA7, + 0xD1, 0x94, 0x26, 0x87, 0x24, 0xA5, 0x06, 0x24, 0xCD, 0x65, 0xDC, 0x41, 0xA8, 0xE9, 0x04, 0xEB, + 0x76, 0x6D, 0x6E, 0x12, 0x05, 0xCE, 0x33, 0x77, 0xC4, 0xB1, 0x26, 0x03, 0xF9, 0xB2, 0xCA, 0x09, + 0xD4, 0xC6, 0xBE, 0x12, 0xA4, 0x3E, 0x52, 0x25, 0xA8, 0x61, 0x5A, 0xD0, 0x76, 0xC0, 0x35, 0x5F, + 0x26, 0x51, 0x4C, 0xC6, 0xB2, 0x07, 0x83, 0x35, 0x74, 0x0F, 0xA4, 0x66, 0x6D, 0x34, 0x91, 0x60, + 0xA9, 0x73, 0x29, 0xFC, 0x66, 0xD9, 0xC2, 0x70, 0x4B, 0x57, 0xC9, 0xB0, 0xBD, 0xF4, 0xA5, 0x35, + 0x59, 0x83, 0xE0, 0x0B, 0x6C, 0x62, 0xE0, 0x1E, 0x68, 0x64, 0xF2, 0x7B, 0x00, 0x77, 0x6B, 0xB6, + 0xA3, 0x3D, 0xD6, 0x8E, 0x6A, 0x35, 0x53, 0x55, 0xE9, 0xAE, 0x0B, 0x6D, 0x4E, 0x74, 0x23, 0x0B, + 0x4B, 0x10, 0xAA, 0x9A, 0x59, 0x0C, 0x38, 0x1B, 0x81, 0xAA, 0xBA, 0xC0, 0x11, 0xD6, 0x98, 0x66, + 0xA9, 0x23, 0xF1, 0x97, 0x1D, 0xC9, 0x13, 0xB5, 0x07, 0x95, 0xF5, 0x05, 0xD4, 0x31, 0xAB, 0x25, + 0x86, 0x30, 0xD3, 0x29, 0x13, 0xDE, 0x04, 0x03, 0x90, 0x07, 0x5A, 0xD5, 0x05, 0x14, 0xB5, 0x8E, + 0x1C, 0x4D, 0x44, 0xB8, 0x1C, 0x05, 0xF9, 0xF0, 0x6B, 0x9A, 0x0F, 0xBC, 0xB4, 0x18, 0xDD, 0x97, + 0x80, 0x50, 0xD2, 0xE6, 0xE0, 0x88, 0x8F, 0xF2, 0x21, 0xF4, 0xB2, 0x05, 0x9D, 0x02, 0x58, 0xFC, + 0xC6, 0x71, 0x3E, 0x8A, 0x27, 0xC5, 0x68, 0x42, 0xEF, 0x17, 0x78, 0x51, 0x01, 0xF5, 0xA9, 0xEE, + 0x28, 0x1B, 0xDB, 0x68, 0xCE, 0xF3, 0x41, 0x6B, 0x29, 0x7F, 0xF0, 0xFF, 0x28, 0x7F, 0xCC, 0xC7, + 0x85, 0x34, 0x71, 0x31, 0x1A, 0xB3, 0x42, 0x96, 0x61, 0x18, 0xFF, 0x90, 0x93, 0xA4, 0xD4, 0x13, + 0x97, 0x7A, 0x5A, 0xF1, 0xB3, 0xB6, 0x53, 0x98, 0x8E, 0x31, 0xAA, 0xF8, 0xE3, 0xC8, 0xF6, 0xF0, + 0xF7, 0x3C, 0xF2, 0x65, 0x6D, 0x69, 0x5A, 0xA1, 0x31, 0x82, 0x3A, 0x57, 0x37, 0xCB, 0x7E, 0x9A, + 0xFD, 0xB7, 0xAD, 0xE8, 0xD1, 0xF1, 0xE9, 0x71, 0xFF, 0xB8, 0x5C, 0x38, 0x23, 0xE7, 0x25, 0x93, + 0x8A, 0x2B, 0x5D, 0xFA, 0xB2, 0x22, 0x80, 0x02, 0x1B, 0x45, 0x01, 0x7B, 0xDD, 0xDC, 0x54, 0x7E, + 0xF1, 0xB6, 0x77, 0x71, 0x6E, 0xC7, 0x24, 0x01, 0x8F, 0x24, 0x15, 0xE6, 0xC2, 0x82, 0x44, 0xF9, + 0xE0, 0xD7, 0xC7, 0xA5, 0x72, 0x5D, 0x7E, 0x61, 0x70, 0xC4, 0xDC, 0x52, 0xA7, 0xA9, 0x7E, 0x78, + 0xE2, 0x62, 0x5D, 0x99, 0xBF, 0x04, 0x41, 0x72, 0x1A, 0x2D, 0x13, 0x55, 0x11, 0x67, 0x46, 0xE5, + 0x30, 0x2F, 0xEE, 0xB2, 0x75, 0x0D, 0xD3, 0xC8, 0xB4, 0xC4, 0x84, 0xA5, 0xE5, 0x46, 0xA5, 0x12, + 0x14, 0xFE, 0xA2, 0xB6, 0xE7, 0x8B, 0x91, 0x24, 0xB7, 0x5A, 0x73, 0xAB, 0x6F, 0x41, 0x2A, 0x3E, + 0x58, 0x04, 0x23, 0x66, 0x39, 0xDB, 0x16, 0x77, 0xA3, 0x43, 0xEE, 0x61, 0x5C, 0x7F, 0xBA, 0x35, + 0x78, 0xD2, 0x3C, 0x79, 0x61, 0x9E, 0xFC, 0xB1, 0x7B, 0x2E, 0x1C, 0x45, 0xF9, 0xDA, 0xE2, 0x98, + 0xF6, 0x10, 0x58, 0xBB, 0x6D, 0x2F, 0x7D, 0x18, 0x20, 0xD2, 0x83, 0xCB, 0x00, 0xF4, 0x63, 0x58, + 0xFF, 0x4A, 0xEE, 0x88, 0x7A, 0x09, 0xAA, 0xA2, 0xAD, 0x73, 0x54, 0xD8, 0xEE, 0xFD, 0x81, 0xA3, + 0xF2, 0xCE, 0x65, 0x18, 0x48, 0x97, 0xC3, 0x92, 0x37, 0x8B, 0x75, 0xC1, 0x61, 0x19, 0x31, 0x64, + 0x6C, 0x00, 0xE3, 0xCD, 0x5D, 0x49, 0x13, 0xD5, 0x1C, 0xB4, 0xF0, 0x1B, 0x08, 0x8A, 0x4F, 0x39, + 0xCE, 0x9A, 0x38, 0xAD, 0x62, 0x72, 0xC5, 0x23, 0xC8, 0x4A, 0x67, 0x89, 0xC0, 0x6E, 0x10, 0x0D, + 0x0D, 0x7C, 0x64, 0x9A, 0xA1, 0xB6, 0x1D, 0x3E, 0x37, 0xD7, 0xBC, 0xD9, 0x54, 0xFA, 0x4B, 0x62, + 0x79, 0xD5, 0xB0, 0x8B, 0x1C, 0x56, 0xCC, 0x75, 0x7D, 0x1F, 0xF4, 0xA3, 0x4E, 0x29, 0xAF, 0x48, + 0xA4, 0x53, 0xD1, 0x83, 0xC4, 0x86, 0xA2, 0x41, 0xBE, 0x91, 0x40, 0x44, 0x72, 0x4A, 0x33, 0x5D, + 0xC7, 0xCA, 0xD2, 0x0B, 0x28, 0x49, 0x7A, 0xB2, 0x73, 0x95, 0x49, 0x6B, 0x25, 0x06, 0xFE, 0xC8, + 0xD7, 0xF0, 0xC7, 0xA1, 0xD0, 0xA3, 0x83, 0x9B, 0x49, 0x2B, 0x83, 0xA4, 0x23, 0x64, 0x83, 0xA9, + 0x37, 0xE4, 0xBB, 0xA8, 0x2D, 0x2F, 0xCB, 0xB4, 0x16, 0x50, 0x70, 0x71, 0x83, 0xBB, 0x11, 0x30, + 0x52, 0x5A, 0xC4, 0x9E, 0x94, 0xA8, 0xC7, 0x8F, 0x10, 0x1F, 0x53, 0x4A, 0x20, 0x06, 0x20, 0xA6, + 0x40, 0xD0, 0xA7, 0x42, 0x8A, 0x54, 0xE6, 0x92, 0x53, 0x2A, 0x20, 0xCA, 0x48, 0xCD, 0xE2, 0xC1, + 0x85, 0x78, 0xD4, 0x46, 0xD6, 0x80, 0xFD, 0xDC, 0xBD, 0x73, 0x33, 0xDE, 0x90, 0x68, 0x09, 0x56, + 0x36, 0x3D, 0x9A, 0xA6, 0x52, 0x5C, 0x54, 0xC7, 0x19, 0xF8, 0xA8, 0xA1, 0x03, 0x5A, 0x23, 0x84, + 0x11, 0x1E, 0x84, 0x8A, 0x01, 0x40, 0x7F, 0x42, 0xC3, 0x1C, 0x22, 0x70, 0x08, 0x20, 0x82, 0xA0, + 0x7F, 0x49, 0x0D, 0xF7, 0x64, 0x05, 0xC9, 0xF8, 0xD8, 0x6D, 0x35, 0xF0, 0x9D, 0x66, 0x95, 0xEC, + 0x20, 0xA5, 0xBD, 0x68, 0x24, 0xFA, 0x64, 0x98, 0x1A, 0x50, 0x00, 0xAC, 0xD9, 0x01, 0xA0, 0x1E, + 0x24, 0x5E, 0x63, 0x2B, 0x3F, 0xEF, 0x04, 0x2A, 0xBB, 0x00, 0xAB, 0xBB, 0x8E, 0x87, 0x5F, 0x39, + 0x4F, 0x19, 0xA7, 0x39, 0x26, 0x00, 0x7B, 0x93, 0x68, 0x7A, 0x99, 0x30, 0x2E, 0xCE, 0x64, 0x1B, + 0x6A, 0x6C, 0xB4, 0xE4, 0xF5, 0xA9, 0x87, 0x15, 0x79, 0x3F, 0xC5, 0x8B, 0xCB, 0x0C, 0xF3, 0xBA, + 0x53, 0x79, 0x77, 0xB1, 0x86, 0x70, 0x21, 0x50, 0x66, 0x38, 0xB3, 0x29, 0x74, 0xB0, 0xFA, 0xA1, + 0x48, 0x82, 0x7A, 0x4F, 0xB7, 0x42, 0xE2, 0xC1, 0x44, 0xED, 0x81, 0xF9, 0xDC, 0xC2, 0xD8, 0xE1, + 0x94, 0x83, 0x5A, 0x0A, 0xB5, 0x02, 0x45, 0xC6, 0x95, 0xCD, 0x98, 0x35, 0x1D, 0x6A, 0x58, 0x88, + 0x61, 0xE0, 0xAF, 0xFE, 0x05, 0x0F, 0x1E, 0x1C, 0xC8, 0x55, 0x3F, 0xE1, 0x23, 0xE3, 0x7E, 0xF4, + 0x23, 0x3E, 0x3E, 0xAF, 0xF0, 0xF1, 0x79, 0x1D, 0x1F, 0xB4, 0xAA, 0x3C, 0x98, 0x0C, 0x80, 0xEC, + 0x19, 0xE1, 0x64, 0x4C, 0x13, 0x58, 0xC0, 0x43, 0x50, 0x25, 0x7F, 0x8B, 0xB3, 0x84, 0xFE, 0x98, + 0xB3, 0xDE, 0x84, 0x8D, 0xC4, 0x23, 0xFE, 0x8A, 0xD5, 0xFF, 0x82, 0x4B, 0x3C, 0x70, 0x3D, 0x97, + 0x79, 0x6D, 0x5A, 0x49, 0x28, 0x3F, 0x7E, 0x2B, 0x91, 0x7E, 0xE4, 0x42, 0x78, 0xA9, 0x38, 0xC8, + 0xDF, 0xB7, 0xF4, 0x00, 0xBC, 0x11, 0xF8, 0x29, 0x35, 0x75, 0xBC, 0x0B, 0xA5, 0xFC, 0x29, 0x30, + 0x64, 0xA8, 0xC0, 0x47, 0xDD, 0xD9, 0xDC, 0x12, 0xAE, 0x01, 0x8A, 0xF1, 0xA3, 0x29, 0xB0, 0xEA, + 0xC9, 0x02, 0xD7, 0x9E, 0x40, 0x26, 0x04, 0x91, 0xE0, 0x48, 0xC8, 0xA7, 0x8D, 0x2F, 0x07, 0x9B, + 0x37, 0x35, 0xC8, 0x43, 0x2E, 0xFC, 0x98, 0x2E, 0x0C, 0x36, 0x6F, 0xFE, 0x6D, 0x36, 0xC6, 0xCC, + 0x5A, 0x76, 0xA4, 0x96, 0x4C, 0xF6, 0xF4, 0x0B, 0xBF, 0x71, 0x09, 0x48, 0x5D, 0x49, 0x78, 0x45, + 0x34, 0x03, 0x6B, 0x43, 0x61, 0xE1, 0x07, 0xFF, 0x47, 0x09, 0xF8, 0x91, 0x9E, 0x07, 0xCE, 0xBD, + 0xE6, 0x3D, 0x5E, 0x2F, 0x3E, 0x85, 0xE9, 0x56, 0xE9, 0xC1, 0x4A, 0xC7, 0xEF, 0x53, 0x3A, 0x76, + 0x59, 0xA2, 0x14, 0x4A, 0x14, 0x59, 0x88, 0x1A, 0x6A, 0x50, 0x0E, 0x51, 0x98, 0x89, 0x17, 0xCD, + 0x81, 0x02, 0x9B, 0x73, 0x34, 0x5B, 0x3A, 0x02, 0x0F, 0xF4, 0xF5, 0x45, 0xEE, 0xFC, 0x74, 0x76, + 0x7A, 0x22, 0x44, 0x7C, 0xA5, 0x62, 0x22, 0xD0, 0xAA, 0x2E, 0x2C, 0x2F, 0xCF, 0x9C, 0x89, 0xE4, + 0xA1, 0x28, 0x75, 0x30, 0x31, 0x28, 0x87, 0xFE, 0x74, 0x31, 0xFC, 0x0A, 0x71, 0xD6, 0xD0, 0xCF, + 0x52, 0x48, 0x58, 0x5B, 0x36, 0xA2, 0xF7, 0xFB, 0x97, 0xF6, 0xAE, 0xDD, 0x84, 0xBA, 0x00, 0xB4, + 0x0A, 0x69, 0x19, 0xEE, 0x7D, 0xFE, 0xB7, 0x90, 0xB7, 0xFF, 0x1E, 0x32, 0x83, 0xA8, 0x95, 0x42, + 0x58, 0x2A, 0xF0, 0xAB, 0xB8, 0x93, 0x24, 0x9A, 0x4A, 0xB4, 0xE3, 0x24, 0xC1, 0x4B, 0xE9, 0x43, + 0x85, 0xA2, 0x0D, 0x61, 0x31, 0xA5, 0x89, 0xE6, 0x47, 0x34, 0xD5, 0x78, 0x24, 0xB4, 0x34, 0x8B, + 0x63, 0x68, 0x5C, 0x56, 0xF4, 0x61, 0xEB, 0xC5, 0xEB, 0xCB, 0xFB, 0x8C, 0x66, 0xD4, 0xCF, 0x97, + 0x69, 0x52, 0xD1, 0x0B, 0x56, 0x50, 0xDF, 0x10, 0xEE, 0x7E, 0xB9, 0xC9, 0xEB, 0xA9, 0x8C, 0x73, + 0x8C, 0xA2, 0x1B, 0x2D, 0x35, 0x07, 0xE9, 0x26, 0x40, 0xD5, 0xE5, 0x59, 0x10, 0xCC, 0xDB, 0x2B, + 0xB4, 0xA0, 0xF1, 0x8A, 0x44, 0x24, 0x9F, 0xCB, 0x67, 0x7F, 0xE4, 0xC9, 0xA9, 0xE2, 0x82, 0x50, + 0xF2, 0x54, 0xA9, 0x36, 0xAD, 0x0D, 0x63, 0x83, 0x6A, 0x8C, 0xA7, 0x82, 0x70, 0x0F, 0xAF, 0x51, + 0xE9, 0xC2, 0x2C, 0x6A, 0x29, 0xDC, 0xDE, 0x46, 0x5F, 0xCB, 0x6D, 0xE9, 0x89, 0x7C, 0x2A, 0x25, + 0xE3, 0xAE, 0xAE, 0x63, 0x55, 0x45, 0xB1, 0x3E, 0x25, 0x61, 0x5A, 0x26, 0x5B, 0x54, 0x06, 0x26, + 0x77, 0x0B, 0x70, 0x9B, 0x06, 0x29, 0x1C, 0xBD, 0x7E, 0x7F, 0xCE, 0x46, 0xD1, 0xCE, 0x11, 0x80, + 0x69, 0xC5, 0x3E, 0x93, 0xD7, 0xE0, 0x24, 0xCC, 0x73, 0x07, 0x32, 0xE9, 0x4A, 0x03, 0x0E, 0xA9, + 0x98, 0x44, 0xFE, 0x81, 0x7E, 0xA0, 0x3B, 0x3A, 0xFC, 0xBB, 0x09, 0x35, 0x47, 0xCD, 0xA5, 0xD0, + 0xA4, 0xFA, 0x74, 0x70, 0xF5, 0x06, 0xC2, 0x53, 0x0C, 0xA5, 0x01, 0x17, 0x50, 0x34, 0xD7, 0x74, + 0x7C, 0x7A, 0x7D, 0x0C, 0x29, 0xC8, 0x7F, 0x21, 0x37, 0x66, 0xBB, 0xAA, 0x6C, 0xB8, 0xF3, 0xEA, + 0x75, 0x56, 0x2E, 0x03, 0x7A, 0x61, 0x8C, 0x58, 0x0F, 0x29, 0x7E, 0xFB, 0x7B, 0xF4, 0x9E, 0x8D, + 0x15, 0xD2, 0x6A, 0x5D, 0x6F, 0xCE, 0x76, 0x90, 0x67, 0x89, 0xD5, 0x43, 0x2C, 0x70, 0x97, 0x1F, + 0x29, 0x59, 0x95, 0x35, 0xDC, 0xF6, 0x48, 0x10, 0xE0, 0xC7, 0x5A, 0x03, 0x1B, 0x6A, 0x22, 0xB2, + 0xD4, 0x42, 0x22, 0x29, 0x08, 0x90, 0xD2, 0x3E, 0x84, 0x39, 0xD3, 0x92, 0x65, 0x86, 0xB2, 0xA1, + 0xBC, 0xFF, 0xC5, 0x9A, 0xA3, 0x64, 0x46, 0xE8, 0xCE, 0xF9, 0x6C, 0x73, 0x53, 0xD8, 0x85, 0x99, + 0x18, 0x05, 0x52, 0x8A, 0x01, 0x1C, 0x9A, 0x7D, 0x68, 0x2D, 0x8C, 0xB2, 0x90, 0x58, 0xAB, 0x3D, + 0xD2, 0xB6, 0x51, 0x55, 0x03, 0x54, 0x7C, 0x46, 0x01, 0x03, 0xCE, 0xB2, 0x24, 0x80, 0xA8, 0x8B, + 0x39, 0xBA, 0xB2, 0x2D, 0xC5, 0xBA, 0xD0, 0x84, 0x0E, 0xEC, 0x67, 0xC8, 0x12, 0x95, 0x97, 0xAD, + 0xA2, 0x27, 0x12, 0xC5, 0x77, 0x95, 0x9E, 0xC8, 0x6F, 0xE5, 0x84, 0xAA, 0xC8, 0x77, 0x88, 0x2F, + 0x13, 0x5C, 0xD4, 0xD1, 0x13, 0xA0, 0x24, 0x83, 0x52, 0x34, 0x60, 0x2A, 0x2C, 0x37, 0xEE, 0xEB, + 0xD3, 0xE9, 0xB4, 0x8E, 0xDF, 0x6A, 0xEB, 0x70, 0x82, 0xB2, 0x02, 0x5F, 0x5F, 0xC7, 0x21, 0x47, + 0x15, 0x58, 0xF8, 0x6E, 0xE1, 0xAC, 0xBA, 0xE8, 0x42, 0x7F, 0x2B, 0xDE, 0xD4, 0xAA, 0xD2, 0x59, + 0xE1, 0x73, 0x79, 0xDB, 0x7B, 0x3B, 0x2B, 0x20, 0x32, 0xC4, 0xAF, 0xB2, 0x90, 0x69, 0x20, 0x0D, + 0x3B, 0xE5, 0x46, 0x56, 0x25, 0x85, 0x65, 0x5C, 0xB0, 0xE3, 0x2C, 0x9D, 0x18, 0x33, 0x60, 0xDD, + 0x11, 0x96, 0xD2, 0x95, 0x43, 0x2D, 0x65, 0xB7, 0x0E, 0xB7, 0x0A, 0xFB, 0x70, 0x30, 0x83, 0x94, + 0x79, 0xFB, 0xF3, 0x4F, 0x39, 0x5B, 0xDE, 0xF6, 0x92, 0x62, 0x71, 0xE1, 0xF3, 0xFC, 0xA9, 0x35, + 0xAF, 0x69, 0xA5, 0xD1, 0xAF, 0xC4, 0x97, 0xBD, 0x46, 0xFE, 0x19, 0x3B, 0xFF, 0x9C, 0xAD, 0x81, + 0xB1, 0x43, 0x23, 0x2A, 0xDC, 0x4C, 0x8C, 0xEA, 0x2F, 0x34, 0xE6, 0x63, 0x79, 0x29, 0xBF, 0x2D, + 0xA0, 0x54, 0xA9, 0xD3, 0x68, 0x78, 0x3E, 0xFF, 0x9A, 0x42, 0x19, 0x1D, 0x65, 0xFE, 0x28, 0x20, + 0x09, 0xC5, 0x82, 0xA3, 0x41, 0xBE, 0x92, 0xFB, 0x46, 0xC0, 0x86, 0x69, 0x03, 0x93, 0x6D, 0xCB, + 0xDE, 0xB2, 0x77, 0x71, 0x64, 0x7F, 0x4D, 0xF7, 0x57, 0x4F, 0xD8, 0x5F, 0x34, 0x69, 0x58, 0x0B, + 0xE7, 0xB5, 0xAB, 0x8A, 0x4D, 0x6A, 0x83, 0xFB, 0xC4, 0xA7, 0x70, 0x3D, 0x6F, 0xB3, 0xCC, 0xB6, + 0x1A, 0xE4, 0x5F, 0x60, 0xD4, 0x31, 0xBA, 0x95, 0x2F, 0x92, 0xF4, 0x81, 0x7B, 0x18, 0x5B, 0x17, + 0x54, 0x26, 0x70, 0x49, 0xD5, 0x87, 0x34, 0xB9, 0xD3, 0x9C, 0x2F, 0x39, 0xC3, 0xB7, 0x3C, 0xA8, + 0x03, 0xE4, 0x37, 0x9C, 0x72, 0x39, 0xB0, 0xBF, 0x07, 0x5D, 0x33, 0x2A, 0x41, 0x79, 0xB1, 0x26, + 0x9B, 0xE6, 0x7C, 0x02, 0x82, 0x01, 0x70, 0xB1, 0xA3, 0x48, 0xCD, 0x2B, 0xCB, 0x98, 0x9B, 0x57, + 0x96, 0x54, 0xE2, 0x5F, 0x59, 0xCC, 0xDB, 0x9F, 0xFC, 0xDB, 0x4C, 0xF9, 0x7F, 0x5B, 0x28, 0x36, + 0x32, 0xF9, 0xE1, 0x09, 0xF7, 0x56, 0x3F, 0x45, 0xAD, 0x47, 0x51, 0xBB, 0xF7, 0xFF, 0x17, 0x53, + 0xE8, 0x9D, 0x36, 0x92, 0x29, 0x00, 0x00 +}; + +#define SPIFFS_MAXLENGTH_FILEPATH 32 +const char *excludeListFile = "/.exclude.files"; + +typedef struct ExcludeListS { + char *item; + ExcludeListS *next; +} ExcludeList; + +static ExcludeList *excludes = NULL; + +static bool matchWild(const char *pattern, const char *testee) { + const char *nxPat = NULL, *nxTst = NULL; + + while (*testee) { + if (( *pattern == '?' ) || (*pattern == *testee)){ + pattern++;testee++; + continue; + } + if (*pattern=='*'){ + nxPat=pattern++; nxTst=testee; + continue; + } + if (nxPat){ + pattern = nxPat+1; testee=++nxTst; + continue; + } + return false; + } + while (*pattern=='*'){pattern++;} + return (*pattern == 0); +} + +static bool addExclude(const char *item){ + size_t len = strlen(item); + if(!len){ + return false; + } + ExcludeList *e = (ExcludeList *)malloc(sizeof(ExcludeList)); + if(!e){ + return false; + } + e->item = (char *)malloc(len+1); + if(!e->item){ + free(e); + return false; + } + memcpy(e->item, item, len+1); + e->next = excludes; + excludes = e; + return true; +} + +static void loadExcludeList(fs::FS &_fs, const char *filename){ + static char linebuf[SPIFFS_MAXLENGTH_FILEPATH]; + fs::File excludeFile=_fs.open(filename, "r"); + if(!excludeFile){ + //addExclude("/*.js.gz"); + return; + } +#ifdef ESP32 + if(excludeFile.isDirectory()){ + excludeFile.close(); + return; + } +#endif + if (excludeFile.size() > 0){ + uint8_t idx; + bool isOverflowed = false; + while (excludeFile.available()){ + linebuf[0] = '\0'; + idx = 0; + int lastChar; + do { + lastChar = excludeFile.read(); + if(lastChar != '\r'){ + linebuf[idx++] = (char) lastChar; + } + } while ((lastChar >= 0) && (lastChar != '\n') && (idx < SPIFFS_MAXLENGTH_FILEPATH)); + + if(isOverflowed){ + isOverflowed = (lastChar != '\n'); + continue; + } + isOverflowed = (idx >= SPIFFS_MAXLENGTH_FILEPATH); + linebuf[idx-1] = '\0'; + if(!addExclude(linebuf)){ + excludeFile.close(); + return; + } + } + } + excludeFile.close(); +} + +static bool isExcluded(fs::FS &_fs, const char *filename) { + if(excludes == NULL){ + loadExcludeList(_fs, excludeListFile); + } + ExcludeList *e = excludes; + while(e){ + if (matchWild(e->item, filename)){ + return true; + } + e = e->next; + } + return false; +} + +// WEB HANDLER IMPLEMENTATION + +#ifdef ESP32 +SPIFFSEditor::SPIFFSEditor(const fs::FS& fs, const String& username, const String& password) +#else +SPIFFSEditor::SPIFFSEditor(const String& username, const String& password, const fs::FS& fs) +#endif +:_fs(fs) +,_username(username) +,_password(password) +,_authenticated(false) +,_startTime(0) +{} + +bool SPIFFSEditor::canHandle(AsyncWebServerRequest *request){ + if(request->url().equalsIgnoreCase("/edit")){ + if(request->method() == HTTP_GET){ + if(request->hasParam("list")) + return true; + if(request->hasParam("edit")){ + request->_tempFile = _fs.open(request->arg("edit"), "r"); + if(!request->_tempFile){ + return false; + } +#ifdef ESP32 + if(request->_tempFile.isDirectory()){ + request->_tempFile.close(); + return false; + } +#endif + } + if(request->hasParam("download")){ + request->_tempFile = _fs.open(request->arg("download"), "r"); + if(!request->_tempFile){ + return false; + } +#ifdef ESP32 + if(request->_tempFile.isDirectory()){ + request->_tempFile.close(); + return false; + } +#endif + } + request->addInterestingHeader("If-Modified-Since"); + return true; + } + else if(request->method() == HTTP_POST) + return true; + else if(request->method() == HTTP_DELETE) + return true; + else if(request->method() == HTTP_PUT) + return true; + + } + return false; +} + + +void SPIFFSEditor::handleRequest(AsyncWebServerRequest *request){ + if(_username.length() && _password.length() && !request->authenticate(_username.c_str(), _password.c_str())) + return request->requestAuthentication(); + + if(request->method() == HTTP_GET){ + if(request->hasParam("list")){ + String path = request->getParam("list")->value(); +#ifdef ESP32 + File dir = _fs.open(path); +#else + Dir dir = _fs.openDir(path); +#endif + path = String(); + String output = "["; +#ifdef ESP32 + File entry = dir.openNextFile(); + while(entry){ +#else + while(dir.next()){ + fs::File entry = dir.openFile("r"); +#endif + if (isExcluded(_fs, entry.name())) { +#ifdef ESP32 + entry = dir.openNextFile(); +#endif + continue; + } + if (output != "[") output += ','; + output += "{\"type\":\""; + output += "file"; + output += "\",\"name\":\""; + output += String(entry.name()); + output += "\",\"size\":"; + output += String(entry.size()); + output += "}"; +#ifdef ESP32 + entry = dir.openNextFile(); +#else + entry.close(); +#endif + } +#ifdef ESP32 + dir.close(); +#endif + output += "]"; + request->send(200, "application/json", output); + output = String(); + } + else if(request->hasParam("edit") || request->hasParam("download")){ + request->send(request->_tempFile, request->_tempFile.name(), String(), request->hasParam("download")); + } + else { + const char * buildTime = __DATE__ " " __TIME__ " GMT"; + if (request->header("If-Modified-Since").equals(buildTime)) { + request->send(304); + } else { + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", edit_htm_gz, edit_htm_gz_len); + response->addHeader("Content-Encoding", "gzip"); + response->addHeader("Last-Modified", buildTime); + request->send(response); + } + } + } else if(request->method() == HTTP_DELETE){ + if(request->hasParam("path", true)){ + _fs.remove(request->getParam("path", true)->value()); + request->send(200, "", "DELETE: "+request->getParam("path", true)->value()); + } else + request->send(404); + } else if(request->method() == HTTP_POST){ + if(request->hasParam("data", true, true) && _fs.exists(request->getParam("data", true, true)->value())) + request->send(200, "", "UPLOADED: "+request->getParam("data", true, true)->value()); + else + request->send(500); + } else if(request->method() == HTTP_PUT){ + if(request->hasParam("path", true)){ + String filename = request->getParam("path", true)->value(); + if(_fs.exists(filename)){ + request->send(200); + } else { + fs::File f = _fs.open(filename, "w"); + if(f){ + f.write((uint8_t)0x00); + f.close(); + request->send(200, "", "CREATE: "+filename); + } else { + request->send(500); + } + } + } else + request->send(400); + } +} + +void SPIFFSEditor::handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final){ + if(!index){ + if(!_username.length() || request->authenticate(_username.c_str(),_password.c_str())){ + _authenticated = true; + request->_tempFile = _fs.open(filename, "w"); + _startTime = millis(); + } + } + if(_authenticated && request->_tempFile){ + if(len){ + request->_tempFile.write(data,len); + } + if(final){ + request->_tempFile.close(); + } + } +} diff --git a/yoRadio/src/AsyncWebServer/SPIFFSEditor.h b/yoRadio/src/AsyncWebServer/SPIFFSEditor.h new file mode 100644 index 0000000..6b5896e --- /dev/null +++ b/yoRadio/src/AsyncWebServer/SPIFFSEditor.h @@ -0,0 +1,24 @@ +#ifndef SPIFFSEditor_H_ +#define SPIFFSEditor_H_ +#include "ESPAsyncWebServer.h" + +class SPIFFSEditor: public AsyncWebHandler { + private: + fs::FS _fs; + String _username; + String _password; + bool _authenticated; + uint32_t _startTime; + public: +#ifdef ESP32 + SPIFFSEditor(const fs::FS& fs, const String& username=String(), const String& password=String()); +#else + SPIFFSEditor(const String& username=String(), const String& password=String(), const fs::FS& fs=SPIFFS); +#endif + virtual bool canHandle(AsyncWebServerRequest *request) override final; + virtual void handleRequest(AsyncWebServerRequest *request) override final; + virtual void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) override final; + virtual bool isRequestHandlerTrivial() override final {return false;} +}; + +#endif diff --git a/yoRadio/src/AsyncWebServer/StringArray.h b/yoRadio/src/AsyncWebServer/StringArray.h new file mode 100644 index 0000000..4c0aa70 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/StringArray.h @@ -0,0 +1,193 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef STRINGARRAY_H_ +#define STRINGARRAY_H_ + +#include "stddef.h" +#include "WString.h" + +template +class LinkedListNode { + T _value; + public: + LinkedListNode* next; + LinkedListNode(const T val): _value(val), next(nullptr) {} + ~LinkedListNode(){} + const T& value() const { return _value; }; + T& value(){ return _value; } +}; + +template class Item = LinkedListNode> +class LinkedList { + public: + typedef Item ItemType; + typedef std::function OnRemove; + typedef std::function Predicate; + private: + ItemType* _root; + OnRemove _onRemove; + + class Iterator { + ItemType* _node; + public: + Iterator(ItemType* current = nullptr) : _node(current) {} + Iterator(const Iterator& i) : _node(i._node) {} + Iterator& operator ++() { _node = _node->next; return *this; } + bool operator != (const Iterator& i) const { return _node != i._node; } + const T& operator * () const { return _node->value(); } + const T* operator -> () const { return &_node->value(); } + }; + + public: + typedef const Iterator ConstIterator; + ConstIterator begin() const { return ConstIterator(_root); } + ConstIterator end() const { return ConstIterator(nullptr); } + + LinkedList(OnRemove onRemove) : _root(nullptr), _onRemove(onRemove) {} + ~LinkedList(){} + void add(const T& t){ + auto it = new ItemType(t); + if(!_root){ + _root = it; + } else { + auto i = _root; + while(i->next) i = i->next; + i->next = it; + } + } + T& front() const { + return _root->value(); + } + + bool isEmpty() const { + return _root == nullptr; + } + size_t length() const { + size_t i = 0; + auto it = _root; + while(it){ + i++; + it = it->next; + } + return i; + } + size_t count_if(Predicate predicate) const { + size_t i = 0; + auto it = _root; + while(it){ + if (!predicate){ + i++; + } + else if (predicate(it->value())) { + i++; + } + it = it->next; + } + return i; + } + const T* nth(size_t N) const { + size_t i = 0; + auto it = _root; + while(it){ + if(i++ == N) + return &(it->value()); + it = it->next; + } + return nullptr; + } + bool remove(const T& t){ + auto it = _root; + auto pit = _root; + while(it){ + if(it->value() == t){ + if(it == _root){ + _root = _root->next; + } else { + pit->next = it->next; + } + + if (_onRemove) { + _onRemove(it->value()); + } + + delete it; + return true; + } + pit = it; + it = it->next; + } + return false; + } + bool remove_first(Predicate predicate){ + auto it = _root; + auto pit = _root; + while(it){ + if(predicate(it->value())){ + if(it == _root){ + _root = _root->next; + } else { + pit->next = it->next; + } + if (_onRemove) { + _onRemove(it->value()); + } + delete it; + return true; + } + pit = it; + it = it->next; + } + return false; + } + + void free(){ + while(_root != nullptr){ + auto it = _root; + _root = _root->next; + if (_onRemove) { + _onRemove(it->value()); + } + delete it; + } + _root = nullptr; + } +}; + + +class StringArray : public LinkedList { +public: + + StringArray() : LinkedList(nullptr) {} + + bool containsIgnoreCase(const String& str){ + for (const auto& s : *this) { + if (str.equalsIgnoreCase(s)) { + return true; + } + } + return false; + } +}; + + + + +#endif /* STRINGARRAY_H_ */ diff --git a/yoRadio/src/AsyncWebServer/WebAuthentication.cpp b/yoRadio/src/AsyncWebServer/WebAuthentication.cpp new file mode 100644 index 0000000..45246a1 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/WebAuthentication.cpp @@ -0,0 +1,235 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "WebAuthentication.h" +#include +#ifdef ESP32 +#include "mbedtls/md5.h" +#else +#include "md5.h" +#endif + + +// Basic Auth hash = base64("username:password") + +bool checkBasicAuthentication(const char * hash, const char * username, const char * password){ + if(username == NULL || password == NULL || hash == NULL) + return false; + + size_t toencodeLen = strlen(username)+strlen(password)+1; + size_t encodedLen = base64_encode_expected_len(toencodeLen); + if(strlen(hash) != encodedLen) + return false; + + char *toencode = new char[toencodeLen+1]; + if(toencode == NULL){ + return false; + } + char *encoded = new char[base64_encode_expected_len(toencodeLen)+1]; + if(encoded == NULL){ + delete[] toencode; + return false; + } + sprintf(toencode, "%s:%s", username, password); + if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && memcmp(hash, encoded, encodedLen) == 0){ + delete[] toencode; + delete[] encoded; + return true; + } + delete[] toencode; + delete[] encoded; + return false; +} + +static bool getMD5(uint8_t * data, uint16_t len, char * output){//33 bytes or more +#ifdef ESP32 + mbedtls_md5_context _ctx; +#else + md5_context_t _ctx; +#endif + uint8_t i; + uint8_t * _buf = (uint8_t*)malloc(16); + if(_buf == NULL) + return false; + memset(_buf, 0x00, 16); +#ifdef ESP32 + mbedtls_md5_init(&_ctx); + mbedtls_md5_starts_ret(&_ctx); + mbedtls_md5_update_ret(&_ctx, data, len); + mbedtls_md5_finish_ret(&_ctx, _buf); +#else + MD5Init(&_ctx); + MD5Update(&_ctx, data, len); + MD5Final(_buf, &_ctx); +#endif + for(i = 0; i < 16; i++) { + sprintf(output + (i * 2), "%02x", _buf[i]); + } + free(_buf); + return true; +} + +static String genRandomMD5(){ +#ifdef ESP8266 + uint32_t r = RANDOM_REG32; +#else + uint32_t r = rand(); +#endif + char * out = (char*)malloc(33); + if(out == NULL || !getMD5((uint8_t*)(&r), 4, out)) + return ""; + String res = String(out); + free(out); + return res; +} + +static String stringMD5(const String& in){ + char * out = (char*)malloc(33); + if(out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) + return ""; + String res = String(out); + free(out); + return res; +} + +String generateDigestHash(const char * username, const char * password, const char * realm){ + if(username == NULL || password == NULL || realm == NULL){ + return ""; + } + char * out = (char*)malloc(33); + String res = String(username); + res.concat(":"); + res.concat(realm); + res.concat(":"); + String in = res; + in.concat(password); + if(out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) + return ""; + res.concat(out); + free(out); + return res; +} + +String requestDigestAuthentication(const char * realm){ + String header = "realm=\""; + if(realm == NULL) + header.concat("asyncesp"); + else + header.concat(realm); + header.concat( "\", qop=\"auth\", nonce=\""); + header.concat(genRandomMD5()); + header.concat("\", opaque=\""); + header.concat(genRandomMD5()); + header.concat("\""); + return header; +} + +bool checkDigestAuthentication(const char * header, const char * method, const char * username, const char * password, const char * realm, bool passwordIsHash, const char * nonce, const char * opaque, const char * uri){ + if(username == NULL || password == NULL || header == NULL || method == NULL){ + //os_printf("AUTH FAIL: missing requred fields\n"); + return false; + } + + String myHeader = String(header); + int nextBreak = myHeader.indexOf(","); + if(nextBreak < 0){ + //os_printf("AUTH FAIL: no variables\n"); + return false; + } + + String myUsername = String(); + String myRealm = String(); + String myNonce = String(); + String myUri = String(); + String myResponse = String(); + String myQop = String(); + String myNc = String(); + String myCnonce = String(); + + myHeader += ", "; + do { + String avLine = myHeader.substring(0, nextBreak); + avLine.trim(); + myHeader = myHeader.substring(nextBreak+1); + nextBreak = myHeader.indexOf(","); + + int eqSign = avLine.indexOf("="); + if(eqSign < 0){ + //os_printf("AUTH FAIL: no = sign\n"); + return false; + } + String varName = avLine.substring(0, eqSign); + avLine = avLine.substring(eqSign + 1); + if(avLine.startsWith("\"")){ + avLine = avLine.substring(1, avLine.length() - 1); + } + + if(varName.equals("username")){ + if(!avLine.equals(username)){ + //os_printf("AUTH FAIL: username\n"); + return false; + } + myUsername = avLine; + } else if(varName.equals("realm")){ + if(realm != NULL && !avLine.equals(realm)){ + //os_printf("AUTH FAIL: realm\n"); + return false; + } + myRealm = avLine; + } else if(varName.equals("nonce")){ + if(nonce != NULL && !avLine.equals(nonce)){ + //os_printf("AUTH FAIL: nonce\n"); + return false; + } + myNonce = avLine; + } else if(varName.equals("opaque")){ + if(opaque != NULL && !avLine.equals(opaque)){ + //os_printf("AUTH FAIL: opaque\n"); + return false; + } + } else if(varName.equals("uri")){ + if(uri != NULL && !avLine.equals(uri)){ + //os_printf("AUTH FAIL: uri\n"); + return false; + } + myUri = avLine; + } else if(varName.equals("response")){ + myResponse = avLine; + } else if(varName.equals("qop")){ + myQop = avLine; + } else if(varName.equals("nc")){ + myNc = avLine; + } else if(varName.equals("cnonce")){ + myCnonce = avLine; + } + } while(nextBreak > 0); + + String ha1 = (passwordIsHash) ? String(password) : stringMD5(myUsername + ":" + myRealm + ":" + String(password)); + String ha2 = String(method) + ":" + myUri; + String response = ha1 + ":" + myNonce + ":" + myNc + ":" + myCnonce + ":" + myQop + ":" + stringMD5(ha2); + + if(myResponse.equals(stringMD5(response))){ + //os_printf("AUTH SUCCESS\n"); + return true; + } + + //os_printf("AUTH FAIL: password\n"); + return false; +} diff --git a/yoRadio/src/AsyncWebServer/WebAuthentication.h b/yoRadio/src/AsyncWebServer/WebAuthentication.h new file mode 100644 index 0000000..ff68265 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/WebAuthentication.h @@ -0,0 +1,34 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef WEB_AUTHENTICATION_H_ +#define WEB_AUTHENTICATION_H_ + +#include "Arduino.h" + +bool checkBasicAuthentication(const char * header, const char * username, const char * password); +String requestDigestAuthentication(const char * realm); +bool checkDigestAuthentication(const char * header, const char * method, const char * username, const char * password, const char * realm, bool passwordIsHash, const char * nonce, const char * opaque, const char * uri); + +//for storing hashed versions on the device that can be authenticated against +String generateDigestHash(const char * username, const char * password, const char * realm); + +#endif diff --git a/yoRadio/src/AsyncWebServer/WebHandlerImpl.h b/yoRadio/src/AsyncWebServer/WebHandlerImpl.h new file mode 100644 index 0000000..9b7ba1b --- /dev/null +++ b/yoRadio/src/AsyncWebServer/WebHandlerImpl.h @@ -0,0 +1,151 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef ASYNCWEBSERVERHANDLERIMPL_H_ +#define ASYNCWEBSERVERHANDLERIMPL_H_ + +#include +#ifdef ASYNCWEBSERVER_REGEX +#include +#endif + +#include "stddef.h" +#include + +class AsyncStaticWebHandler: public AsyncWebHandler { + using File = fs::File; + using FS = fs::FS; + private: + bool _getFile(AsyncWebServerRequest *request); + bool _fileExists(AsyncWebServerRequest *request, const String& path); + uint8_t _countBits(const uint8_t value) const; + protected: + FS _fs; + String _uri; + String _path; + String _default_file; + String _cache_control; + String _last_modified; + AwsTemplateProcessor _callback; + bool _isDir; + bool _gzipFirst; + uint8_t _gzipStats; + public: + AsyncStaticWebHandler(const char* uri, FS& fs, const char* path, const char* cache_control); + virtual bool canHandle(AsyncWebServerRequest *request) override final; + virtual void handleRequest(AsyncWebServerRequest *request) override final; + AsyncStaticWebHandler& setIsDir(bool isDir); + AsyncStaticWebHandler& setDefaultFile(const char* filename); + AsyncStaticWebHandler& setCacheControl(const char* cache_control); + AsyncStaticWebHandler& setLastModified(const char* last_modified); + AsyncStaticWebHandler& setLastModified(struct tm* last_modified); + #ifdef ESP8266 + AsyncStaticWebHandler& setLastModified(time_t last_modified); + AsyncStaticWebHandler& setLastModified(); //sets to current time. Make sure sntp is runing and time is updated + #endif + AsyncStaticWebHandler& setTemplateProcessor(AwsTemplateProcessor newCallback) {_callback = newCallback; return *this;} +}; + +class AsyncCallbackWebHandler: public AsyncWebHandler { + private: + protected: + String _uri; + WebRequestMethodComposite _method; + ArRequestHandlerFunction _onRequest; + ArUploadHandlerFunction _onUpload; + ArBodyHandlerFunction _onBody; + bool _isRegex; + public: + AsyncCallbackWebHandler() : _uri(), _method(HTTP_ANY), _onRequest(NULL), _onUpload(NULL), _onBody(NULL), _isRegex(false) {} + void setUri(const String& uri){ + _uri = uri; + _isRegex = uri.startsWith("^") && uri.endsWith("$"); + } + void setMethod(WebRequestMethodComposite method){ _method = method; } + void onRequest(ArRequestHandlerFunction fn){ _onRequest = fn; } + void onUpload(ArUploadHandlerFunction fn){ _onUpload = fn; } + void onBody(ArBodyHandlerFunction fn){ _onBody = fn; } + + virtual bool canHandle(AsyncWebServerRequest *request) override final{ + + if(!_onRequest) + return false; + + if(!(_method & request->method())) + return false; + +#ifdef ASYNCWEBSERVER_REGEX + if (_isRegex) { + std::regex pattern(_uri.c_str()); + std::smatch matches; + std::string s(request->url().c_str()); + if(std::regex_search(s, matches, pattern)) { + for (size_t i = 1; i < matches.size(); ++i) { // start from 1 + request->_addPathParam(matches[i].str().c_str()); + } + } else { + return false; + } + } else +#endif + if (_uri.length() && _uri.startsWith("/*.")) { + String uriTemplate = String (_uri); + uriTemplate = uriTemplate.substring(uriTemplate.lastIndexOf(".")); + if (!request->url().endsWith(uriTemplate)) + return false; + } + else + if (_uri.length() && _uri.endsWith("*")) { + String uriTemplate = String(_uri); + uriTemplate = uriTemplate.substring(0, uriTemplate.length() - 1); + if (!request->url().startsWith(uriTemplate)) + return false; + } + else if(_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri+"/"))) + return false; + + request->addInterestingHeader("ANY"); + return true; + } + + virtual void handleRequest(AsyncWebServerRequest *request) override final { + if((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) + return request->requestAuthentication(); + if(_onRequest) + _onRequest(request); + else + request->send(500); + } + virtual void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) override final { + if((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) + return request->requestAuthentication(); + if(_onUpload) + _onUpload(request, filename, index, data, len, final); + } + virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final { + if((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) + return request->requestAuthentication(); + if(_onBody) + _onBody(request, data, len, index, total); + } + virtual bool isRequestHandlerTrivial() override final {return _onRequest ? false : true;} +}; + +#endif /* ASYNCWEBSERVERHANDLERIMPL_H_ */ diff --git a/yoRadio/src/AsyncWebServer/WebHandlers.cpp b/yoRadio/src/AsyncWebServer/WebHandlers.cpp new file mode 100644 index 0000000..1f435e6 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/WebHandlers.cpp @@ -0,0 +1,220 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "ESPAsyncWebServer.h" +#include "WebHandlerImpl.h" + +AsyncStaticWebHandler::AsyncStaticWebHandler(const char* uri, FS& fs, const char* path, const char* cache_control) + : _fs(fs), _uri(uri), _path(path), _default_file("index.htm"), _cache_control(cache_control), _last_modified(""), _callback(nullptr) +{ + // Ensure leading '/' + if (_uri.length() == 0 || _uri[0] != '/') _uri = "/" + _uri; + if (_path.length() == 0 || _path[0] != '/') _path = "/" + _path; + + // If path ends with '/' we assume a hint that this is a directory to improve performance. + // However - if it does not end with '/' we, can't assume a file, path can still be a directory. + _isDir = _path[_path.length()-1] == '/'; + + // Remove the trailing '/' so we can handle default file + // Notice that root will be "" not "/" + if (_uri[_uri.length()-1] == '/') _uri = _uri.substring(0, _uri.length()-1); + if (_path[_path.length()-1] == '/') _path = _path.substring(0, _path.length()-1); + + // Reset stats + _gzipFirst = false; + _gzipStats = 0xF8; +} + +AsyncStaticWebHandler& AsyncStaticWebHandler::setIsDir(bool isDir){ + _isDir = isDir; + return *this; +} + +AsyncStaticWebHandler& AsyncStaticWebHandler::setDefaultFile(const char* filename){ + _default_file = String(filename); + return *this; +} + +AsyncStaticWebHandler& AsyncStaticWebHandler::setCacheControl(const char* cache_control){ + _cache_control = String(cache_control); + return *this; +} + +AsyncStaticWebHandler& AsyncStaticWebHandler::setLastModified(const char* last_modified){ + _last_modified = String(last_modified); + return *this; +} + +AsyncStaticWebHandler& AsyncStaticWebHandler::setLastModified(struct tm* last_modified){ + char result[30]; + strftime (result,30,"%a, %d %b %Y %H:%M:%S %Z", last_modified); + return setLastModified((const char *)result); +} + +#ifdef ESP8266 +AsyncStaticWebHandler& AsyncStaticWebHandler::setLastModified(time_t last_modified){ + return setLastModified((struct tm *)gmtime(&last_modified)); +} + +AsyncStaticWebHandler& AsyncStaticWebHandler::setLastModified(){ + time_t last_modified; + if(time(&last_modified) == 0) //time is not yet set + return *this; + return setLastModified(last_modified); +} +#endif +bool AsyncStaticWebHandler::canHandle(AsyncWebServerRequest *request){ + if(request->method() != HTTP_GET + || !request->url().startsWith(_uri) + || !request->isExpectedRequestedConnType(RCT_DEFAULT, RCT_HTTP) + ){ + return false; + } + if (_getFile(request)) { + // We interested in "If-Modified-Since" header to check if file was modified + if (_last_modified.length()) + request->addInterestingHeader("If-Modified-Since"); + + if(_cache_control.length()) + request->addInterestingHeader("If-None-Match"); + + DEBUGF("[AsyncStaticWebHandler::canHandle] TRUE\n"); + return true; + } + + return false; +} + +bool AsyncStaticWebHandler::_getFile(AsyncWebServerRequest *request) +{ + // Remove the found uri + String path = request->url().substring(_uri.length()); + + // We can skip the file check and look for default if request is to the root of a directory or that request path ends with '/' + bool canSkipFileCheck = (_isDir && path.length() == 0) || (path.length() && path[path.length()-1] == '/'); + + path = _path + path; + + // Do we have a file or .gz file + if (!canSkipFileCheck && _fileExists(request, path)) + return true; + + // Can't handle if not default file + if (_default_file.length() == 0) + return false; + + // Try to add default file, ensure there is a trailing '/' ot the path. + if (path.length() == 0 || path[path.length()-1] != '/') + path += "/"; + path += _default_file; + + return _fileExists(request, path); +} + +#ifdef ESP32 +#define FILE_IS_REAL(f) (f == true && !f.isDirectory()) +#else +#define FILE_IS_REAL(f) (f == true) +#endif + +bool AsyncStaticWebHandler::_fileExists(AsyncWebServerRequest *request, const String& path) +{ + bool fileFound = false; + bool gzipFound = false; + + String gzip = path + ".gz"; + + if (_gzipFirst) { + request->_tempFile = _fs.open(gzip, "r"); + gzipFound = FILE_IS_REAL(request->_tempFile); + if (!gzipFound){ + request->_tempFile = _fs.open(path, "r"); + fileFound = FILE_IS_REAL(request->_tempFile); + } + } else { + request->_tempFile = _fs.open(path, "r"); + fileFound = FILE_IS_REAL(request->_tempFile); + if (!fileFound){ + request->_tempFile = _fs.open(gzip, "r"); + gzipFound = FILE_IS_REAL(request->_tempFile); + } + } + + bool found = fileFound || gzipFound; + + if (found) { + // Extract the file name from the path and keep it in _tempObject + size_t pathLen = path.length(); + char * _tempPath = (char*)malloc(pathLen+1); + snprintf(_tempPath, pathLen+1, "%s", path.c_str()); + request->_tempObject = (void*)_tempPath; + + // Calculate gzip statistic + _gzipStats = (_gzipStats << 1) + (gzipFound ? 1 : 0); + if (_gzipStats == 0x00) _gzipFirst = false; // All files are not gzip + else if (_gzipStats == 0xFF) _gzipFirst = true; // All files are gzip + else _gzipFirst = _countBits(_gzipStats) > 4; // IF we have more gzip files - try gzip first + } + + return found; +} + +uint8_t AsyncStaticWebHandler::_countBits(const uint8_t value) const +{ + uint8_t w = value; + uint8_t n; + for (n=0; w!=0; n++) w&=w-1; + return n; +} + +void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) +{ + // Get the filename from request->_tempObject and free it + String filename = String((char*)request->_tempObject); + free(request->_tempObject); + request->_tempObject = NULL; + if((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) + return request->requestAuthentication(); + + if (request->_tempFile == true) { + String etag = String(request->_tempFile.size()); + if (_last_modified.length() && _last_modified == request->header("If-Modified-Since")) { + request->_tempFile.close(); + request->send(304); // Not modified + } else if (_cache_control.length() && request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag)) { + request->_tempFile.close(); + AsyncWebServerResponse * response = new AsyncBasicResponse(304); // Not modified + response->addHeader("Cache-Control", _cache_control); + response->addHeader("ETag", etag); + request->send(response); + } else { + AsyncWebServerResponse * response = new AsyncFileResponse(request->_tempFile, filename, String(), false, _callback); + if (_last_modified.length()) + response->addHeader("Last-Modified", _last_modified); + if (_cache_control.length()){ + response->addHeader("Cache-Control", _cache_control); + response->addHeader("ETag", etag); + } + request->send(response); + } + } else { + request->send(404); + } +} diff --git a/yoRadio/src/AsyncWebServer/WebRequest.cpp b/yoRadio/src/AsyncWebServer/WebRequest.cpp new file mode 100644 index 0000000..bbce5ca --- /dev/null +++ b/yoRadio/src/AsyncWebServer/WebRequest.cpp @@ -0,0 +1,1008 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "ESPAsyncWebServer.h" +#include "WebResponseImpl.h" +#include "WebAuthentication.h" + +#ifndef ESP8266 +#define os_strlen strlen +#endif + +static const String SharedEmptyString = String(); + +#define __is_param_char(c) ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '=')) + +enum { PARSE_REQ_START, PARSE_REQ_HEADERS, PARSE_REQ_BODY, PARSE_REQ_END, PARSE_REQ_FAIL }; + +AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c) + : _client(c) + , _server(s) + , _handler(NULL) + , _response(NULL) + , _temp() + , _parseState(0) + , _version(0) + , _method(HTTP_ANY) + , _url() + , _host() + , _contentType() + , _boundary() + , _authorization() + , _reqconntype(RCT_HTTP) + , _isDigest(false) + , _isMultipart(false) + , _isPlainPost(false) + , _expectingContinue(false) + , _contentLength(0) + , _parsedLength(0) + , _headers(LinkedList([](AsyncWebHeader *h){ delete h; })) + , _params(LinkedList([](AsyncWebParameter *p){ delete p; })) + , _pathParams(LinkedList([](String *p){ delete p; })) + , _multiParseState(0) + , _boundaryPosition(0) + , _itemStartIndex(0) + , _itemSize(0) + , _itemName() + , _itemFilename() + , _itemType() + , _itemValue() + , _itemBuffer(0) + , _itemBufferIndex(0) + , _itemIsFile(false) + , _tempObject(NULL) +{ + c->onError([](void *r, AsyncClient* c, int8_t error){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onError(error); }, this); + c->onAck([](void *r, AsyncClient* c, size_t len, uint32_t time){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onAck(len, time); }, this); + c->onDisconnect([](void *r, AsyncClient* c){ AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onDisconnect(); delete c; }, this); + c->onTimeout([](void *r, AsyncClient* c, uint32_t time){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onTimeout(time); }, this); + c->onData([](void *r, AsyncClient* c, void *buf, size_t len){ (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onData(buf, len); }, this); + c->onPoll([](void *r, AsyncClient* c){ (void)c; AsyncWebServerRequest *req = ( AsyncWebServerRequest*)r; req->_onPoll(); }, this); +} + +AsyncWebServerRequest::~AsyncWebServerRequest(){ + _headers.free(); + + _params.free(); + _pathParams.free(); + + _interestingHeaders.free(); + + if(_response != NULL){ + delete _response; + } + + if(_tempObject != NULL){ + free(_tempObject); + } + + if(_tempFile){ + _tempFile.close(); + } +} + +void AsyncWebServerRequest::_onData(void *buf, size_t len){ + size_t i = 0; + while (true) { + + if(_parseState < PARSE_REQ_BODY){ + // Find new line in buf + char *str = (char*)buf; + for (i = 0; i < len; i++) { + if (str[i] == '\n') { + break; + } + } + if (i == len) { // No new line, just add the buffer in _temp + char ch = str[len-1]; + str[len-1] = 0; + _temp.reserve(_temp.length()+len); + _temp.concat(str); + _temp.concat(ch); + } else { // Found new line - extract it and parse + str[i] = 0; // Terminate the string at the end of the line. + _temp.concat(str); + _temp.trim(); + _parseLine(); + if (++i < len) { + // Still have more buffer to process + buf = str+i; + len-= i; + continue; + } + } + } else if(_parseState == PARSE_REQ_BODY){ + // A handler should be already attached at this point in _parseLine function. + // If handler does nothing (_onRequest is NULL), we don't need to really parse the body. + const bool needParse = _handler && !_handler->isRequestHandlerTrivial(); + if(_isMultipart){ + if(needParse){ + size_t i; + for(i=0; i end) equal = end; + String name = params.substring(start, equal); + String value = equal + 1 < end ? params.substring(equal + 1, end) : String(); + _addParam(new AsyncWebParameter(urlDecode(name), urlDecode(value))); + start = end + 1; + } +} + +bool AsyncWebServerRequest::_parseReqHead(){ + // Split the head into method, url and version + int index = _temp.indexOf(' '); + String m = _temp.substring(0, index); + index = _temp.indexOf(' ', index+1); + String u = _temp.substring(m.length()+1, index); + _temp = _temp.substring(index+1); + + if(m == "GET"){ + _method = HTTP_GET; + } else if(m == "POST"){ + _method = HTTP_POST; + } else if(m == "DELETE"){ + _method = HTTP_DELETE; + } else if(m == "PUT"){ + _method = HTTP_PUT; + } else if(m == "PATCH"){ + _method = HTTP_PATCH; + } else if(m == "HEAD"){ + _method = HTTP_HEAD; + } else if(m == "OPTIONS"){ + _method = HTTP_OPTIONS; + } + + String g = String(); + index = u.indexOf('?'); + if(index > 0){ + g = u.substring(index +1); + u = u.substring(0, index); + } + _url = urlDecode(u); + _addGetParams(g); + + if(!_temp.startsWith("HTTP/1.0")) + _version = 1; + + _temp = String(); + return true; +} + +bool strContains(String src, String find, bool mindcase = true) { + int pos=0, i=0; + const int slen = src.length(); + const int flen = find.length(); + + if (slen < flen) return false; + while (pos <= (slen - flen)) { + for (i=0; i < flen; i++) { + if (mindcase) { + if (src[pos+i] != find[i]) i = flen + 1; // no match + } else if (tolower(src[pos+i]) != tolower(find[i])) i = flen + 1; // no match + } + if (i == flen) return true; + pos++; + } + return false; +} + +bool AsyncWebServerRequest::_parseReqHeader(){ + int index = _temp.indexOf(':'); + if(index){ + String name = _temp.substring(0, index); + String value = _temp.substring(index + 2); + if(name.equalsIgnoreCase("Host")){ + _host = value; + } else if(name.equalsIgnoreCase("Content-Type")){ + _contentType = value.substring(0, value.indexOf(';')); + if (value.startsWith("multipart/")){ + _boundary = value.substring(value.indexOf('=')+1); + _boundary.replace("\"",""); + _isMultipart = true; + } + } else if(name.equalsIgnoreCase("Content-Length")){ + _contentLength = atoi(value.c_str()); + } else if(name.equalsIgnoreCase("Expect") && value == "100-continue"){ + _expectingContinue = true; + } else if(name.equalsIgnoreCase("Authorization")){ + if(value.length() > 5 && value.substring(0,5).equalsIgnoreCase("Basic")){ + _authorization = value.substring(6); + } else if(value.length() > 6 && value.substring(0,6).equalsIgnoreCase("Digest")){ + _isDigest = true; + _authorization = value.substring(7); + } + } else { + if(name.equalsIgnoreCase("Upgrade") && value.equalsIgnoreCase("websocket")){ + // WebSocket request can be uniquely identified by header: [Upgrade: websocket] + _reqconntype = RCT_WS; + } else { + if(name.equalsIgnoreCase("Accept") && strContains(value, "text/event-stream", false)){ + // WebEvent request can be uniquely identified by header: [Accept: text/event-stream] + _reqconntype = RCT_EVENT; + } + } + } + _headers.add(new AsyncWebHeader(name, value)); + } + _temp = String(); + return true; +} + +void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data){ + if(data && (char)data != '&') + _temp += (char)data; + if(!data || (char)data == '&' || _parsedLength == _contentLength){ + String name = "body"; + String value = _temp; + if(!_temp.startsWith("{") && !_temp.startsWith("[") && _temp.indexOf('=') > 0){ + name = _temp.substring(0, _temp.indexOf('=')); + value = _temp.substring(_temp.indexOf('=') + 1); + } + _addParam(new AsyncWebParameter(urlDecode(name), urlDecode(value), true)); + _temp = String(); + } +} + +void AsyncWebServerRequest::_handleUploadByte(uint8_t data, bool last){ + _itemBuffer[_itemBufferIndex++] = data; + + if(last || _itemBufferIndex == 1460){ + //check if authenticated before calling the upload + if(_handler) + _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, false); + _itemBufferIndex = 0; + } +} + +enum { + EXPECT_BOUNDARY, + PARSE_HEADERS, + WAIT_FOR_RETURN1, + EXPECT_FEED1, + EXPECT_DASH1, + EXPECT_DASH2, + BOUNDARY_OR_DATA, + DASH3_OR_RETURN2, + EXPECT_FEED2, + PARSING_FINISHED, + PARSE_ERROR +}; + +void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last){ +#define itemWriteByte(b) do { _itemSize++; if(_itemIsFile) _handleUploadByte(b, last); else _itemValue+=(char)(b); } while(0) + + if(!_parsedLength){ + _multiParseState = EXPECT_BOUNDARY; + _temp = String(); + _itemName = String(); + _itemFilename = String(); + _itemType = String(); + } + + if(_multiParseState == WAIT_FOR_RETURN1){ + if(data != '\r'){ + itemWriteByte(data); + } else { + _multiParseState = EXPECT_FEED1; + } + } else if(_multiParseState == EXPECT_BOUNDARY){ + if(_parsedLength < 2 && data != '-'){ + _multiParseState = PARSE_ERROR; + return; + } else if(_parsedLength - 2 < _boundary.length() && _boundary.c_str()[_parsedLength - 2] != data){ + _multiParseState = PARSE_ERROR; + return; + } else if(_parsedLength - 2 == _boundary.length() && data != '\r'){ + _multiParseState = PARSE_ERROR; + return; + } else if(_parsedLength - 3 == _boundary.length()){ + if(data != '\n'){ + _multiParseState = PARSE_ERROR; + return; + } + _multiParseState = PARSE_HEADERS; + _itemIsFile = false; + } + } else if(_multiParseState == PARSE_HEADERS){ + if((char)data != '\r' && (char)data != '\n') + _temp += (char)data; + if((char)data == '\n'){ + if(_temp.length()){ + if(_temp.length() > 12 && _temp.substring(0, 12).equalsIgnoreCase("Content-Type")){ + _itemType = _temp.substring(14); + _itemIsFile = true; + } else if(_temp.length() > 19 && _temp.substring(0, 19).equalsIgnoreCase("Content-Disposition")){ + _temp = _temp.substring(_temp.indexOf(';') + 2); + while(_temp.indexOf(';') > 0){ + String name = _temp.substring(0, _temp.indexOf('=')); + String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.indexOf(';') - 1); + if(name == "name"){ + _itemName = nameVal; + } else if(name == "filename"){ + _itemFilename = nameVal; + _itemIsFile = true; + } + _temp = _temp.substring(_temp.indexOf(';') + 2); + } + String name = _temp.substring(0, _temp.indexOf('=')); + String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.length() - 1); + if(name == "name"){ + _itemName = nameVal; + } else if(name == "filename"){ + _itemFilename = nameVal; + _itemIsFile = true; + } + } + _temp = String(); + } else { + _multiParseState = WAIT_FOR_RETURN1; + //value starts from here + _itemSize = 0; + _itemStartIndex = _parsedLength; + _itemValue = String(); + if(_itemIsFile){ + if(_itemBuffer) + free(_itemBuffer); + _itemBuffer = (uint8_t*)malloc(1460); + if(_itemBuffer == NULL){ + _multiParseState = PARSE_ERROR; + return; + } + _itemBufferIndex = 0; + } + } + } + } else if(_multiParseState == EXPECT_FEED1){ + if(data != '\n'){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); _parseMultipartPostByte(data, last); + } else { + _multiParseState = EXPECT_DASH1; + } + } else if(_multiParseState == EXPECT_DASH1){ + if(data != '-'){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); _parseMultipartPostByte(data, last); + } else { + _multiParseState = EXPECT_DASH2; + } + } else if(_multiParseState == EXPECT_DASH2){ + if(data != '-'){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); _parseMultipartPostByte(data, last); + } else { + _multiParseState = BOUNDARY_OR_DATA; + _boundaryPosition = 0; + } + } else if(_multiParseState == BOUNDARY_OR_DATA){ + if(_boundaryPosition < _boundary.length() && _boundary.c_str()[_boundaryPosition] != data){ + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); + uint8_t i; + for(i=0; i<_boundaryPosition; i++) + itemWriteByte(_boundary.c_str()[i]); + _parseMultipartPostByte(data, last); + } else if(_boundaryPosition == _boundary.length() - 1){ + _multiParseState = DASH3_OR_RETURN2; + if(!_itemIsFile){ + _addParam(new AsyncWebParameter(_itemName, _itemValue, true)); + } else { + if(_itemSize){ + //check if authenticated before calling the upload + if(_handler) _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); + _itemBufferIndex = 0; + _addParam(new AsyncWebParameter(_itemName, _itemFilename, true, true, _itemSize)); + } + free(_itemBuffer); + _itemBuffer = NULL; + } + + } else { + _boundaryPosition++; + } + } else if(_multiParseState == DASH3_OR_RETURN2){ + if(data == '-' && (_contentLength - _parsedLength - 4) != 0){ + //os_printf("ERROR: The parser got to the end of the POST but is expecting %u bytes more!\nDrop an issue so we can have more info on the matter!\n", _contentLength - _parsedLength - 4); + _contentLength = _parsedLength + 4;//lets close the request gracefully + } + if(data == '\r'){ + _multiParseState = EXPECT_FEED2; + } else if(data == '-' && _contentLength == (_parsedLength + 4)){ + _multiParseState = PARSING_FINISHED; + } else { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); + uint8_t i; for(i=0; i<_boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); + _parseMultipartPostByte(data, last); + } + } else if(_multiParseState == EXPECT_FEED2){ + if(data == '\n'){ + _multiParseState = PARSE_HEADERS; + _itemIsFile = false; + } else { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); + uint8_t i; for(i=0; i<_boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); + itemWriteByte('\r'); _parseMultipartPostByte(data, last); + } + } +} + +void AsyncWebServerRequest::_parseLine(){ + if(_parseState == PARSE_REQ_START){ + if(!_temp.length()){ + _parseState = PARSE_REQ_FAIL; + _client->close(); + } else { + _parseReqHead(); + _parseState = PARSE_REQ_HEADERS; + } + return; + } + + if(_parseState == PARSE_REQ_HEADERS){ + if(!_temp.length()){ + //end of headers + _server->_rewriteRequest(this); + _server->_attachHandler(this); + _removeNotInterestingHeaders(); + if(_expectingContinue){ + const char * response = "HTTP/1.1 100 Continue\r\n\r\n"; + _client->write(response, os_strlen(response)); + } + //check handler for authentication + if(_contentLength){ + _parseState = PARSE_REQ_BODY; + } else { + _parseState = PARSE_REQ_END; + if(_handler) _handler->handleRequest(this); + else send(501); + } + } else _parseReqHeader(); + } +} + +size_t AsyncWebServerRequest::headers() const{ + return _headers.length(); +} + +bool AsyncWebServerRequest::hasHeader(const String& name) const { + for(const auto& h: _headers){ + if(h->name().equalsIgnoreCase(name)){ + return true; + } + } + return false; +} + +bool AsyncWebServerRequest::hasHeader(const __FlashStringHelper * data) const { + PGM_P p = reinterpret_cast(data); + size_t n = 0; + while (1) { + if (pgm_read_byte(p+n) == 0) break; + n += 1; + } + char * name = (char*) malloc(n+1); + name[n] = 0; + if (name) { + for(size_t b=0; bname().equalsIgnoreCase(name)){ + return h; + } + } + return nullptr; +} + +AsyncWebHeader* AsyncWebServerRequest::getHeader(const __FlashStringHelper * data) const { + PGM_P p = reinterpret_cast(data); + size_t n = strlen_P(p); + char * name = (char*) malloc(n+1); + if (name) { + strcpy_P(name, p); + AsyncWebHeader* result = getHeader( String(name)); + free(name); + return result; + } else { + return nullptr; + } +} + +AsyncWebHeader* AsyncWebServerRequest::getHeader(size_t num) const { + auto header = _headers.nth(num); + return header ? *header : nullptr; +} + +size_t AsyncWebServerRequest::params() const { + return _params.length(); +} + +bool AsyncWebServerRequest::hasParam(const String& name, bool post, bool file) const { + for(const auto& p: _params){ + if(p->name() == name && p->isPost() == post && p->isFile() == file){ + return true; + } + } + return false; +} + +bool AsyncWebServerRequest::hasParam(const __FlashStringHelper * data, bool post, bool file) const { + PGM_P p = reinterpret_cast(data); + size_t n = strlen_P(p); + + char * name = (char*) malloc(n+1); + name[n] = 0; + if (name) { + strcpy_P(name,p); + bool result = hasParam( name, post, file); + free(name); + return result; + } else { + return false; + } +} + +AsyncWebParameter* AsyncWebServerRequest::getParam(const String& name, bool post, bool file) const { + for(const auto& p: _params){ + if(p->name() == name && p->isPost() == post && p->isFile() == file){ + return p; + } + } + return nullptr; +} + +AsyncWebParameter* AsyncWebServerRequest::getParam(const __FlashStringHelper * data, bool post, bool file) const { + PGM_P p = reinterpret_cast(data); + size_t n = strlen_P(p); + char * name = (char*) malloc(n+1); + if (name) { + strcpy_P(name, p); + AsyncWebParameter* result = getParam(name, post, file); + free(name); + return result; + } else { + return nullptr; + } +} + +AsyncWebParameter* AsyncWebServerRequest::getParam(size_t num) const { + auto param = _params.nth(num); + return param ? *param : nullptr; +} + +void AsyncWebServerRequest::addInterestingHeader(const String& name){ + if(!_interestingHeaders.containsIgnoreCase(name)) + _interestingHeaders.add(name); +} + +void AsyncWebServerRequest::send(AsyncWebServerResponse *response){ + _response = response; + if(_response == NULL){ + _client->close(true); + _onDisconnect(); + return; + } + if(!_response->_sourceValid()){ + delete response; + _response = NULL; + send(500); + } + else { + _client->setRxTimeout(0); + _response->_respond(this); + } +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(int code, const String& contentType, const String& content){ + return new AsyncBasicResponse(code, contentType, content); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ + if(fs.exists(path) || (!download && fs.exists(path+".gz"))) + return new AsyncFileResponse(fs, path, contentType, download, callback); + return NULL; +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ + if(content == true) + return new AsyncFileResponse(content, path, contentType, download, callback); + return NULL; +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback){ + return new AsyncStreamResponse(stream, contentType, len, callback); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + return new AsyncCallbackResponse(contentType, len, callback, templateCallback); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + if(_version) + return new AsyncChunkedResponse(contentType, callback, templateCallback); + return new AsyncCallbackResponse(contentType, 0, callback, templateCallback); +} + +AsyncResponseStream * AsyncWebServerRequest::beginResponseStream(const String& contentType, size_t bufferSize){ + return new AsyncResponseStream(contentType, bufferSize); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback){ + return new AsyncProgmemResponse(code, contentType, content, len, callback); +} + +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback){ + return beginResponse_P(code, contentType, (const uint8_t *)content, strlen_P(content), callback); +} + +void AsyncWebServerRequest::send(int code, const String& contentType, const String& content){ + send(beginResponse(code, contentType, content)); +} + +void AsyncWebServerRequest::send(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ + if(fs.exists(path) || (!download && fs.exists(path+".gz"))){ + send(beginResponse(fs, path, contentType, download, callback)); + } else send(404); +} + +void AsyncWebServerRequest::send(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){ + if(content == true){ + send(beginResponse(content, path, contentType, download, callback)); + } else send(404); +} + +void AsyncWebServerRequest::send(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback){ + send(beginResponse(stream, contentType, len, callback)); +} + +void AsyncWebServerRequest::send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + send(beginResponse(contentType, len, callback, templateCallback)); +} + +void AsyncWebServerRequest::sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback){ + send(beginChunkedResponse(contentType, callback, templateCallback)); +} + +void AsyncWebServerRequest::send_P(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback){ + send(beginResponse_P(code, contentType, content, len, callback)); +} + +void AsyncWebServerRequest::send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback){ + send(beginResponse_P(code, contentType, content, callback)); +} + +void AsyncWebServerRequest::redirect(const String& url){ + AsyncWebServerResponse * response = beginResponse(302); + response->addHeader("Location",url); + send(response); +} + +bool AsyncWebServerRequest::authenticate(const char * username, const char * password, const char * realm, bool passwordIsHash){ + if(_authorization.length()){ + if(_isDigest) + return checkDigestAuthentication(_authorization.c_str(), methodToString(), username, password, realm, passwordIsHash, NULL, NULL, NULL); + else if(!passwordIsHash) + return checkBasicAuthentication(_authorization.c_str(), username, password); + else + return _authorization.equals(password); + } + return false; +} + +bool AsyncWebServerRequest::authenticate(const char * hash){ + if(!_authorization.length() || hash == NULL) + return false; + + if(_isDigest){ + String hStr = String(hash); + int separator = hStr.indexOf(":"); + if(separator <= 0) + return false; + String username = hStr.substring(0, separator); + hStr = hStr.substring(separator + 1); + separator = hStr.indexOf(":"); + if(separator <= 0) + return false; + String realm = hStr.substring(0, separator); + hStr = hStr.substring(separator + 1); + return checkDigestAuthentication(_authorization.c_str(), methodToString(), username.c_str(), hStr.c_str(), realm.c_str(), true, NULL, NULL, NULL); + } + + return (_authorization.equals(hash)); +} + +void AsyncWebServerRequest::requestAuthentication(const char * realm, bool isDigest){ + AsyncWebServerResponse * r = beginResponse(401); + if(!isDigest && realm == NULL){ + r->addHeader("WWW-Authenticate", "Basic realm=\"Login Required\""); + } else if(!isDigest){ + String header = "Basic realm=\""; + header.concat(realm); + header.concat("\""); + r->addHeader("WWW-Authenticate", header); + } else { + String header = "Digest "; + header.concat(requestDigestAuthentication(realm)); + r->addHeader("WWW-Authenticate", header); + } + send(r); +} + +bool AsyncWebServerRequest::hasArg(const char* name) const { + for(const auto& arg: _params){ + if(arg->name() == name){ + return true; + } + } + return false; +} + +bool AsyncWebServerRequest::hasArg(const __FlashStringHelper * data) const { + PGM_P p = reinterpret_cast(data); + size_t n = strlen_P(p); + char * name = (char*) malloc(n+1); + if (name) { + strcpy_P(name, p); + bool result = hasArg( name ); + free(name); + return result; + } else { + return false; + } +} + + +const String& AsyncWebServerRequest::arg(const String& name) const { + for(const auto& arg: _params){ + if(arg->name() == name){ + return arg->value(); + } + } + return SharedEmptyString; +} + +const String& AsyncWebServerRequest::arg(const __FlashStringHelper * data) const { + PGM_P p = reinterpret_cast(data); + size_t n = strlen_P(p); + char * name = (char*) malloc(n+1); + if (name) { + strcpy_P(name, p); + const String & result = arg( String(name) ); + free(name); + return result; + } else { + return SharedEmptyString; + } + +} + +const String& AsyncWebServerRequest::arg(size_t i) const { + return getParam(i)->value(); +} + +const String& AsyncWebServerRequest::argName(size_t i) const { + return getParam(i)->name(); +} + +const String& AsyncWebServerRequest::pathArg(size_t i) const { + auto param = _pathParams.nth(i); + return param ? **param : SharedEmptyString; +} + +const String& AsyncWebServerRequest::header(const char* name) const { + AsyncWebHeader* h = getHeader(String(name)); + return h ? h->value() : SharedEmptyString; +} + +const String& AsyncWebServerRequest::header(const __FlashStringHelper * data) const { + PGM_P p = reinterpret_cast(data); + size_t n = strlen_P(p); + char * name = (char*) malloc(n+1); + if (name) { + strcpy_P(name, p); + const String & result = header( (const char *)name ); + free(name); + return result; + } else { + return SharedEmptyString; + } +}; + + +const String& AsyncWebServerRequest::header(size_t i) const { + AsyncWebHeader* h = getHeader(i); + return h ? h->value() : SharedEmptyString; +} + +const String& AsyncWebServerRequest::headerName(size_t i) const { + AsyncWebHeader* h = getHeader(i); + return h ? h->name() : SharedEmptyString; +} + +String AsyncWebServerRequest::urlDecode(const String& text) const { + char temp[] = "0x00"; + unsigned int len = text.length(); + unsigned int i = 0; + String decoded = String(); + decoded.reserve(len); // Allocate the string internal buffer - never longer from source text + while (i < len){ + char decodedChar; + char encodedChar = text.charAt(i++); + if ((encodedChar == '%') && (i + 1 < len)){ + temp[2] = text.charAt(i++); + temp[3] = text.charAt(i++); + decodedChar = strtol(temp, NULL, 16); + } else if (encodedChar == '+') { + decodedChar = ' '; + } else { + decodedChar = encodedChar; // normal ascii char + } + decoded.concat(decodedChar); + } + return decoded; +} + + +const char * AsyncWebServerRequest::methodToString() const { + if(_method == HTTP_ANY) return "ANY"; + else if(_method & HTTP_GET) return "GET"; + else if(_method & HTTP_POST) return "POST"; + else if(_method & HTTP_DELETE) return "DELETE"; + else if(_method & HTTP_PUT) return "PUT"; + else if(_method & HTTP_PATCH) return "PATCH"; + else if(_method & HTTP_HEAD) return "HEAD"; + else if(_method & HTTP_OPTIONS) return "OPTIONS"; + return "UNKNOWN"; +} + +const char *AsyncWebServerRequest::requestedConnTypeToString() const { + switch (_reqconntype) { + case RCT_NOT_USED: return "RCT_NOT_USED"; + case RCT_DEFAULT: return "RCT_DEFAULT"; + case RCT_HTTP: return "RCT_HTTP"; + case RCT_WS: return "RCT_WS"; + case RCT_EVENT: return "RCT_EVENT"; + default: return "ERROR"; + } +} + +bool AsyncWebServerRequest::isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2, RequestedConnectionType erct3) { + bool res = false; + if ((erct1 != RCT_NOT_USED) && (erct1 == _reqconntype)) res = true; + if ((erct2 != RCT_NOT_USED) && (erct2 == _reqconntype)) res = true; + if ((erct3 != RCT_NOT_USED) && (erct3 == _reqconntype)) res = true; + return res; +} diff --git a/yoRadio/src/AsyncWebServer/WebResponseImpl.h b/yoRadio/src/AsyncWebServer/WebResponseImpl.h new file mode 100644 index 0000000..9a64e3a --- /dev/null +++ b/yoRadio/src/AsyncWebServer/WebResponseImpl.h @@ -0,0 +1,136 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef ASYNCWEBSERVERRESPONSEIMPL_H_ +#define ASYNCWEBSERVERRESPONSEIMPL_H_ + +#ifdef Arduino_h +// arduino is not compatible with std::vector +#undef min +#undef max +#endif +#include +// It is possible to restore these defines, but one can use _min and _max instead. Or std::min, std::max. + +class AsyncBasicResponse: public AsyncWebServerResponse { + private: + String _content; + public: + AsyncBasicResponse(int code, const String& contentType=String(), const String& content=String()); + void _respond(AsyncWebServerRequest *request); + size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); + bool _sourceValid() const { return true; } +}; + +class AsyncAbstractResponse: public AsyncWebServerResponse { + private: + String _head; + // Data is inserted into cache at begin(). + // This is inefficient with vector, but if we use some other container, + // we won't be able to access it as contiguous array of bytes when reading from it, + // so by gaining performance in one place, we'll lose it in another. + std::vector _cache; + size_t _readDataFromCacheOrContent(uint8_t* data, const size_t len); + size_t _fillBufferAndProcessTemplates(uint8_t* buf, size_t maxLen); + protected: + AwsTemplateProcessor _callback; + public: + AsyncAbstractResponse(AwsTemplateProcessor callback=nullptr); + void _respond(AsyncWebServerRequest *request); + size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); + bool _sourceValid() const { return false; } + virtual size_t _fillBuffer(uint8_t *buf __attribute__((unused)), size_t maxLen __attribute__((unused))) { return 0; } +}; + +#ifndef TEMPLATE_PLACEHOLDER +#define TEMPLATE_PLACEHOLDER '%' +#endif + +#define TEMPLATE_PARAM_NAME_LENGTH 32 +class AsyncFileResponse: public AsyncAbstractResponse { + using File = fs::File; + using FS = fs::FS; + private: + File _content; + String _path; + void _setContentType(const String& path); + public: + AsyncFileResponse(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + AsyncFileResponse(File content, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr); + ~AsyncFileResponse(); + bool _sourceValid() const { return !!(_content); } + virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; +}; + +class AsyncStreamResponse: public AsyncAbstractResponse { + private: + Stream *_content; + public: + AsyncStreamResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback=nullptr); + bool _sourceValid() const { return !!(_content); } + virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; +}; + +class AsyncCallbackResponse: public AsyncAbstractResponse { + private: + AwsResponseFiller _content; + size_t _filledLength; + public: + AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + bool _sourceValid() const { return !!(_content); } + virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; +}; + +class AsyncChunkedResponse: public AsyncAbstractResponse { + private: + AwsResponseFiller _content; + size_t _filledLength; + public: + AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback=nullptr); + bool _sourceValid() const { return !!(_content); } + virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; +}; + +class AsyncProgmemResponse: public AsyncAbstractResponse { + private: + const uint8_t * _content; + size_t _readLength; + public: + AsyncProgmemResponse(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback=nullptr); + bool _sourceValid() const { return true; } + virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; +}; + +class cbuf; + +class AsyncResponseStream: public AsyncAbstractResponse, public Print { + private: + cbuf *_content; + public: + AsyncResponseStream(const String& contentType, size_t bufferSize); + ~AsyncResponseStream(); + bool _sourceValid() const { return (_state < RESPONSE_END); } + virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override; + size_t write(const uint8_t *data, size_t len); + size_t write(uint8_t data); + using Print::write; +}; + +#endif /* ASYNCWEBSERVERRESPONSEIMPL_H_ */ diff --git a/yoRadio/src/AsyncWebServer/WebResponses.cpp b/yoRadio/src/AsyncWebServer/WebResponses.cpp new file mode 100644 index 0000000..a22e991 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/WebResponses.cpp @@ -0,0 +1,699 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "ESPAsyncWebServer.h" +#include "WebResponseImpl.h" +#include "cbuf.h" + +// Since ESP8266 does not link memchr by default, here's its implementation. +void* memchr(void* ptr, int ch, size_t count) +{ + unsigned char* p = static_cast(ptr); + while(count--) + if(*p++ == static_cast(ch)) + return --p; + return nullptr; +} + + +/* + * Abstract Response + * */ +const char* AsyncWebServerResponse::_responseCodeToString(int code) { + switch (code) { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Time-out"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-URI Too Large"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested range not satisfiable"; + case 417: return "Expectation Failed"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Time-out"; + case 505: return "HTTP Version not supported"; + default: return ""; + } +} + +AsyncWebServerResponse::AsyncWebServerResponse() + : _code(0) + , _headers(LinkedList([](AsyncWebHeader *h){ delete h; })) + , _contentType() + , _contentLength(0) + , _sendContentLength(true) + , _chunked(false) + , _headLength(0) + , _sentLength(0) + , _ackedLength(0) + , _writtenLength(0) + , _state(RESPONSE_SETUP) +{ + for(auto header: DefaultHeaders::Instance()) { + _headers.add(new AsyncWebHeader(header->name(), header->value())); + } +} + +AsyncWebServerResponse::~AsyncWebServerResponse(){ + _headers.free(); +} + +void AsyncWebServerResponse::setCode(int code){ + if(_state == RESPONSE_SETUP) + _code = code; +} + +void AsyncWebServerResponse::setContentLength(size_t len){ + if(_state == RESPONSE_SETUP) + _contentLength = len; +} + +void AsyncWebServerResponse::setContentType(const String& type){ + if(_state == RESPONSE_SETUP) + _contentType = type; +} + +void AsyncWebServerResponse::addHeader(const String& name, const String& value){ + _headers.add(new AsyncWebHeader(name, value)); +} + +String AsyncWebServerResponse::_assembleHead(uint8_t version){ + if(version){ + addHeader("Accept-Ranges","none"); + if(_chunked) + addHeader("Transfer-Encoding","chunked"); + } + String out = String(); + int bufSize = 300; + char buf[bufSize]; + + snprintf(buf, bufSize, "HTTP/1.%d %d %s\r\n", version, _code, _responseCodeToString(_code)); + out.concat(buf); + + if(_sendContentLength) { + snprintf(buf, bufSize, "Content-Length: %d\r\n", _contentLength); + out.concat(buf); + } + if(_contentType.length()) { + snprintf(buf, bufSize, "Content-Type: %s\r\n", _contentType.c_str()); + out.concat(buf); + } + + for(const auto& header: _headers){ + snprintf(buf, bufSize, "%s: %s\r\n", header->name().c_str(), header->value().c_str()); + out.concat(buf); + } + _headers.free(); + + out.concat("\r\n"); + _headLength = out.length(); + return out; +} + +bool AsyncWebServerResponse::_started() const { return _state > RESPONSE_SETUP; } +bool AsyncWebServerResponse::_finished() const { return _state > RESPONSE_WAIT_ACK; } +bool AsyncWebServerResponse::_failed() const { return _state == RESPONSE_FAILED; } +bool AsyncWebServerResponse::_sourceValid() const { return false; } +void AsyncWebServerResponse::_respond(AsyncWebServerRequest *request){ _state = RESPONSE_END; request->client()->close(); } +size_t AsyncWebServerResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ (void)request; (void)len; (void)time; return 0; } + +/* + * String/Code Response + * */ +AsyncBasicResponse::AsyncBasicResponse(int code, const String& contentType, const String& content){ + _code = code; + _content = content; + _contentType = contentType; + if(_content.length()){ + _contentLength = _content.length(); + if(!_contentType.length()) + _contentType = "text/plain"; + } + addHeader("Connection","close"); +} + +void AsyncBasicResponse::_respond(AsyncWebServerRequest *request){ + _state = RESPONSE_HEADERS; + String out = _assembleHead(request->version()); + size_t outLen = out.length(); + size_t space = request->client()->space(); + if(!_contentLength && space >= outLen){ + _writtenLength += request->client()->write(out.c_str(), outLen); + _state = RESPONSE_WAIT_ACK; + } else if(_contentLength && space >= outLen + _contentLength){ + out += _content; + outLen += _contentLength; + _writtenLength += request->client()->write(out.c_str(), outLen); + _state = RESPONSE_WAIT_ACK; + } else if(space && space < outLen){ + String partial = out.substring(0, space); + _content = out.substring(space) + _content; + _contentLength += outLen - space; + _writtenLength += request->client()->write(partial.c_str(), partial.length()); + _state = RESPONSE_CONTENT; + } else if(space > outLen && space < (outLen + _contentLength)){ + size_t shift = space - outLen; + outLen += shift; + _sentLength += shift; + out += _content.substring(0, shift); + _content = _content.substring(shift); + _writtenLength += request->client()->write(out.c_str(), outLen); + _state = RESPONSE_CONTENT; + } else { + _content = out + _content; + _contentLength += outLen; + _state = RESPONSE_CONTENT; + } +} + +size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ + (void)time; + _ackedLength += len; + if(_state == RESPONSE_CONTENT){ + size_t available = _contentLength - _sentLength; + size_t space = request->client()->space(); + //we can fit in this packet + if(space > available){ + _writtenLength += request->client()->write(_content.c_str(), available); + _content = String(); + _state = RESPONSE_WAIT_ACK; + return available; + } + //send some data, the rest on ack + String out = _content.substring(0, space); + _content = _content.substring(space); + _sentLength += space; + _writtenLength += request->client()->write(out.c_str(), space); + return space; + } else if(_state == RESPONSE_WAIT_ACK){ + if(_ackedLength >= _writtenLength){ + _state = RESPONSE_END; + } + } + return 0; +} + + +/* + * Abstract Response + * */ + +AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback): _callback(callback) +{ + // In case of template processing, we're unable to determine real response size + if(callback) { + _contentLength = 0; + _sendContentLength = false; + _chunked = true; + } +} + +void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request){ + addHeader("Connection","close"); + _head = _assembleHead(request->version()); + _state = RESPONSE_HEADERS; + _ack(request, 0, 0); +} + +size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ + (void)time; + if(!_sourceValid()){ + _state = RESPONSE_FAILED; + request->client()->close(); + return 0; + } + _ackedLength += len; + size_t space = request->client()->space(); + + size_t headLen = _head.length(); + if(_state == RESPONSE_HEADERS){ + if(space >= headLen){ + _state = RESPONSE_CONTENT; + space -= headLen; + } else { + String out = _head.substring(0, space); + _head = _head.substring(space); + _writtenLength += request->client()->write(out.c_str(), out.length()); + return out.length(); + } + } + + if(_state == RESPONSE_CONTENT){ + size_t outLen; + if(_chunked){ + if(space <= 8){ + return 0; + } + outLen = space; + } else if(!_sendContentLength){ + outLen = space; + } else { + outLen = ((_contentLength - _sentLength) > space)?space:(_contentLength - _sentLength); + } + + uint8_t *buf = (uint8_t *)malloc(outLen+headLen); + if (!buf) { + // os_printf("_ack malloc %d failed\n", outLen+headLen); + return 0; + } + + if(headLen){ + memcpy(buf, _head.c_str(), _head.length()); + } + + size_t readLen = 0; + + if(_chunked){ + // HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added. + // See RFC2616 sections 2, 3.6.1. + readLen = _fillBufferAndProcessTemplates(buf+headLen+6, outLen - 8); + if(readLen == RESPONSE_TRY_AGAIN){ + free(buf); + return 0; + } + outLen = sprintf((char*)buf+headLen, "%x", readLen) + headLen; + while(outLen < headLen + 4) buf[outLen++] = ' '; + buf[outLen++] = '\r'; + buf[outLen++] = '\n'; + outLen += readLen; + buf[outLen++] = '\r'; + buf[outLen++] = '\n'; + } else { + readLen = _fillBufferAndProcessTemplates(buf+headLen, outLen); + if(readLen == RESPONSE_TRY_AGAIN){ + free(buf); + return 0; + } + outLen = readLen + headLen; + } + + if(headLen){ + _head = String(); + } + + if(outLen){ + _writtenLength += request->client()->write((const char*)buf, outLen); + } + + if(_chunked){ + _sentLength += readLen; + } else { + _sentLength += outLen - headLen; + } + + free(buf); + + if((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)){ + _state = RESPONSE_WAIT_ACK; + } + return outLen; + + } else if(_state == RESPONSE_WAIT_ACK){ + if(!_sendContentLength || _ackedLength >= _writtenLength){ + _state = RESPONSE_END; + if(!_chunked && !_sendContentLength) + request->client()->close(true); + } + } + return 0; +} + +size_t AsyncAbstractResponse::_readDataFromCacheOrContent(uint8_t* data, const size_t len) +{ + // If we have something in cache, copy it to buffer + const size_t readFromCache = std::min(len, _cache.size()); + if(readFromCache) { + memcpy(data, _cache.data(), readFromCache); + _cache.erase(_cache.begin(), _cache.begin() + readFromCache); + } + // If we need to read more... + const size_t needFromFile = len - readFromCache; + const size_t readFromContent = _fillBuffer(data + readFromCache, needFromFile); + return readFromCache + readFromContent; +} + +size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t* data, size_t len) +{ + if(!_callback) + return _fillBuffer(data, len); + + const size_t originalLen = len; + len = _readDataFromCacheOrContent(data, len); + // Now we've read 'len' bytes, either from cache or from file + // Search for template placeholders + uint8_t* pTemplateStart = data; + while((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t*)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))) { // data[0] ... data[len - 1] + uint8_t* pTemplateEnd = (pTemplateStart < &data[len - 1]) ? (uint8_t*)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr; + // temporary buffer to hold parameter name + uint8_t buf[TEMPLATE_PARAM_NAME_LENGTH + 1]; + String paramName; + // If closing placeholder is found: + if(pTemplateEnd) { + // prepare argument to callback + const size_t paramNameLength = std::min(sizeof(buf) - 1, (unsigned int)(pTemplateEnd - pTemplateStart - 1)); + if(paramNameLength) { + memcpy(buf, pTemplateStart + 1, paramNameLength); + buf[paramNameLength] = 0; + paramName = String(reinterpret_cast(buf)); + } else { // double percent sign encountered, this is single percent sign escaped. + // remove the 2nd percent sign + memmove(pTemplateEnd, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); + len += _readDataFromCacheOrContent(&data[len - 1], 1) - 1; + ++pTemplateStart; + } + } else if(&data[len - 1] - pTemplateStart + 1 < TEMPLATE_PARAM_NAME_LENGTH + 2) { // closing placeholder not found, check if it's in the remaining file data + memcpy(buf, pTemplateStart + 1, &data[len - 1] - pTemplateStart); + const size_t readFromCacheOrContent = _readDataFromCacheOrContent(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PARAM_NAME_LENGTH + 2 - (&data[len - 1] - pTemplateStart + 1)); + if(readFromCacheOrContent) { + pTemplateEnd = (uint8_t*)memchr(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PLACEHOLDER, readFromCacheOrContent); + if(pTemplateEnd) { + // prepare argument to callback + *pTemplateEnd = 0; + paramName = String(reinterpret_cast(buf)); + // Copy remaining read-ahead data into cache + _cache.insert(_cache.begin(), pTemplateEnd + 1, buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); + pTemplateEnd = &data[len - 1]; + } + else // closing placeholder not found in file data, store found percent symbol as is and advance to the next position + { + // but first, store read file data in cache + _cache.insert(_cache.begin(), buf + (&data[len - 1] - pTemplateStart), buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); + ++pTemplateStart; + } + } + else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position + ++pTemplateStart; + } + else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position + ++pTemplateStart; + if(paramName.length()) { + // call callback and replace with result. + // Everything in range [pTemplateStart, pTemplateEnd] can be safely replaced with parameter value. + // Data after pTemplateEnd may need to be moved. + // The first byte of data after placeholder is located at pTemplateEnd + 1. + // It should be located at pTemplateStart + numBytesCopied (to begin right after inserted parameter value). + const String paramValue(_callback(paramName)); + const char* pvstr = paramValue.c_str(); + const unsigned int pvlen = paramValue.length(); + const size_t numBytesCopied = std::min(pvlen, static_cast(&data[originalLen - 1] - pTemplateStart + 1)); + // make room for param value + // 1. move extra data to cache if parameter value is longer than placeholder AND if there is no room to store + if((pTemplateEnd + 1 < pTemplateStart + numBytesCopied) && (originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1) < len)) { + _cache.insert(_cache.begin(), &data[originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1)], &data[len]); + //2. parameter value is longer than placeholder text, push the data after placeholder which not saved into cache further to the end + memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[originalLen] - pTemplateStart - numBytesCopied); + len = originalLen; // fix issue with truncated data, not sure if it has any side effects + } else if(pTemplateEnd + 1 != pTemplateStart + numBytesCopied) + //2. Either parameter value is shorter than placeholder text OR there is enough free space in buffer to fit. + // Move the entire data after the placeholder + memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); + // 3. replace placeholder with actual value + memcpy(pTemplateStart, pvstr, numBytesCopied); + // If result is longer than buffer, copy the remainder into cache (this could happen only if placeholder text itself did not fit entirely in buffer) + if(numBytesCopied < pvlen) { + _cache.insert(_cache.begin(), pvstr + numBytesCopied, pvstr + pvlen); + } else if(pTemplateStart + numBytesCopied < pTemplateEnd + 1) { // result is copied fully; if result is shorter than placeholder text... + // there is some free room, fill it from cache + const size_t roomFreed = pTemplateEnd + 1 - pTemplateStart - numBytesCopied; + const size_t totalFreeRoom = originalLen - len + roomFreed; + len += _readDataFromCacheOrContent(&data[len - roomFreed], totalFreeRoom) - roomFreed; + } else { // result is copied fully; it is longer than placeholder text + const size_t roomTaken = pTemplateStart + numBytesCopied - pTemplateEnd - 1; + len = std::min(len + roomTaken, originalLen); + } + } + } // while(pTemplateStart) + return len; +} + + +/* + * File Response + * */ + +AsyncFileResponse::~AsyncFileResponse(){ + if(_content) + _content.close(); +} + +void AsyncFileResponse::_setContentType(const String& path){ + if (path.endsWith(".html")) _contentType = "text/html"; + else if (path.endsWith(".htm")) _contentType = "text/html"; + else if (path.endsWith(".css")) _contentType = "text/css"; + else if (path.endsWith(".json")) _contentType = "application/json"; + else if (path.endsWith(".js")) _contentType = "application/javascript"; + else if (path.endsWith(".png")) _contentType = "image/png"; + else if (path.endsWith(".gif")) _contentType = "image/gif"; + else if (path.endsWith(".jpg")) _contentType = "image/jpeg"; + else if (path.endsWith(".ico")) _contentType = "image/x-icon"; + else if (path.endsWith(".svg")) _contentType = "image/svg+xml"; + else if (path.endsWith(".eot")) _contentType = "font/eot"; + else if (path.endsWith(".woff")) _contentType = "font/woff"; + else if (path.endsWith(".woff2")) _contentType = "font/woff2"; + else if (path.endsWith(".ttf")) _contentType = "font/ttf"; + else if (path.endsWith(".xml")) _contentType = "text/xml"; + else if (path.endsWith(".pdf")) _contentType = "application/pdf"; + else if (path.endsWith(".zip")) _contentType = "application/zip"; + else if(path.endsWith(".gz")) _contentType = "application/x-gzip"; + else _contentType = "text/plain"; +} + +AsyncFileResponse::AsyncFileResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){ + _code = 200; + _path = path; + + if(!download && !fs.exists(_path) && fs.exists(_path+".gz")){ + _path = _path+".gz"; + addHeader("Content-Encoding", "gzip"); + _callback = nullptr; // Unable to process zipped templates + _sendContentLength = true; + _chunked = false; + } + + _content = fs.open(_path, "r"); + _contentLength = _content.size(); + + if(contentType == "") + _setContentType(path); + else + _contentType = contentType; + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26+path.length()-filenameStart]; + char* filename = (char*)path.c_str() + filenameStart; + + if(download) { + // set filename and force download + snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename); + } else { + // set filename and force rendering + snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename); + } + addHeader("Content-Disposition", buf); +} + +AsyncFileResponse::AsyncFileResponse(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){ + _code = 200; + _path = path; + + if(!download && String(content.name()).endsWith(".gz") && !path.endsWith(".gz")){ + addHeader("Content-Encoding", "gzip"); + _callback = nullptr; // Unable to process gzipped templates + _sendContentLength = true; + _chunked = false; + } + + _content = content; + _contentLength = _content.size(); + + if(contentType == "") + _setContentType(path); + else + _contentType = contentType; + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26+path.length()-filenameStart]; + char* filename = (char*)path.c_str() + filenameStart; + + if(download) { + snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename); + } else { + snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename); + } + addHeader("Content-Disposition", buf); +} + +size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len){ + return _content.read(data, len); +} + +/* + * Stream Response + * */ + +AsyncStreamResponse::AsyncStreamResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) { + _code = 200; + _content = &stream; + _contentLength = len; + _contentType = contentType; +} + +size_t AsyncStreamResponse::_fillBuffer(uint8_t *data, size_t len){ + size_t available = _content->available(); + size_t outLen = (available > len)?len:available; + size_t i; + for(i=0;iread(); + return outLen; +} + +/* + * Callback Response + * */ + +AsyncCallbackResponse::AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback): AsyncAbstractResponse(templateCallback) { + _code = 200; + _content = callback; + _contentLength = len; + if(!len) + _sendContentLength = false; + _contentType = contentType; + _filledLength = 0; +} + +size_t AsyncCallbackResponse::_fillBuffer(uint8_t *data, size_t len){ + size_t ret = _content(data, len, _filledLength); + if(ret != RESPONSE_TRY_AGAIN){ + _filledLength += ret; + } + return ret; +} + +/* + * Chunked Response + * */ + +AsyncChunkedResponse::AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor processorCallback): AsyncAbstractResponse(processorCallback) { + _code = 200; + _content = callback; + _contentLength = 0; + _contentType = contentType; + _sendContentLength = false; + _chunked = true; + _filledLength = 0; +} + +size_t AsyncChunkedResponse::_fillBuffer(uint8_t *data, size_t len){ + size_t ret = _content(data, len, _filledLength); + if(ret != RESPONSE_TRY_AGAIN){ + _filledLength += ret; + } + return ret; +} + +/* + * Progmem Response + * */ + +AsyncProgmemResponse::AsyncProgmemResponse(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) { + _code = code; + _content = content; + _contentType = contentType; + _contentLength = len; + _readLength = 0; +} + +size_t AsyncProgmemResponse::_fillBuffer(uint8_t *data, size_t len){ + size_t left = _contentLength - _readLength; + if (left > len) { + memcpy_P(data, _content + _readLength, len); + _readLength += len; + return len; + } + memcpy_P(data, _content + _readLength, left); + _readLength += left; + return left; +} + + +/* + * Response Stream (You can print/write/printf to it, up to the contentLen bytes) + * */ + +AsyncResponseStream::AsyncResponseStream(const String& contentType, size_t bufferSize){ + _code = 200; + _contentLength = 0; + _contentType = contentType; + _content = new cbuf(bufferSize); +} + +AsyncResponseStream::~AsyncResponseStream(){ + delete _content; +} + +size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen){ + return _content->read((char*)buf, maxLen); +} + +size_t AsyncResponseStream::write(const uint8_t *data, size_t len){ + if(_started()) + return 0; + + if(len > _content->room()){ + size_t needed = len - _content->room(); + _content->resizeAdd(needed); + } + size_t written = _content->write((const char*)data, len); + _contentLength += written; + return written; +} + +size_t AsyncResponseStream::write(uint8_t data){ + return write(&data, 1); +} diff --git a/yoRadio/src/AsyncWebServer/WebServer.cpp b/yoRadio/src/AsyncWebServer/WebServer.cpp new file mode 100644 index 0000000..95c2dd6 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/WebServer.cpp @@ -0,0 +1,193 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "ESPAsyncWebServer.h" +#include "WebHandlerImpl.h" + +bool ON_STA_FILTER(AsyncWebServerRequest *request) { + return WiFi.localIP() == request->client()->localIP(); +} + +bool ON_AP_FILTER(AsyncWebServerRequest *request) { + return WiFi.localIP() != request->client()->localIP(); +} + + +AsyncWebServer::AsyncWebServer(uint16_t port) + : _server(port) + , _rewrites(LinkedList([](AsyncWebRewrite* r){ delete r; })) + , _handlers(LinkedList([](AsyncWebHandler* h){ delete h; })) +{ + _catchAllHandler = new AsyncCallbackWebHandler(); + if(_catchAllHandler == NULL) + return; + _server.onClient([](void *s, AsyncClient* c){ + if(c == NULL) + return; + c->setRxTimeout(3); + AsyncWebServerRequest *r = new AsyncWebServerRequest((AsyncWebServer*)s, c); + if(r == NULL){ + c->close(true); + c->free(); + delete c; + } + }, this); +} + +AsyncWebServer::~AsyncWebServer(){ + reset(); + end(); + if(_catchAllHandler) delete _catchAllHandler; +} + +AsyncWebRewrite& AsyncWebServer::addRewrite(AsyncWebRewrite* rewrite){ + _rewrites.add(rewrite); + return *rewrite; +} + +bool AsyncWebServer::removeRewrite(AsyncWebRewrite *rewrite){ + return _rewrites.remove(rewrite); +} + +AsyncWebRewrite& AsyncWebServer::rewrite(const char* from, const char* to){ + return addRewrite(new AsyncWebRewrite(from, to)); +} + +AsyncWebHandler& AsyncWebServer::addHandler(AsyncWebHandler* handler){ + _handlers.add(handler); + return *handler; +} + +bool AsyncWebServer::removeHandler(AsyncWebHandler *handler){ + return _handlers.remove(handler); +} + +void AsyncWebServer::begin(){ + _server.setNoDelay(true); + _server.begin(); +} + +void AsyncWebServer::end(){ + _server.end(); +} + +#if ASYNC_TCP_SSL_ENABLED +void AsyncWebServer::onSslFileRequest(AcSSlFileHandler cb, void* arg){ + _server.onSslFileRequest(cb, arg); +} + +void AsyncWebServer::beginSecure(const char *cert, const char *key, const char *password){ + _server.beginSecure(cert, key, password); +} +#endif + +void AsyncWebServer::_handleDisconnect(AsyncWebServerRequest *request){ + delete request; +} + +void AsyncWebServer::_rewriteRequest(AsyncWebServerRequest *request){ + for(const auto& r: _rewrites){ + if (r->match(request)){ + request->_url = r->toUrl(); + request->_addGetParams(r->params()); + } + } +} + +void AsyncWebServer::_attachHandler(AsyncWebServerRequest *request){ + for(const auto& h: _handlers){ + if (h->filter(request) && h->canHandle(request)){ + request->setHandler(h); + return; + } + } + + request->addInterestingHeader("ANY"); + request->setHandler(_catchAllHandler); +} + + +AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody){ + AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); + handler->setUri(uri); + handler->setMethod(method); + handler->onRequest(onRequest); + handler->onUpload(onUpload); + handler->onBody(onBody); + addHandler(handler); + return *handler; +} + +AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload){ + AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); + handler->setUri(uri); + handler->setMethod(method); + handler->onRequest(onRequest); + handler->onUpload(onUpload); + addHandler(handler); + return *handler; +} + +AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest){ + AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); + handler->setUri(uri); + handler->setMethod(method); + handler->onRequest(onRequest); + addHandler(handler); + return *handler; +} + +AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, ArRequestHandlerFunction onRequest){ + AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); + handler->setUri(uri); + handler->onRequest(onRequest); + addHandler(handler); + return *handler; +} + +AsyncStaticWebHandler& AsyncWebServer::serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control){ + AsyncStaticWebHandler* handler = new AsyncStaticWebHandler(uri, fs, path, cache_control); + addHandler(handler); + return *handler; +} + +void AsyncWebServer::onNotFound(ArRequestHandlerFunction fn){ + _catchAllHandler->onRequest(fn); +} + +void AsyncWebServer::onFileUpload(ArUploadHandlerFunction fn){ + _catchAllHandler->onUpload(fn); +} + +void AsyncWebServer::onRequestBody(ArBodyHandlerFunction fn){ + _catchAllHandler->onBody(fn); +} + +void AsyncWebServer::reset(){ + _rewrites.free(); + _handlers.free(); + + if (_catchAllHandler != NULL){ + _catchAllHandler->onRequest(NULL); + _catchAllHandler->onUpload(NULL); + _catchAllHandler->onBody(NULL); + } +} + diff --git a/yoRadio/src/AsyncWebServer/edit.htm b/yoRadio/src/AsyncWebServer/edit.htm new file mode 100644 index 0000000..43d4984 --- /dev/null +++ b/yoRadio/src/AsyncWebServer/edit.htm @@ -0,0 +1,627 @@ + + + + +ESP Editor + + + + + + +
+
+
+
+ + + + diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient.cpp new file mode 100644 index 0000000..29c10eb --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient.cpp @@ -0,0 +1,755 @@ +#include "AsyncMqttClient.hpp" + +AsyncMqttClient::AsyncMqttClient() +: _client() +, _head(nullptr) +, _tail(nullptr) +, _sent(0) +, _state(DISCONNECTED) +, _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) +, _lastClientActivity(0) +, _lastServerActivity(0) +, _lastPingRequestTime(0) +, _generatedClientId{0} +, _ip() +, _host(nullptr) +, _useIp(false) +#if ASYNC_TCP_SSL_ENABLED +, _secure(false) +#endif +, _port(0) +, _keepAlive(15) +, _cleanSession(true) +, _clientId(nullptr) +, _username(nullptr) +, _password(nullptr) +, _willTopic(nullptr) +, _willPayload(nullptr) +, _willPayloadLength(0) +, _willQos(0) +, _willRetain(false) +#if ASYNC_TCP_SSL_ENABLED +, _secureServerFingerprints() +#endif +, _onConnectUserCallbacks() +, _onDisconnectUserCallbacks() +, _onSubscribeUserCallbacks() +, _onUnsubscribeUserCallbacks() +, _onMessageUserCallbacks() +, _onPublishUserCallbacks() +, _parsingInformation { .bufferState = AsyncMqttClientInternals::BufferState::NONE, .maxTopicLength = 128, .topicBuffer = NULL, .packetType = 0, .packetFlags = 0, .remainingLength = 0 } +, _currentParsedPacket(nullptr) +, _remainingLengthBufferPosition(0) +, _remainingLengthBuffer{0} +, _pendingPubRels() { + _client.onConnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onConnect(); }, this); + _client.onDisconnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onDisconnect(); }, this); + // _client.onError([](void* obj, AsyncClient* c, int8_t error) { (static_cast(obj))->_onError(error); }, this); + // _client.onTimeout([](void* obj, AsyncClient* c, uint32_t time) { (static_cast(obj))->_onTimeout(); }, this); + _client.onAck([](void* obj, AsyncClient* c, size_t len, uint32_t time) { (static_cast(obj))->_onAck(len); }, this); + _client.onData([](void* obj, AsyncClient* c, void* data, size_t len) { (static_cast(obj))->_onData(static_cast(data), len); }, this); + _client.onPoll([](void* obj, AsyncClient* c) { (static_cast(obj))->_onPoll(); }, this); + _client.setNoDelay(true); // send small packets immediately (PINGREQ/DISCONN are only 2 bytes) +#ifdef ESP32 + sprintf(_generatedClientId, "esp32-%06llx", ESP.getEfuseMac()); + _xSemaphore = xSemaphoreCreateMutex(); +#elif defined(ESP8266) + sprintf(_generatedClientId, "esp8266-%06x", ESP.getChipId()); +#endif + _clientId = _generatedClientId; + + setMaxTopicLength(128); +} + +AsyncMqttClient::~AsyncMqttClient() { + delete _currentParsedPacket; + delete[] _parsingInformation.topicBuffer; + _clear(); + _pendingPubRels.clear(); + _pendingPubRels.shrink_to_fit(); + _clearQueue(false); // _clear() doesn't clear session data +#ifdef ESP32 + vSemaphoreDelete(_xSemaphore); +#endif +} + +AsyncMqttClient& AsyncMqttClient::setKeepAlive(uint16_t keepAlive) { + _keepAlive = keepAlive; + return *this; +} + +AsyncMqttClient& AsyncMqttClient::setClientId(const char* clientId) { + _clientId = clientId; + return *this; +} + +AsyncMqttClient& AsyncMqttClient::setCleanSession(bool cleanSession) { + _cleanSession = cleanSession; + return *this; +} + +AsyncMqttClient& AsyncMqttClient::setMaxTopicLength(uint16_t maxTopicLength) { + _parsingInformation.maxTopicLength = maxTopicLength; + delete[] _parsingInformation.topicBuffer; + _parsingInformation.topicBuffer = new char[maxTopicLength + 1]; + return *this; +} + +AsyncMqttClient& AsyncMqttClient::setCredentials(const char* username, const char* password) { + _username = username; + _password = password; + return *this; +} + +AsyncMqttClient& AsyncMqttClient::setWill(const char* topic, uint8_t qos, bool retain, const char* payload, size_t length) { + _willTopic = topic; + _willQos = qos; + _willRetain = retain; + _willPayload = payload; + _willPayloadLength = length; + return *this; +} + +AsyncMqttClient& AsyncMqttClient::setServer(IPAddress ip, uint16_t port) { + _useIp = true; + _ip = ip; + _port = port; + return *this; +} + +AsyncMqttClient& AsyncMqttClient::setServer(const char* host, uint16_t port) { + _useIp = false; + _host = host; + _port = port; + return *this; +} + +#if ASYNC_TCP_SSL_ENABLED +AsyncMqttClient& AsyncMqttClient::setSecure(bool secure) { + _secure = secure; + return *this; +} + +AsyncMqttClient& AsyncMqttClient::addServerFingerprint(const uint8_t* fingerprint) { + std::array newFingerprint; + memcpy(newFingerprint.data(), fingerprint, SHA1_SIZE); + _secureServerFingerprints.push_back(newFingerprint); + return *this; +} +#endif + +AsyncMqttClient& AsyncMqttClient::onConnect(AsyncMqttClientInternals::OnConnectUserCallback callback) { + _onConnectUserCallbacks.push_back(callback); + return *this; +} + +AsyncMqttClient& AsyncMqttClient::onDisconnect(AsyncMqttClientInternals::OnDisconnectUserCallback callback) { + _onDisconnectUserCallbacks.push_back(callback); + return *this; +} + +AsyncMqttClient& AsyncMqttClient::onSubscribe(AsyncMqttClientInternals::OnSubscribeUserCallback callback) { + _onSubscribeUserCallbacks.push_back(callback); + return *this; +} + +AsyncMqttClient& AsyncMqttClient::onUnsubscribe(AsyncMqttClientInternals::OnUnsubscribeUserCallback callback) { + _onUnsubscribeUserCallbacks.push_back(callback); + return *this; +} + +AsyncMqttClient& AsyncMqttClient::onMessage(AsyncMqttClientInternals::OnMessageUserCallback callback) { + _onMessageUserCallbacks.push_back(callback); + return *this; +} + +AsyncMqttClient& AsyncMqttClient::onPublish(AsyncMqttClientInternals::OnPublishUserCallback callback) { + _onPublishUserCallbacks.push_back(callback); + return *this; +} + +void AsyncMqttClient::_freeCurrentParsedPacket() { + delete _currentParsedPacket; + _currentParsedPacket = nullptr; +} + +void AsyncMqttClient::_clear() { + _lastPingRequestTime = 0; + _freeCurrentParsedPacket(); + _clearQueue(true); // keep session data for now + + _parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::NONE; + + _client.setRxTimeout(0); +} + +/* TCP */ +void AsyncMqttClient::_onConnect() { + log_i("TCP conn, MQTT CONNECT"); +#if ASYNC_TCP_SSL_ENABLED + if (_secure && _secureServerFingerprints.size() > 0) { + SSL* clientSsl = _client.getSSL(); + + bool sslFoundFingerprint = false; + for (std::array fingerprint : _secureServerFingerprints) { + if (ssl_match_fingerprint(clientSsl, fingerprint.data()) == SSL_OK) { + sslFoundFingerprint = true; + break; + } + } + + if (!sslFoundFingerprint) { + _disconnectReason = AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT; + _client.close(true); + return; + } + } +#endif + AsyncMqttClientInternals::OutPacket* msg = + new AsyncMqttClientInternals::ConnectOutPacket(_cleanSession, + _username, + _password, + _willTopic, + _willRetain, + _willQos, + _willPayload, + _willPayloadLength, + _keepAlive, + _clientId); + _addFront(msg); + _handleQueue(); +} + +void AsyncMqttClient::_onDisconnect() { + log_i("TCP disconn"); + _state = DISCONNECTED; + + _clear(); + + for (auto callback : _onDisconnectUserCallbacks) callback(_disconnectReason); +} + +/* +void AsyncMqttClient::_onError(int8_t error) { + (void)error; + // _onDisconnect called anyway +} + +void AsyncMqttClient::_onTimeout() { + // disconnection will be handled by ping/pong management +} +*/ + +void AsyncMqttClient::_onAck(size_t len) { + log_i("ack %u", len); + _handleQueue(); +} + +void AsyncMqttClient::_onData(char* data, size_t len) { + log_i("data rcv (%u)", len); + size_t currentBytePosition = 0; + char currentByte; + _lastServerActivity = millis(); + do { + switch (_parsingInformation.bufferState) { + case AsyncMqttClientInternals::BufferState::NONE: + currentByte = data[currentBytePosition++]; + _parsingInformation.packetType = currentByte >> 4; + _parsingInformation.packetFlags = (currentByte << 4) >> 4; + _parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::REMAINING_LENGTH; + switch (_parsingInformation.packetType) { + case AsyncMqttClientInternals::PacketType.CONNACK: + log_i("rcv CONNACK"); + _currentParsedPacket = new AsyncMqttClientInternals::ConnAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onConnAck, this, std::placeholders::_1, std::placeholders::_2)); + _client.setRxTimeout(0); + break; + case AsyncMqttClientInternals::PacketType.PINGRESP: + log_i("rcv PINGRESP"); + _currentParsedPacket = new AsyncMqttClientInternals::PingRespPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPingResp, this)); + break; + case AsyncMqttClientInternals::PacketType.SUBACK: + log_i("rcv SUBACK"); + _currentParsedPacket = new AsyncMqttClientInternals::SubAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onSubAck, this, std::placeholders::_1, std::placeholders::_2)); + break; + case AsyncMqttClientInternals::PacketType.UNSUBACK: + log_i("rcv UNSUBACK"); + _currentParsedPacket = new AsyncMqttClientInternals::UnsubAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onUnsubAck, this, std::placeholders::_1)); + break; + case AsyncMqttClientInternals::PacketType.PUBLISH: + log_i("rcv PUBLISH"); + _currentParsedPacket = new AsyncMqttClientInternals::PublishPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6, std::placeholders::_7, std::placeholders::_8, std::placeholders::_9), std::bind(&AsyncMqttClient::_onPublish, this, std::placeholders::_1, std::placeholders::_2)); + break; + case AsyncMqttClientInternals::PacketType.PUBREL: + log_i("rcv PUBREL"); + _currentParsedPacket = new AsyncMqttClientInternals::PubRelPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubRel, this, std::placeholders::_1)); + break; + case AsyncMqttClientInternals::PacketType.PUBACK: + log_i("rcv PUBACK"); + _currentParsedPacket = new AsyncMqttClientInternals::PubAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubAck, this, std::placeholders::_1)); + break; + case AsyncMqttClientInternals::PacketType.PUBREC: + log_i("rcv PUBREC"); + _currentParsedPacket = new AsyncMqttClientInternals::PubRecPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubRec, this, std::placeholders::_1)); + break; + case AsyncMqttClientInternals::PacketType.PUBCOMP: + log_i("rcv PUBCOMP"); + _currentParsedPacket = new AsyncMqttClientInternals::PubCompPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubComp, this, std::placeholders::_1)); + break; + default: + log_i("rcv PROTOCOL VIOLATION"); + disconnect(true); + break; + } + break; + case AsyncMqttClientInternals::BufferState::REMAINING_LENGTH: + currentByte = data[currentBytePosition++]; + _remainingLengthBuffer[_remainingLengthBufferPosition++] = currentByte; + if (currentByte >> 7 == 0) { + _parsingInformation.remainingLength = AsyncMqttClientInternals::Helpers::decodeRemainingLength(_remainingLengthBuffer); + _remainingLengthBufferPosition = 0; + if (_parsingInformation.remainingLength > 0) { + _parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::VARIABLE_HEADER; + } else { + // PINGRESP is a special case where it has no variable header, so the packet ends right here + _parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::NONE; + _onPingResp(); + } + } + break; + case AsyncMqttClientInternals::BufferState::VARIABLE_HEADER: + _currentParsedPacket->parseVariableHeader(data, len, ¤tBytePosition); + break; + case AsyncMqttClientInternals::BufferState::PAYLOAD: + _currentParsedPacket->parsePayload(data, len, ¤tBytePosition); + break; + default: + currentBytePosition = len; + } + } while (currentBytePosition != len); +} + +void AsyncMqttClient::_onPoll() { + // if there is too much time the client has sent a ping request without a response, disconnect client to avoid half open connections + if (_lastPingRequestTime != 0 && (millis() - _lastPingRequestTime) >= (_keepAlive * 1000 * 2)) { + log_w("PING t/o, disconnecting"); + disconnect(true); + return; + } + // send ping to ensure the server will receive at least one message inside keepalive window + if (_state == CONNECTED && _lastPingRequestTime == 0 && (millis() - _lastClientActivity) >= (_keepAlive * 1000 * 0.7)) { + _sendPing(); + // send ping to verify if the server is still there (ensure this is not a half connection) + } else if (_state == CONNECTED && _lastPingRequestTime == 0 && (millis() - _lastServerActivity) >= (_keepAlive * 1000 * 0.7)) { + _sendPing(); + } + _handleQueue(); +} + +/* QUEUE */ + +void AsyncMqttClient::_insert(AsyncMqttClientInternals::OutPacket* packet) { + // We only use this for QoS2 PUBREL so there must be a PUBLISH packet present. + // The queue therefore cannot be empty and _head points to this PUBLISH packet. + SEMAPHORE_TAKE(); + log_i("new insert #%u", packet->packetType()); + packet->next = _head->next; + _head->next = packet; + if (_head == _tail) { // PUB packet is the only one in the queue + _tail = packet; + } + SEMAPHORE_GIVE(); + _handleQueue(); +} + +void AsyncMqttClient::_addFront(AsyncMqttClientInternals::OutPacket* packet) { + // This is only used for the CONNECT packet, to be able to establish a connection + // before anything else. The queue can be empty or has packets from the continued session. + // In both cases, _head should always point to the CONNECT packet afterwards. + SEMAPHORE_TAKE(); + log_i("new front #%u", packet->packetType()); + if (_head == nullptr) { + _tail = packet; + } else { + packet->next = _head; + } + _head = packet; + SEMAPHORE_GIVE(); + _handleQueue(); +} + +void AsyncMqttClient::_addBack(AsyncMqttClientInternals::OutPacket* packet) { + SEMAPHORE_TAKE(); + log_i("new back #%u", packet->packetType()); + if (!_tail) { + _head = packet; + } else { + _tail->next = packet; + } + _tail = packet; + _tail->next = nullptr; + SEMAPHORE_GIVE(); + _handleQueue(); +} + +void AsyncMqttClient::_handleQueue() { + SEMAPHORE_TAKE(); + // On ESP32, onDisconnect is called within the close()-call. So we need to make sure we don't lock + bool disconnect = false; + + while (_head && _client.space() > 10) { // safe but arbitrary value, send at least 10 bytes + // 1. try to send + if (_head->size() > _sent) { + // On SSL the TCP library returns the total amount of bytes, not just the unencrypted payload length. + // So we calculate the amount to be written ourselves. + size_t willSend = std::min(_head->size() - _sent, _client.space()); + size_t realSent = _client.add(reinterpret_cast(_head->data(_sent)), willSend, ASYNC_WRITE_FLAG_COPY); // flag is set by LWIP anyway, added for clarity + _sent += willSend; + (void)realSent; + _client.send(); + _lastClientActivity = millis(); + _lastPingRequestTime = 0; + #if ASYNC_TCP_SSL_ENABLED + log_i("snd #%u: (tls: %u) %u/%u", _head->packetType(), realSent, _sent, _head->size()); + #else + log_i("snd #%u: %u/%u", _head->packetType(), _sent, _head->size()); + #endif + if (_head->packetType() == AsyncMqttClientInternals::PacketType.DISCONNECT) { + disconnect = true; + } + } + + // 2. stop processing when we have to wait for an MQTT acknowledgment + if (_head->size() == _sent) { + if (_head->released()) { + log_i("p #%d rel", _head->packetType()); + AsyncMqttClientInternals::OutPacket* tmp = _head; + _head = _head->next; + if (!_head) _tail = nullptr; + delete tmp; + _sent = 0; + } else { + break; // sending is complete however send next only after mqtt confirmation + } + } + } + + SEMAPHORE_GIVE(); + if (disconnect) { + log_i("snd DISCONN, disconnecting"); + _client.close(); + } +} + +void AsyncMqttClient::_clearQueue(bool keepSessionData) { + SEMAPHORE_TAKE(); + AsyncMqttClientInternals::OutPacket* packet = _head; + _head = nullptr; + _tail = nullptr; + + while (packet) { + /* MQTT spec 3.1.2.4 Clean Session: + * - QoS 1 and QoS 2 messages which have been sent to the Server, but have not been completely acknowledged. + * - QoS 2 messages which have been received from the Server, but have not been completely acknowledged. + * + (unsent PUB messages with QoS > 0) + * + * To be kept: + * - possibly first message (sent to server but not acked) + * - PUBREC messages (QoS 2 PUB received but not acked) + * - PUBCOMP messages (QoS 2 PUBREL received but not acked) + */ + if (keepSessionData) { + if (packet->qos() > 0 && packet->size() <= _sent) { // check for qos includes check for PUB-packet type + reinterpret_cast(packet)->setDup(); + AsyncMqttClientInternals::OutPacket* next = packet->next; + log_i("keep #%u", packet->packetType()); + SEMAPHORE_GIVE(); + _addBack(packet); + SEMAPHORE_TAKE(); + packet = next; + } else if (packet->qos() > 0 || + packet->packetType() == AsyncMqttClientInternals::PacketType.PUBREC || + packet->packetType() == AsyncMqttClientInternals::PacketType.PUBCOMP) { + AsyncMqttClientInternals::OutPacket* next = packet->next; + log_i("keep #%u", packet->packetType()); + SEMAPHORE_GIVE(); + _addBack(packet); + SEMAPHORE_TAKE(); + packet = next; + } else { + AsyncMqttClientInternals::OutPacket* next = packet->next; + delete packet; + packet = next; + } + /* Delete everything when not keeping session data + */ + } else { + AsyncMqttClientInternals::OutPacket* next = packet->next; + delete packet; + packet = next; + } + } + _sent = 0; + SEMAPHORE_GIVE(); +} + +/* MQTT */ +void AsyncMqttClient::_onPingResp() { + log_i("PINGRESP"); + _freeCurrentParsedPacket(); + _lastPingRequestTime = 0; +} + +void AsyncMqttClient::_onConnAck(bool sessionPresent, uint8_t connectReturnCode) { + log_i("CONNACK"); + _freeCurrentParsedPacket(); + + if (!sessionPresent) { + _pendingPubRels.clear(); + _pendingPubRels.shrink_to_fit(); + _clearQueue(false); // remove session data + } + + if (connectReturnCode == 0) { + _state = CONNECTED; + for (auto callback : _onConnectUserCallbacks) callback(sessionPresent); + } else { + // Callbacks are handled by the onDisconnect function which is called from the AsyncTcp lib + _disconnectReason = static_cast(connectReturnCode); + return; + } + _handleQueue(); // send any remaining data from continued session +} + +void AsyncMqttClient::_onSubAck(uint16_t packetId, char status) { + log_i("SUBACK"); + _freeCurrentParsedPacket(); + SEMAPHORE_TAKE(); + if (_head && _head->packetId() == packetId) { + _head->release(); + log_i("SUB released"); + } + SEMAPHORE_GIVE(); + + for (auto callback : _onSubscribeUserCallbacks) callback(packetId, status); + + _handleQueue(); // subscribe confirmed, ready to send next queued item +} + +void AsyncMqttClient::_onUnsubAck(uint16_t packetId) { + log_i("UNSUBACK"); + _freeCurrentParsedPacket(); + SEMAPHORE_TAKE(); + if (_head && _head->packetId() == packetId) { + _head->release(); + log_i("UNSUB released"); + } + SEMAPHORE_GIVE(); + + for (auto callback : _onUnsubscribeUserCallbacks) callback(packetId); + + _handleQueue(); // unsubscribe confirmed, ready to send next queued item +} + +void AsyncMqttClient::_onMessage(char* topic, char* payload, uint8_t qos, bool dup, bool retain, size_t len, size_t index, size_t total, uint16_t packetId) { + bool notifyPublish = true; + + if (qos == 2) { + for (AsyncMqttClientInternals::PendingPubRel pendingPubRel : _pendingPubRels) { + if (pendingPubRel.packetId == packetId) { + notifyPublish = false; + break; + } + } + } + + if (notifyPublish) { + AsyncMqttClientMessageProperties properties; + properties.qos = qos; + properties.dup = dup; + properties.retain = retain; + + for (auto callback : _onMessageUserCallbacks) callback(topic, payload, properties, len, index, total); + } +} + +void AsyncMqttClient::_onPublish(uint16_t packetId, uint8_t qos) { + AsyncMqttClientInternals::PendingAck pendingAck; + + if (qos == 1) { + pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBACK; + pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBACK_RESERVED; + pendingAck.packetId = packetId; + AsyncMqttClientInternals::OutPacket* msg = new AsyncMqttClientInternals::PubAckOutPacket(pendingAck); + _addBack(msg); + } else if (qos == 2) { + pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBREC; + pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBREC_RESERVED; + pendingAck.packetId = packetId; + AsyncMqttClientInternals::OutPacket* msg = new AsyncMqttClientInternals::PubAckOutPacket(pendingAck); + _addBack(msg); + + bool pubRelAwaiting = false; + for (AsyncMqttClientInternals::PendingPubRel pendingPubRel : _pendingPubRels) { + if (pendingPubRel.packetId == packetId) { + pubRelAwaiting = true; + break; + } + } + + if (!pubRelAwaiting) { + AsyncMqttClientInternals::PendingPubRel pendingPubRel; + pendingPubRel.packetId = packetId; + _pendingPubRels.push_back(pendingPubRel); + } + } + + _freeCurrentParsedPacket(); +} + +void AsyncMqttClient::_onPubRel(uint16_t packetId) { + _freeCurrentParsedPacket(); + + AsyncMqttClientInternals::PendingAck pendingAck; + pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBCOMP; + pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBCOMP_RESERVED; + pendingAck.packetId = packetId; + if (_head && _head->packetId() == packetId) { + AsyncMqttClientInternals::OutPacket* msg = new AsyncMqttClientInternals::PubAckOutPacket(pendingAck); + _head->release(); + _insert(msg); + log_i("PUBREC released"); + } + + for (size_t i = 0; i < _pendingPubRels.size(); i++) { + if (_pendingPubRels[i].packetId == packetId) { + _pendingPubRels.erase(_pendingPubRels.begin() + i); + _pendingPubRels.shrink_to_fit(); + } + } +} + +void AsyncMqttClient::_onPubAck(uint16_t packetId) { + _freeCurrentParsedPacket(); + if (_head && _head->packetId() == packetId) { + _head->release(); + log_i("PUB released"); + } + + for (auto callback : _onPublishUserCallbacks) callback(packetId); +} + +void AsyncMqttClient::_onPubRec(uint16_t packetId) { + _freeCurrentParsedPacket(); + + // We will only be sending 1 QoS>0 PUB message at a time (to honor message + // ordering). So no need to store ACKS in a separate container as it will + // be stored in the outgoing queue until a PUBCOMP comes in. + AsyncMqttClientInternals::PendingAck pendingAck; + pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBREL; + pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBREL_RESERVED; + pendingAck.packetId = packetId; + log_i("snd PUBREL"); + + AsyncMqttClientInternals::OutPacket* msg = new AsyncMqttClientInternals::PubAckOutPacket(pendingAck); + if (_head && _head->packetId() == packetId) { + _head->release(); + log_i("PUB released"); + } + _insert(msg); +} + +void AsyncMqttClient::_onPubComp(uint16_t packetId) { + _freeCurrentParsedPacket(); + + // _head points to the PUBREL package + if (_head && _head->packetId() == packetId) { + _head->release(); + log_i("PUBREL released"); + } + + for (auto callback : _onPublishUserCallbacks) callback(packetId); +} + +void AsyncMqttClient::_sendPing() { + log_i("PING"); + _lastPingRequestTime = millis(); + AsyncMqttClientInternals::OutPacket* msg = new AsyncMqttClientInternals::PingReqOutPacket; + _addBack(msg); +} + +bool AsyncMqttClient::connected() const { + return _state == CONNECTED; +} + +void AsyncMqttClient::connect() { + if (_state != DISCONNECTED) return; + log_i("CONNECTING"); + _state = CONNECTING; + _disconnectReason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED; // reset any previous + + _client.setRxTimeout(_keepAlive); + +#if ASYNC_TCP_SSL_ENABLED + if (_useIp) { + _client.connect(_ip, _port, _secure); + } else { + _client.connect(_host, _port, _secure); + } +#else + if (_useIp) { + _client.connect(_ip, _port); + } else { + _client.connect(_host, _port); + } +#endif +} + +void AsyncMqttClient::disconnect(bool force) { + if (_state == DISCONNECTED) return; + log_i("DISCONNECT (f:%d)", force); + if (force) { + _state = DISCONNECTED; + _client.close(true); + } else if (_state != DISCONNECTING) { + _state = DISCONNECTING; + AsyncMqttClientInternals::OutPacket* msg = new AsyncMqttClientInternals::DisconnOutPacket; + _addBack(msg); + } +} + +uint16_t AsyncMqttClient::subscribe(const char* topic, uint8_t qos) { + if (_state != CONNECTED) return 0; + log_i("SUBSCRIBE"); + + AsyncMqttClientInternals::OutPacket* msg = new AsyncMqttClientInternals::SubscribeOutPacket(topic, qos); + _addBack(msg); + return msg->packetId(); +} + +uint16_t AsyncMqttClient::unsubscribe(const char* topic) { + if (_state != CONNECTED) return 0; + log_i("UNSUBSCRIBE"); + + AsyncMqttClientInternals::OutPacket* msg = new AsyncMqttClientInternals::UnsubscribeOutPacket(topic); + _addBack(msg); + return msg->packetId(); +} + +uint16_t AsyncMqttClient::publish(const char* topic, uint8_t qos, bool retain, const char* payload, size_t length, bool dup, uint16_t message_id) { + if (_state != CONNECTED || GET_FREE_MEMORY() < MQTT_MIN_FREE_MEMORY) return 0; + log_i("PUBLISH"); + + AsyncMqttClientInternals::OutPacket* msg = new AsyncMqttClientInternals::PublishOutPacket(topic, qos, retain, payload, length); + _addBack(msg); + return msg->packetId(); +} + +bool AsyncMqttClient::clearQueue() { + if (_state != DISCONNECTED) return false; + _clearQueue(false); + return true; +} + +const char* AsyncMqttClient::getClientId() const { + return _clientId; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient.h b/yoRadio/src/async-mqtt-client/AsyncMqttClient.h new file mode 100644 index 0000000..23d3055 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient.h @@ -0,0 +1,6 @@ +#ifndef SRC_ASYNCMQTTCLIENT_H_ +#define SRC_ASYNCMQTTCLIENT_H_ + +#include "AsyncMqttClient.hpp" + +#endif // SRC_ASYNCMQTTCLIENT_H_ diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient.hpp new file mode 100644 index 0000000..95931f1 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient.hpp @@ -0,0 +1,179 @@ +#pragma once + +#include +#include + +#include "Arduino.h" + +#ifndef MQTT_MIN_FREE_MEMORY +#define MQTT_MIN_FREE_MEMORY 4096 +#endif + +#ifdef ESP32 +#include "../AsyncWebServer/AsyncTCP.h" +#include +#elif defined(ESP8266) +#include +#else +#error Platform not supported +#endif + +#if ASYNC_TCP_SSL_ENABLED +#include +#define SHA1_SIZE 20 +#endif + +#include "AsyncMqttClient/Flags.hpp" +#include "AsyncMqttClient/ParsingInformation.hpp" +#include "AsyncMqttClient/MessageProperties.hpp" +#include "AsyncMqttClient/Helpers.hpp" +#include "AsyncMqttClient/Callbacks.hpp" +#include "AsyncMqttClient/DisconnectReasons.hpp" +#include "AsyncMqttClient/Storage.hpp" + +#include "AsyncMqttClient/Packets/Packet.hpp" +#include "AsyncMqttClient/Packets/ConnAckPacket.hpp" +#include "AsyncMqttClient/Packets/PingRespPacket.hpp" +#include "AsyncMqttClient/Packets/SubAckPacket.hpp" +#include "AsyncMqttClient/Packets/UnsubAckPacket.hpp" +#include "AsyncMqttClient/Packets/PublishPacket.hpp" +#include "AsyncMqttClient/Packets/PubRelPacket.hpp" +#include "AsyncMqttClient/Packets/PubAckPacket.hpp" +#include "AsyncMqttClient/Packets/PubRecPacket.hpp" +#include "AsyncMqttClient/Packets/PubCompPacket.hpp" + +#include "AsyncMqttClient/Packets/Out/Connect.hpp" +#include "AsyncMqttClient/Packets/Out/PingReq.hpp" +#include "AsyncMqttClient/Packets/Out/PubAck.hpp" +#include "AsyncMqttClient/Packets/Out/Disconn.hpp" +#include "AsyncMqttClient/Packets/Out/Subscribe.hpp" +#include "AsyncMqttClient/Packets/Out/Unsubscribe.hpp" +#include "AsyncMqttClient/Packets/Out/Publish.hpp" + +class AsyncMqttClient { + public: + AsyncMqttClient(); + ~AsyncMqttClient(); + + AsyncMqttClient& setKeepAlive(uint16_t keepAlive); + AsyncMqttClient& setClientId(const char* clientId); + AsyncMqttClient& setCleanSession(bool cleanSession); + AsyncMqttClient& setMaxTopicLength(uint16_t maxTopicLength); + AsyncMqttClient& setCredentials(const char* username, const char* password = nullptr); + AsyncMqttClient& setWill(const char* topic, uint8_t qos, bool retain, const char* payload = nullptr, size_t length = 0); + AsyncMqttClient& setServer(IPAddress ip, uint16_t port); + AsyncMqttClient& setServer(const char* host, uint16_t port); +#if ASYNC_TCP_SSL_ENABLED + AsyncMqttClient& setSecure(bool secure); + AsyncMqttClient& addServerFingerprint(const uint8_t* fingerprint); +#endif + + AsyncMqttClient& onConnect(AsyncMqttClientInternals::OnConnectUserCallback callback); + AsyncMqttClient& onDisconnect(AsyncMqttClientInternals::OnDisconnectUserCallback callback); + AsyncMqttClient& onSubscribe(AsyncMqttClientInternals::OnSubscribeUserCallback callback); + AsyncMqttClient& onUnsubscribe(AsyncMqttClientInternals::OnUnsubscribeUserCallback callback); + AsyncMqttClient& onMessage(AsyncMqttClientInternals::OnMessageUserCallback callback); + AsyncMqttClient& onPublish(AsyncMqttClientInternals::OnPublishUserCallback callback); + + bool connected() const; + void connect(); + void disconnect(bool force = false); + uint16_t subscribe(const char* topic, uint8_t qos); + uint16_t unsubscribe(const char* topic); + uint16_t publish(const char* topic, uint8_t qos, bool retain, const char* payload = nullptr, size_t length = 0, bool dup = false, uint16_t message_id = 0); + bool clearQueue(); // Not MQTT compliant! + + const char* getClientId() const; + + private: + AsyncClient _client; + AsyncMqttClientInternals::OutPacket* _head; + AsyncMqttClientInternals::OutPacket* _tail; + size_t _sent; + enum { + CONNECTING, + CONNECTED, + DISCONNECTING, + DISCONNECTED + } _state; + AsyncMqttClientDisconnectReason _disconnectReason; + uint32_t _lastClientActivity; + uint32_t _lastServerActivity; + uint32_t _lastPingRequestTime; + + char _generatedClientId[18 + 1]; // esp8266-abc123 and esp32-abcdef123456 + IPAddress _ip; + const char* _host; + bool _useIp; +#if ASYNC_TCP_SSL_ENABLED + bool _secure; +#endif + uint16_t _port; + uint16_t _keepAlive; + bool _cleanSession; + const char* _clientId; + const char* _username; + const char* _password; + const char* _willTopic; + const char* _willPayload; + uint16_t _willPayloadLength; + uint8_t _willQos; + bool _willRetain; + +#if ASYNC_TCP_SSL_ENABLED + std::vector> _secureServerFingerprints; +#endif + + std::vector _onConnectUserCallbacks; + std::vector _onDisconnectUserCallbacks; + std::vector _onSubscribeUserCallbacks; + std::vector _onUnsubscribeUserCallbacks; + std::vector _onMessageUserCallbacks; + std::vector _onPublishUserCallbacks; + + AsyncMqttClientInternals::ParsingInformation _parsingInformation; + AsyncMqttClientInternals::Packet* _currentParsedPacket; + uint8_t _remainingLengthBufferPosition; + char _remainingLengthBuffer[4]; + + std::vector _pendingPubRels; + +#if defined(ESP32) + SemaphoreHandle_t _xSemaphore = nullptr; +#elif defined(ESP8266) + bool _xSemaphore = false; +#endif + + void _clear(); + void _freeCurrentParsedPacket(); + + // TCP + void _onConnect(); + void _onDisconnect(); + // void _onError(int8_t error); + // void _onTimeout(); + void _onAck(size_t len); + void _onData(char* data, size_t len); + void _onPoll(); + + // QUEUE + void _insert(AsyncMqttClientInternals::OutPacket* packet); // for PUBREL + void _addFront(AsyncMqttClientInternals::OutPacket* packet); // for CONNECT + void _addBack(AsyncMqttClientInternals::OutPacket* packet); // all the rest + void _handleQueue(); + void _clearQueue(bool keepSessionData); + + // MQTT + void _onPingResp(); + void _onConnAck(bool sessionPresent, uint8_t connectReturnCode); + void _onSubAck(uint16_t packetId, char status); + void _onUnsubAck(uint16_t packetId); + void _onMessage(char* topic, char* payload, uint8_t qos, bool dup, bool retain, size_t len, size_t index, size_t total, uint16_t packetId); + void _onPublish(uint16_t packetId, uint8_t qos); + void _onPubRel(uint16_t packetId); + void _onPubAck(uint16_t packetId); + void _onPubRec(uint16_t packetId); + void _onPubComp(uint16_t packetId); + + void _sendPing(); +}; diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Callbacks.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Callbacks.hpp new file mode 100644 index 0000000..414034d --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Callbacks.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include "DisconnectReasons.hpp" +#include "MessageProperties.hpp" +#include "Errors.hpp" + +namespace AsyncMqttClientInternals { +// user callbacks +typedef std::function OnConnectUserCallback; +typedef std::function OnDisconnectUserCallback; +typedef std::function OnSubscribeUserCallback; +typedef std::function OnUnsubscribeUserCallback; +typedef std::function OnMessageUserCallback; +typedef std::function OnPublishUserCallback; +typedef std::function OnErrorUserCallback; + +// internal callbacks +typedef std::function OnConnAckInternalCallback; +typedef std::function OnPingRespInternalCallback; +typedef std::function OnSubAckInternalCallback; +typedef std::function OnUnsubAckInternalCallback; +typedef std::function OnMessageInternalCallback; +typedef std::function OnPublishInternalCallback; +typedef std::function OnPubRelInternalCallback; +typedef std::function OnPubAckInternalCallback; +typedef std::function OnPubRecInternalCallback; +typedef std::function OnPubCompInternalCallback; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/DisconnectReasons.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/DisconnectReasons.hpp new file mode 100644 index 0000000..a151140 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/DisconnectReasons.hpp @@ -0,0 +1,15 @@ +#pragma once + +enum class AsyncMqttClientDisconnectReason : uint8_t { + TCP_DISCONNECTED = 0, + + MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1, + MQTT_IDENTIFIER_REJECTED = 2, + MQTT_SERVER_UNAVAILABLE = 3, + MQTT_MALFORMED_CREDENTIALS = 4, + MQTT_NOT_AUTHORIZED = 5, + + ESP8266_NOT_ENOUGH_SPACE = 6, + + TLS_BAD_FINGERPRINT = 7 +}; diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Errors.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Errors.hpp new file mode 100644 index 0000000..f93e80e --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Errors.hpp @@ -0,0 +1,6 @@ +#pragma once + +enum class AsyncMqttClientError : uint8_t { + MAX_RETRIES = 0, + OUT_OF_MEMORY = 1 +}; diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Flags.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Flags.hpp new file mode 100644 index 0000000..a1fb3e3 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Flags.hpp @@ -0,0 +1,57 @@ +#pragma once + +namespace AsyncMqttClientInternals { +constexpr struct { + const uint8_t RESERVED = 0; + const uint8_t CONNECT = 1; + const uint8_t CONNACK = 2; + const uint8_t PUBLISH = 3; + const uint8_t PUBACK = 4; + const uint8_t PUBREC = 5; + const uint8_t PUBREL = 6; + const uint8_t PUBCOMP = 7; + const uint8_t SUBSCRIBE = 8; + const uint8_t SUBACK = 9; + const uint8_t UNSUBSCRIBE = 10; + const uint8_t UNSUBACK = 11; + const uint8_t PINGREQ = 12; + const uint8_t PINGRESP = 13; + const uint8_t DISCONNECT = 14; + const uint8_t RESERVED2 = 1; +} PacketType; + +constexpr struct { + const uint8_t CONNECT_RESERVED = 0x00; + const uint8_t CONNACK_RESERVED = 0x00; + const uint8_t PUBLISH_DUP = 0x08; + const uint8_t PUBLISH_QOS0 = 0x00; + const uint8_t PUBLISH_QOS1 = 0x02; + const uint8_t PUBLISH_QOS2 = 0x04; + const uint8_t PUBLISH_QOSRESERVED = 0x06; + const uint8_t PUBLISH_RETAIN = 0x01; + const uint8_t PUBACK_RESERVED = 0x00; + const uint8_t PUBREC_RESERVED = 0x00; + const uint8_t PUBREL_RESERVED = 0x02; + const uint8_t PUBCOMP_RESERVED = 0x00; + const uint8_t SUBSCRIBE_RESERVED = 0x02; + const uint8_t SUBACK_RESERVED = 0x00; + const uint8_t UNSUBSCRIBE_RESERVED = 0x02; + const uint8_t UNSUBACK_RESERVED = 0x00; + const uint8_t PINGREQ_RESERVED = 0x00; + const uint8_t PINGRESP_RESERVED = 0x00; + const uint8_t DISCONNECT_RESERVED = 0x00; + const uint8_t RESERVED2_RESERVED = 0x00; +} HeaderFlag; + +constexpr struct { + const uint8_t USERNAME = 0x80; + const uint8_t PASSWORD = 0x40; + const uint8_t WILL_RETAIN = 0x20; + const uint8_t WILL_QOS0 = 0x00; + const uint8_t WILL_QOS1 = 0x08; + const uint8_t WILL_QOS2 = 0x10; + const uint8_t WILL = 0x04; + const uint8_t CLEAN_SESSION = 0x02; + const uint8_t RESERVED = 0x00; +} ConnectFlag; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Helpers.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Helpers.hpp new file mode 100644 index 0000000..ecb620f --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Helpers.hpp @@ -0,0 +1,61 @@ +#pragma once + +namespace AsyncMqttClientInternals { +class Helpers { + public: + static uint32_t decodeRemainingLength(char* bytes) { + uint32_t multiplier = 1; + uint32_t value = 0; + uint8_t currentByte = 0; + uint8_t encodedByte; + do { + encodedByte = bytes[currentByte++]; + value += (encodedByte & 127) * multiplier; + multiplier *= 128; + } while ((encodedByte & 128) != 0); + + return value; + } + + static uint8_t encodeRemainingLength(uint32_t remainingLength, char* destination) { + uint8_t currentByte = 0; + uint8_t bytesNeeded = 0; + + do { + uint8_t encodedByte = remainingLength % 128; + remainingLength /= 128; + if (remainingLength > 0) { + encodedByte = encodedByte | 128; + } + + destination[currentByte++] = encodedByte; + bytesNeeded++; + } while (remainingLength > 0); + + return bytesNeeded; + } +}; + +#if defined(ARDUINO_ARCH_ESP32) + #define SEMAPHORE_TAKE() xSemaphoreTake(_xSemaphore, portMAX_DELAY) + #define SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) + #define GET_FREE_MEMORY() ESP.getMaxAllocHeap() + #include +#elif defined(ARDUINO_ARCH_ESP8266) + #define SEMAPHORE_TAKE(X) while (_xSemaphore) { /*ESP.wdtFeed();*/ } _xSemaphore = true + #define SEMAPHORE_GIVE() _xSemaphore = false + #define GET_FREE_MEMORY() ESP.getMaxFreeBlockSize() + #if defined(DEBUG_ESP_PORT) && defined(DEBUG_ASYNC_MQTT_CLIENT) + #define log_i(...) DEBUG_ESP_PORT.printf(__VA_ARGS__); DEBUG_ESP_PORT.print("\n") + #define log_e(...) DEBUG_ESP_PORT.printf(__VA_ARGS__); DEBUG_ESP_PORT.print("\n") + #define log_w(...) DEBUG_ESP_PORT.printf(__VA_ARGS__); DEBUG_ESP_PORT.print("\n") + #else + #define log_i(...) + #define log_e(...) + #define log_w(...) + #endif +#else + #pragma error "No valid architecture" +#endif + +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/MessageProperties.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/MessageProperties.hpp new file mode 100644 index 0000000..c04b596 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/MessageProperties.hpp @@ -0,0 +1,7 @@ +#pragma once + +struct AsyncMqttClientMessageProperties { + uint8_t qos; + bool dup; + bool retain; +}; diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.cpp new file mode 100644 index 0000000..fb4b971 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.cpp @@ -0,0 +1,30 @@ +#include "ConnAckPacket.hpp" + +using AsyncMqttClientInternals::ConnAckPacket; + +ConnAckPacket::ConnAckPacket(ParsingInformation* parsingInformation, OnConnAckInternalCallback callback) +: _parsingInformation(parsingInformation) +, _callback(callback) +, _bytePosition(0) +, _sessionPresent(false) +, _connectReturnCode(0) { +} + +ConnAckPacket::~ConnAckPacket() { +} + +void ConnAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { + char currentByte = data[(*currentBytePosition)++]; + if (_bytePosition++ == 0) { + _sessionPresent = (currentByte << 7) >> 7; + } else { + _connectReturnCode = currentByte; + _parsingInformation->bufferState = BufferState::NONE; + _callback(_sessionPresent, _connectReturnCode); + } +} + +void ConnAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { + (void)data; + (void)currentBytePosition; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.hpp new file mode 100644 index 0000000..3dcd162 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/ConnAckPacket.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Arduino.h" +#include "Packet.hpp" +#include "../ParsingInformation.hpp" +#include "../Callbacks.hpp" + +namespace AsyncMqttClientInternals { +class ConnAckPacket : public Packet { + public: + explicit ConnAckPacket(ParsingInformation* parsingInformation, OnConnAckInternalCallback callback); + ~ConnAckPacket(); + + void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); + void parsePayload(char* data, size_t len, size_t* currentBytePosition); + + private: + ParsingInformation* _parsingInformation; + OnConnAckInternalCallback _callback; + + uint8_t _bytePosition; + bool _sessionPresent; + uint8_t _connectReturnCode; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Connect.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Connect.cpp new file mode 100644 index 0000000..a9a86e4 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Connect.cpp @@ -0,0 +1,162 @@ +#include "Connect.hpp" + +using AsyncMqttClientInternals::ConnectOutPacket; + +ConnectOutPacket::ConnectOutPacket(bool cleanSession, + const char* username, + const char* password, + const char* willTopic, + bool willRetain, + uint8_t willQos, + const char* willPayload, + uint16_t willPayloadLength, + uint16_t keepAlive, + const char* clientId) { + char fixedHeader[5]; + fixedHeader[0] = AsyncMqttClientInternals::PacketType.CONNECT; + fixedHeader[0] = fixedHeader[0] << 4; + fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.CONNECT_RESERVED; + + uint16_t protocolNameLength = 4; + char protocolNameLengthBytes[2]; + protocolNameLengthBytes[0] = protocolNameLength >> 8; + protocolNameLengthBytes[1] = protocolNameLength & 0xFF; + + char protocolLevel[1]; + protocolLevel[0] = 0x04; + + char connectFlags[1]; + connectFlags[0] = 0; + if (cleanSession) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.CLEAN_SESSION; + if (username != nullptr) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.USERNAME; + if (password != nullptr) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.PASSWORD; + if (willTopic != nullptr) { + connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL; + if (willRetain) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_RETAIN; + switch (willQos) { + case 0: + connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_QOS0; + break; + case 1: + connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_QOS1; + break; + case 2: + connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_QOS2; + break; + } + } + + char keepAliveBytes[2]; + keepAliveBytes[0] = keepAlive >> 8; + keepAliveBytes[1] = keepAlive & 0xFF; + + uint16_t clientIdLength = strlen(clientId); + char clientIdLengthBytes[2]; + clientIdLengthBytes[0] = clientIdLength >> 8; + clientIdLengthBytes[1] = clientIdLength & 0xFF; + + // Optional fields + uint16_t willTopicLength = 0; + char willTopicLengthBytes[2]; + char willPayloadLengthBytes[2]; + if (willTopic != nullptr) { + willTopicLength = strlen(willTopic); + willTopicLengthBytes[0] = willTopicLength >> 8; + willTopicLengthBytes[1] = willTopicLength & 0xFF; + + if (willPayload != nullptr && willPayloadLength == 0) willPayloadLength = strlen(willPayload); + + willPayloadLengthBytes[0] = willPayloadLength >> 8; + willPayloadLengthBytes[1] = willPayloadLength & 0xFF; + } + + uint16_t usernameLength = 0; + char usernameLengthBytes[2]; + if (username != nullptr) { + usernameLength = strlen(username); + usernameLengthBytes[0] = usernameLength >> 8; + usernameLengthBytes[1] = usernameLength & 0xFF; + } + + uint16_t passwordLength = 0; + char passwordLengthBytes[2]; + if (password != nullptr) { + passwordLength = strlen(password); + passwordLengthBytes[0] = passwordLength >> 8; + passwordLengthBytes[1] = passwordLength & 0xFF; + } + + uint32_t remainingLength = 2 + protocolNameLength + 1 + 1 + 2 + 2 + clientIdLength; // always present + if (willTopic != nullptr) remainingLength += 2 + willTopicLength + 2 + willPayloadLength; + if (username != nullptr) remainingLength += 2 + usernameLength; + if (password != nullptr) remainingLength += 2 + passwordLength; + uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(remainingLength, fixedHeader + 1); + + uint32_t neededSpace = 1 + remainingLengthLength; + neededSpace += 2; + neededSpace += protocolNameLength; + neededSpace += 1; + neededSpace += 1; + neededSpace += 2; + neededSpace += 2; + neededSpace += clientIdLength; + if (willTopic != nullptr) { + neededSpace += 2; + neededSpace += willTopicLength; + + neededSpace += 2; + if (willPayload != nullptr) neededSpace += willPayloadLength; + } + if (username != nullptr) { + neededSpace += 2; + neededSpace += usernameLength; + } + if (password != nullptr) { + neededSpace += 2; + neededSpace += passwordLength; + } + + _data.reserve(neededSpace); + + _data.insert(_data.end(), fixedHeader, fixedHeader + 1 + remainingLengthLength); + + _data.push_back(protocolNameLengthBytes[0]); + _data.push_back(protocolNameLengthBytes[1]); + + _data.push_back('M'); + _data.push_back('Q'); + _data.push_back('T'); + _data.push_back('T'); + + _data.push_back(protocolLevel[0]); + _data.push_back(connectFlags[0]); + _data.push_back(keepAliveBytes[0]); + _data.push_back(keepAliveBytes[1]); + _data.push_back(clientIdLengthBytes[0]); + _data.push_back(clientIdLengthBytes[1]); + + _data.insert(_data.end(), clientId, clientId + clientIdLength); + if (willTopic != nullptr) { + _data.insert(_data.end(), willTopicLengthBytes, willTopicLengthBytes + 2); + _data.insert(_data.end(), willTopic, willTopic + willTopicLength); + + _data.insert(_data.end(), willPayloadLengthBytes, willPayloadLengthBytes + 2); + if (willPayload != nullptr) _data.insert(_data.end(), willPayload, willPayload + willPayloadLength); + } + if (username != nullptr) { + _data.insert(_data.end(), usernameLengthBytes, usernameLengthBytes + 2); + _data.insert(_data.end(), username, username + usernameLength); + } + if (password != nullptr) { + _data.insert(_data.end(), passwordLengthBytes, passwordLengthBytes + 2); + _data.insert(_data.end(), password, password + passwordLength); + } +} + +const uint8_t* ConnectOutPacket::data(size_t index) const { + return &_data.data()[index]; +} + +size_t ConnectOutPacket::size() const { + return _data.size(); +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Connect.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Connect.hpp new file mode 100644 index 0000000..5b17632 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Connect.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include // strlen + +#include "OutPacket.hpp" +#include "../../Flags.hpp" +#include "../../Helpers.hpp" + +namespace AsyncMqttClientInternals { +class ConnectOutPacket : public OutPacket { + public: + ConnectOutPacket(bool cleanSession, + const char* username, + const char* password, + const char* willTopic, + bool willRetain, + uint8_t willQos, + const char* willPayload, + uint16_t willPayloadLength, + uint16_t keepAlive, + const char* clientId); + const uint8_t* data(size_t index = 0) const; + size_t size() const; + + private: + std::vector _data; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Disconn.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Disconn.cpp new file mode 100644 index 0000000..3e2890d --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Disconn.cpp @@ -0,0 +1,18 @@ +#include "Disconn.hpp" + +using AsyncMqttClientInternals::DisconnOutPacket; + +DisconnOutPacket::DisconnOutPacket() { + _data[0] = AsyncMqttClientInternals::PacketType.DISCONNECT; + _data[0] = _data[0] << 4; + _data[0] = _data[0] | AsyncMqttClientInternals::HeaderFlag.DISCONNECT_RESERVED; + _data[1] = 0; +} + +const uint8_t* DisconnOutPacket::data(size_t index) const { + return &_data[index]; +} + +size_t DisconnOutPacket::size() const { + return 2; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Disconn.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Disconn.hpp new file mode 100644 index 0000000..38dc915 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Disconn.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "OutPacket.hpp" +#include "../../Flags.hpp" +#include "../../Helpers.hpp" + +namespace AsyncMqttClientInternals { +class DisconnOutPacket : public OutPacket { + public: + DisconnOutPacket(); + const uint8_t* data(size_t index = 0) const; + size_t size() const; + + private: + uint8_t _data[2]; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/OutPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/OutPacket.cpp new file mode 100644 index 0000000..e69a87f --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/OutPacket.cpp @@ -0,0 +1,44 @@ +#include "OutPacket.hpp" + +using AsyncMqttClientInternals::OutPacket; + +OutPacket::OutPacket() +: next(nullptr) +, timeout(0) +, noTries(0) +, _released(true) +, _packetId(0) {} + +OutPacket::~OutPacket() {} + +bool OutPacket::released() const { + return _released; +} + +uint8_t OutPacket::packetType() const { + return data(0)[0] >> 4; +} + +uint16_t OutPacket::packetId() const { + return _packetId; +} + +uint8_t OutPacket::qos() const { + if (packetType() == AsyncMqttClientInternals::PacketType.PUBLISH) { + return (data()[1] & 0x06) >> 1; + } + return 0; +} + +void OutPacket::release() { + _released = true; +} + +uint16_t OutPacket::_nextPacketId = 0; + +uint16_t OutPacket::_getNextPacketId() { + if (++_nextPacketId == 0) { + ++_nextPacketId; + } + return _nextPacketId; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/OutPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/OutPacket.hpp new file mode 100644 index 0000000..52c37de --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/OutPacket.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include // uint*_t +#include // size_t +#include // std::min + +#include "../../Flags.hpp" + +namespace AsyncMqttClientInternals { +class OutPacket { + public: + OutPacket(); + virtual ~OutPacket(); + virtual const uint8_t* data(size_t index = 0) const = 0; + virtual size_t size() const = 0; + bool released() const; + uint8_t packetType() const; + uint16_t packetId() const; + uint8_t qos() const; + void release(); + + public: + OutPacket* next; + uint32_t timeout; + uint8_t noTries; + + protected: + static uint16_t _getNextPacketId(); + bool _released; + uint16_t _packetId; + + private: + static uint16_t _nextPacketId; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PingReq.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PingReq.cpp new file mode 100644 index 0000000..d59cf3d --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PingReq.cpp @@ -0,0 +1,18 @@ +#include "PingReq.hpp" + +using AsyncMqttClientInternals::PingReqOutPacket; + +PingReqOutPacket::PingReqOutPacket() { + _data[0] = AsyncMqttClientInternals::PacketType.PINGREQ; + _data[0] = _data[0] << 4; + _data[0] = _data[0] | AsyncMqttClientInternals::HeaderFlag.PINGREQ_RESERVED; + _data[1] = 0; +} + +const uint8_t* PingReqOutPacket::data(size_t index) const { + return &_data[index];; +} + +size_t PingReqOutPacket::size() const { + return 2; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PingReq.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PingReq.hpp new file mode 100644 index 0000000..1cb19a3 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PingReq.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "OutPacket.hpp" +#include "../../Flags.hpp" +#include "../../Helpers.hpp" + +namespace AsyncMqttClientInternals { +class PingReqOutPacket : public OutPacket { + public: + PingReqOutPacket(); + const uint8_t* data(size_t index = 0) const; + size_t size() const; + + private: + uint8_t _data[2]; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PubAck.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PubAck.cpp new file mode 100644 index 0000000..634607b --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PubAck.cpp @@ -0,0 +1,25 @@ +#include "PubAck.hpp" + +using AsyncMqttClientInternals::PubAckOutPacket; + +PubAckOutPacket::PubAckOutPacket(PendingAck pendingAck) { + _data[0] = pendingAck.packetType; + _data[0] = _data[0] << 4; + _data[0] = _data[0] | pendingAck.headerFlag; + _data[1] = 2; + _packetId = pendingAck.packetId; + _data[2] = pendingAck.packetId >> 8; + _data[3] = pendingAck.packetId & 0xFF; + if (packetType() == AsyncMqttClientInternals::PacketType.PUBREL || + packetType() == AsyncMqttClientInternals::PacketType.PUBREC) { + _released = false; + } +} + +const uint8_t* PubAckOutPacket::data(size_t index) const { + return &_data[index]; +} + +size_t PubAckOutPacket::size() const { + return 4; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PubAck.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PubAck.hpp new file mode 100644 index 0000000..9cd830e --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/PubAck.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "OutPacket.hpp" +#include "../../Flags.hpp" +#include "../../Helpers.hpp" +#include "../../Storage.hpp" + +namespace AsyncMqttClientInternals { +class PubAckOutPacket : public OutPacket { + public: + explicit PubAckOutPacket(PendingAck pendingAck); + const uint8_t* data(size_t index = 0) const; + size_t size() const; + + private: + uint8_t _data[4]; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Publish.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Publish.cpp new file mode 100644 index 0000000..3f4365b --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Publish.cpp @@ -0,0 +1,69 @@ +#include "Publish.hpp" + +using AsyncMqttClientInternals::PublishOutPacket; + +PublishOutPacket::PublishOutPacket(const char* topic, uint8_t qos, bool retain, const char* payload, size_t length) { + char fixedHeader[5]; + fixedHeader[0] = AsyncMqttClientInternals::PacketType.PUBLISH; + fixedHeader[0] = fixedHeader[0] << 4; + // if (dup) fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_DUP; + if (retain) fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_RETAIN; + switch (qos) { + case 0: + fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_QOS0; + break; + case 1: + fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_QOS1; + break; + case 2: + fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_QOS2; + break; + } + + uint16_t topicLength = strlen(topic); + char topicLengthBytes[2]; + topicLengthBytes[0] = topicLength >> 8; + topicLengthBytes[1] = topicLength & 0xFF; + + uint32_t payloadLength = length; + if (payload != nullptr && payloadLength == 0) payloadLength = strlen(payload); + + uint32_t remainingLength = 2 + topicLength + payloadLength; + if (qos != 0) remainingLength += 2; + uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(remainingLength, fixedHeader + 1); + + size_t neededSpace = 0; + neededSpace += 1 + remainingLengthLength; + neededSpace += 2; + neededSpace += topicLength; + if (qos != 0) neededSpace += 2; + if (payload != nullptr) neededSpace += payloadLength; + + _data.reserve(neededSpace); + + _packetId = (qos !=0) ? _getNextPacketId() : 1; + char packetIdBytes[2]; + packetIdBytes[0] = _packetId >> 8; + packetIdBytes[1] = _packetId & 0xFF; + + _data.insert(_data.end(), fixedHeader, fixedHeader + 1 + remainingLengthLength); + _data.insert(_data.end(), topicLengthBytes, topicLengthBytes + 2); + _data.insert(_data.end(), topic, topic + topicLength); + if (qos != 0) { + _data.insert(_data.end(), packetIdBytes, packetIdBytes + 2); + _released = false; + } + if (payload != nullptr) _data.insert(_data.end(), payload, payload + payloadLength); +} + +const uint8_t* PublishOutPacket::data(size_t index) const { + return &_data.data()[index]; +} + +size_t PublishOutPacket::size() const { + return _data.size(); +} + +void PublishOutPacket::setDup() { + _data[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_DUP; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Publish.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Publish.hpp new file mode 100644 index 0000000..6b8272e --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Publish.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include // strlen +#include + +#include "OutPacket.hpp" +#include "../../Flags.hpp" +#include "../../Helpers.hpp" +#include "../../Storage.hpp" + +namespace AsyncMqttClientInternals { +class PublishOutPacket : public OutPacket { + public: + PublishOutPacket(const char* topic, uint8_t qos, bool retain, const char* payload, size_t length); + const uint8_t* data(size_t index = 0) const; + size_t size() const; + + void setDup(); // you cannot unset dup + + private: + std::vector _data; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Subscribe.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Subscribe.cpp new file mode 100644 index 0000000..85c10db --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Subscribe.cpp @@ -0,0 +1,49 @@ +#include "Subscribe.hpp" + +using AsyncMqttClientInternals::SubscribeOutPacket; + +SubscribeOutPacket::SubscribeOutPacket(const char* topic, uint8_t qos) { + char fixedHeader[5]; + fixedHeader[0] = AsyncMqttClientInternals::PacketType.SUBSCRIBE; + fixedHeader[0] = fixedHeader[0] << 4; + fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.SUBSCRIBE_RESERVED; + + uint16_t topicLength = strlen(topic); + char topicLengthBytes[2]; + topicLengthBytes[0] = topicLength >> 8; + topicLengthBytes[1] = topicLength & 0xFF; + + char qosByte[1]; + qosByte[0] = qos; + + uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(2 + 2 + topicLength + 1, fixedHeader + 1); + + size_t neededSpace = 0; + neededSpace += 1 + remainingLengthLength; + neededSpace += 2; + neededSpace += 2; + neededSpace += topicLength; + neededSpace += 1; + + _data.reserve(neededSpace); + + _packetId = _getNextPacketId(); + char packetIdBytes[2]; + packetIdBytes[0] = _packetId >> 8; + packetIdBytes[1] = _packetId & 0xFF; + + _data.insert(_data.end(), fixedHeader, fixedHeader + 1 + remainingLengthLength); + _data.insert(_data.end(), packetIdBytes, packetIdBytes + 2); + _data.insert(_data.end(), topicLengthBytes, topicLengthBytes + 2); + _data.insert(_data.end(), topic, topic + topicLength); + _data.push_back(qosByte[0]); + _released = false; +} + +const uint8_t* SubscribeOutPacket::data(size_t index) const { + return &_data.data()[index]; +} + +size_t SubscribeOutPacket::size() const { + return _data.size(); +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Subscribe.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Subscribe.hpp new file mode 100644 index 0000000..1f85f59 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Subscribe.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include // strlen +#include + +#include "OutPacket.hpp" +#include "../../Flags.hpp" +#include "../../Helpers.hpp" +#include "../../Storage.hpp" + +namespace AsyncMqttClientInternals { +class SubscribeOutPacket : public OutPacket { + public: + SubscribeOutPacket(const char* topic, uint8_t qos); + const uint8_t* data(size_t index = 0) const; + size_t size() const; + + private: + std::vector _data; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Unsubscribe.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Unsubscribe.cpp new file mode 100644 index 0000000..4d859c9 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Unsubscribe.cpp @@ -0,0 +1,42 @@ +#include "Unsubscribe.hpp" + +using AsyncMqttClientInternals::UnsubscribeOutPacket; + +UnsubscribeOutPacket::UnsubscribeOutPacket(const char* topic) { + char fixedHeader[5]; + fixedHeader[0] = AsyncMqttClientInternals::PacketType.UNSUBSCRIBE; + fixedHeader[0] = fixedHeader[0] << 4; + fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.UNSUBSCRIBE_RESERVED; + + uint16_t topicLength = strlen(topic); + char topicLengthBytes[2]; + topicLengthBytes[0] = topicLength >> 8; + topicLengthBytes[1] = topicLength & 0xFF; + + uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(2 + 2 + topicLength, fixedHeader + 1); + + size_t neededSpace = 0; + neededSpace += 1 + remainingLengthLength; + neededSpace += 2; + neededSpace += 2; + neededSpace += topicLength; + + _packetId = _getNextPacketId(); + char packetIdBytes[2]; + packetIdBytes[0] = _packetId >> 8; + packetIdBytes[1] = _packetId & 0xFF; + + _data.insert(_data.end(), fixedHeader, fixedHeader + 1 + remainingLengthLength); + _data.insert(_data.end(), packetIdBytes, packetIdBytes + 2); + _data.insert(_data.end(), topicLengthBytes, topicLengthBytes + 2); + _data.insert(_data.end(), topic, topic + topicLength); + _released = false; +} + +const uint8_t* UnsubscribeOutPacket::data(size_t index) const { + return &_data.data()[index]; +} + +size_t UnsubscribeOutPacket::size() const { + return _data.size(); +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Unsubscribe.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Unsubscribe.hpp new file mode 100644 index 0000000..621802f --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Out/Unsubscribe.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include // strlen +#include + +#include "OutPacket.hpp" +#include "../../Flags.hpp" +#include "../../Helpers.hpp" +#include "../../Storage.hpp" + +namespace AsyncMqttClientInternals { +class UnsubscribeOutPacket : public OutPacket { + public: + explicit UnsubscribeOutPacket(const char* topic); + const uint8_t* data(size_t index = 0) const; + size_t size() const; + + private: + std::vector _data; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Packet.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Packet.hpp new file mode 100644 index 0000000..e8dfdfb --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/Packet.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace AsyncMqttClientInternals { +class Packet { + public: + virtual ~Packet() {} + + virtual void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) = 0; + virtual void parsePayload(char* data, size_t len, size_t* currentBytePosition) = 0; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.cpp new file mode 100644 index 0000000..4aa4db8 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.cpp @@ -0,0 +1,21 @@ +#include "PingRespPacket.hpp" + +using AsyncMqttClientInternals::PingRespPacket; + +PingRespPacket::PingRespPacket(ParsingInformation* parsingInformation, OnPingRespInternalCallback callback) +: _parsingInformation(parsingInformation) +, _callback(callback) { +} + +PingRespPacket::~PingRespPacket() { +} + +void PingRespPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { + (void)data; + (void)currentBytePosition; +} + +void PingRespPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { + (void)data; + (void)currentBytePosition; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.hpp new file mode 100644 index 0000000..098049a --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PingRespPacket.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "Arduino.h" +#include "Packet.hpp" +#include "../ParsingInformation.hpp" +#include "../Callbacks.hpp" + +namespace AsyncMqttClientInternals { +class PingRespPacket : public Packet { + public: + explicit PingRespPacket(ParsingInformation* parsingInformation, OnPingRespInternalCallback callback); + ~PingRespPacket(); + + void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); + void parsePayload(char* data, size_t len, size_t* currentBytePosition); + + private: + ParsingInformation* _parsingInformation; + OnPingRespInternalCallback _callback; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.cpp new file mode 100644 index 0000000..66e303b --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.cpp @@ -0,0 +1,30 @@ +#include "PubAckPacket.hpp" + +using AsyncMqttClientInternals::PubAckPacket; + +PubAckPacket::PubAckPacket(ParsingInformation* parsingInformation, OnPubAckInternalCallback callback) +: _parsingInformation(parsingInformation) +, _callback(callback) +, _bytePosition(0) +, _packetIdMsb(0) +, _packetId(0) { +} + +PubAckPacket::~PubAckPacket() { +} + +void PubAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { + char currentByte = data[(*currentBytePosition)++]; + if (_bytePosition++ == 0) { + _packetIdMsb = currentByte; + } else { + _packetId = currentByte | _packetIdMsb << 8; + _parsingInformation->bufferState = BufferState::NONE; + _callback(_packetId); + } +} + +void PubAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { + (void)data; + (void)currentBytePosition; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.hpp new file mode 100644 index 0000000..ea8a526 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubAckPacket.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Arduino.h" +#include "Packet.hpp" +#include "../ParsingInformation.hpp" +#include "../Callbacks.hpp" + +namespace AsyncMqttClientInternals { +class PubAckPacket : public Packet { + public: + explicit PubAckPacket(ParsingInformation* parsingInformation, OnPubAckInternalCallback callback); + ~PubAckPacket(); + + void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); + void parsePayload(char* data, size_t len, size_t* currentBytePosition); + + private: + ParsingInformation* _parsingInformation; + OnPubAckInternalCallback _callback; + + uint8_t _bytePosition; + char _packetIdMsb; + uint16_t _packetId; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.cpp new file mode 100644 index 0000000..b845d23 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.cpp @@ -0,0 +1,30 @@ +#include "PubCompPacket.hpp" + +using AsyncMqttClientInternals::PubCompPacket; + +PubCompPacket::PubCompPacket(ParsingInformation* parsingInformation, OnPubCompInternalCallback callback) +: _parsingInformation(parsingInformation) +, _callback(callback) +, _bytePosition(0) +, _packetIdMsb(0) +, _packetId(0) { +} + +PubCompPacket::~PubCompPacket() { +} + +void PubCompPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { + char currentByte = data[(*currentBytePosition)++]; + if (_bytePosition++ == 0) { + _packetIdMsb = currentByte; + } else { + _packetId = currentByte | _packetIdMsb << 8; + _parsingInformation->bufferState = BufferState::NONE; + _callback(_packetId); + } +} + +void PubCompPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { + (void)data; + (void)currentBytePosition; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.hpp new file mode 100644 index 0000000..b6aaaa9 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubCompPacket.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Arduino.h" +#include "Packet.hpp" +#include "../ParsingInformation.hpp" +#include "../Callbacks.hpp" + +namespace AsyncMqttClientInternals { +class PubCompPacket : public Packet { + public: + explicit PubCompPacket(ParsingInformation* parsingInformation, OnPubCompInternalCallback callback); + ~PubCompPacket(); + + void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); + void parsePayload(char* data, size_t len, size_t* currentBytePosition); + + private: + ParsingInformation* _parsingInformation; + OnPubCompInternalCallback _callback; + + uint8_t _bytePosition; + char _packetIdMsb; + uint16_t _packetId; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.cpp new file mode 100644 index 0000000..8e19135 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.cpp @@ -0,0 +1,30 @@ +#include "PubRecPacket.hpp" + +using AsyncMqttClientInternals::PubRecPacket; + +PubRecPacket::PubRecPacket(ParsingInformation* parsingInformation, OnPubRecInternalCallback callback) +: _parsingInformation(parsingInformation) +, _callback(callback) +, _bytePosition(0) +, _packetIdMsb(0) +, _packetId(0) { +} + +PubRecPacket::~PubRecPacket() { +} + +void PubRecPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { + char currentByte = data[(*currentBytePosition)++]; + if (_bytePosition++ == 0) { + _packetIdMsb = currentByte; + } else { + _packetId = currentByte | _packetIdMsb << 8; + _parsingInformation->bufferState = BufferState::NONE; + _callback(_packetId); + } +} + +void PubRecPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { + (void)data; + (void)currentBytePosition; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.hpp new file mode 100644 index 0000000..09e2d77 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRecPacket.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Arduino.h" +#include "Packet.hpp" +#include "../ParsingInformation.hpp" +#include "../Callbacks.hpp" + +namespace AsyncMqttClientInternals { +class PubRecPacket : public Packet { + public: + explicit PubRecPacket(ParsingInformation* parsingInformation, OnPubRecInternalCallback callback); + ~PubRecPacket(); + + void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); + void parsePayload(char* data, size_t len, size_t* currentBytePosition); + + private: + ParsingInformation* _parsingInformation; + OnPubRecInternalCallback _callback; + + uint8_t _bytePosition; + char _packetIdMsb; + uint16_t _packetId; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.cpp new file mode 100644 index 0000000..8a1f2d4 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.cpp @@ -0,0 +1,30 @@ +#include "PubRelPacket.hpp" + +using AsyncMqttClientInternals::PubRelPacket; + +PubRelPacket::PubRelPacket(ParsingInformation* parsingInformation, OnPubRelInternalCallback callback) +: _parsingInformation(parsingInformation) +, _callback(callback) +, _bytePosition(0) +, _packetIdMsb(0) +, _packetId(0) { +} + +PubRelPacket::~PubRelPacket() { +} + +void PubRelPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { + char currentByte = data[(*currentBytePosition)++]; + if (_bytePosition++ == 0) { + _packetIdMsb = currentByte; + } else { + _packetId = currentByte | _packetIdMsb << 8; + _parsingInformation->bufferState = BufferState::NONE; + _callback(_packetId); + } +} + +void PubRelPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { + (void)data; + (void)currentBytePosition; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.hpp new file mode 100644 index 0000000..96e9af7 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PubRelPacket.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Arduino.h" +#include "Packet.hpp" +#include "../ParsingInformation.hpp" +#include "../Callbacks.hpp" + +namespace AsyncMqttClientInternals { +class PubRelPacket : public Packet { + public: + explicit PubRelPacket(ParsingInformation* parsingInformation, OnPubRelInternalCallback callback); + ~PubRelPacket(); + + void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); + void parsePayload(char* data, size_t len, size_t* currentBytePosition); + + private: + ParsingInformation* _parsingInformation; + OnPubRelInternalCallback _callback; + + uint8_t _bytePosition; + char _packetIdMsb; + uint16_t _packetId; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.cpp new file mode 100644 index 0000000..2c5192f --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.cpp @@ -0,0 +1,91 @@ +#include "PublishPacket.hpp" + +using AsyncMqttClientInternals::PublishPacket; + +PublishPacket::PublishPacket(ParsingInformation* parsingInformation, OnMessageInternalCallback dataCallback, OnPublishInternalCallback completeCallback) +: _parsingInformation(parsingInformation) +, _dataCallback(dataCallback) +, _completeCallback(completeCallback) +, _dup(false) +, _qos(0) +, _retain(0) +, _bytePosition(0) +, _topicLengthMsb(0) +, _topicLength(0) +, _ignore(false) +, _packetIdMsb(0) +, _packetId(0) +, _payloadLength(0) +, _payloadBytesRead(0) { + _dup = _parsingInformation->packetFlags & HeaderFlag.PUBLISH_DUP; + _retain = _parsingInformation->packetFlags & HeaderFlag.PUBLISH_RETAIN; + char qosMasked = _parsingInformation->packetFlags & 0x06; + switch (qosMasked) { + case HeaderFlag.PUBLISH_QOS0: + _qos = 0; + break; + case HeaderFlag.PUBLISH_QOS1: + _qos = 1; + break; + case HeaderFlag.PUBLISH_QOS2: + _qos = 2; + break; + } +} + +PublishPacket::~PublishPacket() { +} + +void PublishPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { + char currentByte = data[(*currentBytePosition)++]; + if (_bytePosition == 0) { + _topicLengthMsb = currentByte; + } else if (_bytePosition == 1) { + _topicLength = currentByte | _topicLengthMsb << 8; + if (_topicLength > _parsingInformation->maxTopicLength) { + _ignore = true; + } else { + _parsingInformation->topicBuffer[_topicLength] = '\0'; + } + } else if (_bytePosition >= 2 && _bytePosition < 2 + _topicLength) { + // Starting from here, _ignore might be true + if (!_ignore) _parsingInformation->topicBuffer[_bytePosition - 2] = currentByte; + if (_bytePosition == 2 + _topicLength - 1 && _qos == 0) { + _preparePayloadHandling(_parsingInformation->remainingLength - (_bytePosition + 1)); + return; + } + } else if (_bytePosition == 2 + _topicLength) { + _packetIdMsb = currentByte; + } else { + _packetId = currentByte | _packetIdMsb << 8; + _preparePayloadHandling(_parsingInformation->remainingLength - (_bytePosition + 1)); + } + _bytePosition++; +} + +void PublishPacket::_preparePayloadHandling(uint32_t payloadLength) { + _payloadLength = payloadLength; + if (payloadLength == 0) { + _parsingInformation->bufferState = BufferState::NONE; + if (!_ignore) { + _dataCallback(_parsingInformation->topicBuffer, nullptr, _qos, _dup, _retain, 0, 0, 0, _packetId); + _completeCallback(_packetId, _qos); + } + } else { + _parsingInformation->bufferState = BufferState::PAYLOAD; + } +} + +void PublishPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { + size_t remainToRead = len - (*currentBytePosition); + if (_payloadBytesRead + remainToRead > _payloadLength) remainToRead = _payloadLength - _payloadBytesRead; + + if (!_ignore) _dataCallback(_parsingInformation->topicBuffer, data + (*currentBytePosition), _qos, _dup, _retain, remainToRead, _payloadBytesRead, _payloadLength, _packetId); + _payloadBytesRead += remainToRead; + (*currentBytePosition) += remainToRead; + + if (_payloadBytesRead == _payloadLength) { + _parsingInformation->bufferState = BufferState::NONE; + if (!_ignore) _completeCallback(_packetId, _qos); + } +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.hpp new file mode 100644 index 0000000..d97205c --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/PublishPacket.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "Arduino.h" +#include "Packet.hpp" +#include "../Flags.hpp" +#include "../ParsingInformation.hpp" +#include "../Callbacks.hpp" + +namespace AsyncMqttClientInternals { +class PublishPacket : public Packet { + public: + explicit PublishPacket(ParsingInformation* parsingInformation, OnMessageInternalCallback dataCallback, OnPublishInternalCallback completeCallback); + ~PublishPacket(); + + void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); + void parsePayload(char* data, size_t len, size_t* currentBytePosition); + + private: + ParsingInformation* _parsingInformation; + OnMessageInternalCallback _dataCallback; + OnPublishInternalCallback _completeCallback; + + void _preparePayloadHandling(uint32_t payloadLength); + + bool _dup; + uint8_t _qos; + bool _retain; + + uint8_t _bytePosition; + char _topicLengthMsb; + uint16_t _topicLength; + bool _ignore; + char _packetIdMsb; + uint16_t _packetId; + uint32_t _payloadLength; + uint32_t _payloadBytesRead; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.cpp new file mode 100644 index 0000000..724f05f --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.cpp @@ -0,0 +1,46 @@ +#include "SubAckPacket.hpp" + +using AsyncMqttClientInternals::SubAckPacket; + +SubAckPacket::SubAckPacket(ParsingInformation* parsingInformation, OnSubAckInternalCallback callback) +: _parsingInformation(parsingInformation) +, _callback(callback) +, _bytePosition(0) +, _packetIdMsb(0) +, _packetId(0) { +} + +SubAckPacket::~SubAckPacket() { +} + +void SubAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { + char currentByte = data[(*currentBytePosition)++]; + if (_bytePosition++ == 0) { + _packetIdMsb = currentByte; + } else { + _packetId = currentByte | _packetIdMsb << 8; + _parsingInformation->bufferState = BufferState::PAYLOAD; + } +} + +void SubAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { + char status = data[(*currentBytePosition)++]; + + /* switch (status) { + case 0: + Serial.println("Success QoS 0"); + break; + case 1: + Serial.println("Success QoS 1"); + break; + case 2: + Serial.println("Success QoS 2"); + break; + case 0x80: + Serial.println("Failure"); + break; + } */ + + _parsingInformation->bufferState = BufferState::NONE; + _callback(_packetId, status); +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.hpp new file mode 100644 index 0000000..26077c2 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/SubAckPacket.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Arduino.h" +#include "Packet.hpp" +#include "../ParsingInformation.hpp" +#include "../Callbacks.hpp" + +namespace AsyncMqttClientInternals { +class SubAckPacket : public Packet { + public: + explicit SubAckPacket(ParsingInformation* parsingInformation, OnSubAckInternalCallback callback); + ~SubAckPacket(); + + void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); + void parsePayload(char* data, size_t len, size_t* currentBytePosition); + + private: + ParsingInformation* _parsingInformation; + OnSubAckInternalCallback _callback; + + uint8_t _bytePosition; + char _packetIdMsb; + uint16_t _packetId; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.cpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.cpp new file mode 100644 index 0000000..3c0ed74 --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.cpp @@ -0,0 +1,30 @@ +#include "UnsubAckPacket.hpp" + +using AsyncMqttClientInternals::UnsubAckPacket; + +UnsubAckPacket::UnsubAckPacket(ParsingInformation* parsingInformation, OnUnsubAckInternalCallback callback) +: _parsingInformation(parsingInformation) +, _callback(callback) +, _bytePosition(0) +, _packetIdMsb(0) +, _packetId(0) { +} + +UnsubAckPacket::~UnsubAckPacket() { +} + +void UnsubAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) { + char currentByte = data[(*currentBytePosition)++]; + if (_bytePosition++ == 0) { + _packetIdMsb = currentByte; + } else { + _packetId = currentByte | _packetIdMsb << 8; + _parsingInformation->bufferState = BufferState::NONE; + _callback(_packetId); + } +} + +void UnsubAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) { + (void)data; + (void)currentBytePosition; +} diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.hpp new file mode 100644 index 0000000..9c17c2d --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Packets/UnsubAckPacket.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Arduino.h" +#include "Packet.hpp" +#include "../ParsingInformation.hpp" +#include "../Callbacks.hpp" + +namespace AsyncMqttClientInternals { +class UnsubAckPacket : public Packet { + public: + explicit UnsubAckPacket(ParsingInformation* parsingInformation, OnUnsubAckInternalCallback callback); + ~UnsubAckPacket(); + + void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition); + void parsePayload(char* data, size_t len, size_t* currentBytePosition); + + private: + ParsingInformation* _parsingInformation; + OnUnsubAckInternalCallback _callback; + + uint8_t _bytePosition; + char _packetIdMsb; + uint16_t _packetId; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/ParsingInformation.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/ParsingInformation.hpp new file mode 100644 index 0000000..a14640c --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/ParsingInformation.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace AsyncMqttClientInternals { +enum class BufferState : uint8_t { + NONE = 0, + REMAINING_LENGTH = 2, + VARIABLE_HEADER = 3, + PAYLOAD = 4 +}; + +struct ParsingInformation { + BufferState bufferState; + + uint16_t maxTopicLength; + char* topicBuffer; + + uint8_t packetType; + uint16_t packetFlags; + uint32_t remainingLength; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/async-mqtt-client/AsyncMqttClient/Storage.hpp b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Storage.hpp new file mode 100644 index 0000000..725307b --- /dev/null +++ b/yoRadio/src/async-mqtt-client/AsyncMqttClient/Storage.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace AsyncMqttClientInternals { +struct PendingPubRel { + uint16_t packetId; +}; + +struct PendingAck { + uint8_t packetType; + uint8_t headerFlag; + uint16_t packetId; +}; +} // namespace AsyncMqttClientInternals diff --git a/yoRadio/src/core/mqtt.h b/yoRadio/src/core/mqtt.h index d4a3e07..bb6c2c6 100644 --- a/yoRadio/src/core/mqtt.h +++ b/yoRadio/src/core/mqtt.h @@ -3,7 +3,7 @@ #if __has_include("../../mqttoptions.h") #include "../../mqttoptions.h" -#include +#include "../async-mqtt-client/AsyncMqttClient.h" void mqttInit(); diff --git a/yoRadio/src/core/netserver.h b/yoRadio/src/core/netserver.h index 83a4407..9a73b43 100644 --- a/yoRadio/src/core/netserver.h +++ b/yoRadio/src/core/netserver.h @@ -2,7 +2,7 @@ #define netserver_h #include "Arduino.h" -#include "ESPAsyncWebServer.h" +#include "../AsyncWebServer/ESPAsyncWebServer.h" #include "AsyncUDP.h" 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, GETMODE=13, GETINDEX=14, GETACTIVE=15, GETSYSTEM=16, GETSCREEN=17, GETTIMEZONE=18, GETWEATHER=19, GETCONTROLS=20, DSPON=21, SDPOS=22, SDLEN=23, SDSNUFFLE=24 }; @@ -27,8 +27,8 @@ class NetServer { void setRSSI(int val) { rssi = val; }; void chunkedHtmlPage(const String& contentType, AsyncWebServerRequest *request, const char * path, bool gzip = false); void onWsMessage(void *arg, uint8_t *data, size_t len, uint8_t clientId); -#if IR_PIN!=255 bool irRecordEnable; +#if IR_PIN!=255 void irToWs(const char* protocol, uint64_t irvalue); void irValsToWs(); #endif diff --git a/yoRadio/src/core/options.h b/yoRadio/src/core/options.h index d05b63e..ef652d2 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.8.901" +#define YOVERSION "0.8.920" /******************************************************* DO NOT EDIT THIS FILE. diff --git a/yoRadio/src/core/player.cpp b/yoRadio/src/core/player.cpp index 7b57f8f..7ef5710 100644 --- a/yoRadio/src/core/player.cpp +++ b/yoRadio/src/core/player.cpp @@ -78,7 +78,8 @@ void Player::stop(const char *nttl){ netserver.requestOnChange(BITRATE, 0); display.putRequest(DBITRATE); display.putRequest(PSTOP); - setDefaults(); + //setDefaults(); + stopSong(); stopInfo(); if (player_on_stop_play) player_on_stop_play(); } diff --git a/yoRadio/src/displays/widgets/pages.h b/yoRadio/src/displays/widgets/pages.h index 7fa364c..d79ee85 100644 --- a/yoRadio/src/displays/widgets/pages.h +++ b/yoRadio/src/displays/widgets/pages.h @@ -2,7 +2,7 @@ #define pages_h #include "Arduino.h" -#include "StringArray.h" +#include "../../AsyncWebServer/StringArray.h" class Page { protected: