From 3276c9527d8c5c165a3925e963a901c01febcd03 Mon Sep 17 00:00:00 2001 From: RnD1 Date: Fri, 3 Apr 2026 20:40:52 +0900 Subject: [PATCH] Initial commit: HermitCrab project structure --- .gitignore | Bin 0 -> 438 bytes .vs/HermitCrab/v16/.suo | Bin 0 -> 5632 bytes .vs/HermitCrab/v16/Browse.VC.db | Bin 0 -> 262144 bytes .vscode/c_cpp_properties.json | 40 + .vscode/settings.json | 69 ++ AHT2x.cpp | 207 +++++ AHT2x.h | 43 + BLEScan.cpp | 433 ++++++++++ BLEScan.h | 72 ++ BearSSL.h | 1346 +++++++++++++++++++++++++++++++ CommSerial.cpp | 154 ++++ Config.cpp | 158 ++++ Config.h | 197 +++++ ConnectWiFi.cpp | 99 +++ ConnectWiFi.h | 13 + HCUpdate.h | 291 +++++++ HCUpdater.cpp | 787 ++++++++++++++++++ HCesp.ino | 683 ++++++++++++++++ HermitCrab.h | 243 ++++++ HermitCrab.vcxproj | 72 ++ HermitCrab.vcxproj.user | 4 + History.cpp | 214 +++++ History.h | 69 ++ LED0.cpp | 52 ++ LED0.h | 38 + NTC_10K.cpp | 127 +++ NTC_10K.h | 32 + OTA.cpp | 134 +++ OTA.h | 10 + README.md | Bin 0 -> 6 bytes SSD1306.cpp | 225 ++++++ SSD1306.h | 86 ++ Setup.cpp | 302 +++++++ Task0.ino | 128 +++ TimeManager.cpp | 92 +++ TimeManager.h | 33 + UI.cpp | 1152 ++++++++++++++++++++++++++ UI.h | 103 +++ UPnPClient.cpp | 491 +++++++++++ UPnPClient.h | 37 + WiFiHost.cpp | 885 ++++++++++++++++++++ WiFiHost.h | 284 +++++++ c_cpp_properties.json | 39 + zcd.cpp | 185 +++++ zcd.h | 13 + 45 files changed, 9642 insertions(+) create mode 100644 .gitignore create mode 100644 .vs/HermitCrab/v16/.suo create mode 100644 .vs/HermitCrab/v16/Browse.VC.db create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/settings.json create mode 100644 AHT2x.cpp create mode 100644 AHT2x.h create mode 100644 BLEScan.cpp create mode 100644 BLEScan.h create mode 100644 BearSSL.h create mode 100644 CommSerial.cpp create mode 100644 Config.cpp create mode 100644 Config.h create mode 100644 ConnectWiFi.cpp create mode 100644 ConnectWiFi.h create mode 100644 HCUpdate.h create mode 100644 HCUpdater.cpp create mode 100644 HCesp.ino create mode 100644 HermitCrab.h create mode 100644 HermitCrab.vcxproj create mode 100644 HermitCrab.vcxproj.user create mode 100644 History.cpp create mode 100644 History.h create mode 100644 LED0.cpp create mode 100644 LED0.h create mode 100644 NTC_10K.cpp create mode 100644 NTC_10K.h create mode 100644 OTA.cpp create mode 100644 OTA.h create mode 100644 README.md create mode 100644 SSD1306.cpp create mode 100644 SSD1306.h create mode 100644 Setup.cpp create mode 100644 Task0.ino create mode 100644 TimeManager.cpp create mode 100644 TimeManager.h create mode 100644 UI.cpp create mode 100644 UI.h create mode 100644 UPnPClient.cpp create mode 100644 UPnPClient.h create mode 100644 WiFiHost.cpp create mode 100644 WiFiHost.h create mode 100644 c_cpp_properties.json create mode 100644 zcd.cpp create mode 100644 zcd.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..35bfb1a08db022ffb9143e1c00540b150857ff99 GIT binary patch literal 438 zcma)2%MOA-5bN2*e<1OqCcrls@!-LWCf<3hN+e=fK>c~OGb{ve9@uO<-L}l*UcEZTHZR1?5j~+4I7Ku+YYFuhrt(t Cw?Z)h literal 0 HcmV?d00001 diff --git a/.vs/HermitCrab/v16/.suo b/.vs/HermitCrab/v16/.suo new file mode 100644 index 0000000000000000000000000000000000000000..455ebe4d2a8574efdb19e64c993a700e49435cd3 GIT binary patch literal 5632 zcmeHJ%}T>S5dPX)Yei5&iin4vJc*A`M5!PsNQ*ZQhFY;wE%Zm~&69WEKrbG=6?_jB zdQ{M(_y}U-d`Y&XTGFHjq#+ZsGufHh`P5x|KOln`I(ic0w)u#S#taM| z=Q_HslSNv{5Jo5iIaK7SBaJLh1y^y-Qo4o|;@z4@2WVZi?7>Nr!2Rjn$uB& zCF}mfyC2tg6tgmmyT;6wj=zJG-M@9Xj7IsbO+ zti@m8M61<{)xGA+{PoBE=hK|mwfG;HmY08yVf1fXiTC#6@b8#MW_{z0p#w|Kq* DY97k& literal 0 HcmV?d00001 diff --git a/.vs/HermitCrab/v16/Browse.VC.db b/.vs/HermitCrab/v16/Browse.VC.db new file mode 100644 index 0000000000000000000000000000000000000000..1526dc802fd0d7f6ee156de0c0a90320cd0fa560 GIT binary patch literal 262144 zcmeI5Yj7Odb>F8kJ=1tMcll^?u@D;sv7BBE0Sp1Ymb>DDz;FpK2iyg~eF>FOW2Oh_ z<-B%!h6KxzSA*52O0rTaNpUK%za>i9vQth{ij#bZBiW8ps#GblU6rhrD2^+&PRdeM zva%ILiCn4Nd%Ju3F|UTm1*6$NijSVY=bm%!@1FZOJ(xS|FW=Ownw%?D3TjoJ4;>2i z^@X07TzSynl7@&xG#_zbu@Lo``%Ua)N)W?>qeUzR&h0#2>d)iu;d{z$;QzN=Tfc7qi-~ zzMIbJdCf@YH8qzmss)X`JifG+T-->?E33=N*W_j)`PQn;1Y?bQ%IdMGbVlL|OmFQi zOK(G^WU0Kpy7KaETeC;ZQA#T*dv6{QBuV1lov*4}dDAau^Qfh1WAWO}r0Iqnll82; zrElxSs=Rt@Ltedo^QOFXYju5NZEcqtM;doZVEn^jh+IaxJ;Klw3EPRWV|&VntDoMy2N@F2p*|cq>n) zl(br_me^|xU4qd+R-Za7NS~PE>aW^8U9Obg)H2m{MavoKOsSaDx6^u-yfjMryIPio zm0RXWn^17ew9`X_#4RVs8V2JYgG#;saY0HEKc|_WB#7Ph+%FhVci~}w_`3_6YWiJ@ zxultD#du7Rt`bkgmYy^k;Hqal-qD3dl_uMb5@j!ZNRXzcxOZ0A_;D+DUq9v-Gn(d~LR&L*&1qfu#kj3aZB zInmlWT;@c7FS})2%41BSIueyGj&X1M725FhtW|xqomxlIt=ze*ynB2|kjBQi&j~DO zEPd90!=6yFZU&ipEm1NlcqiW-)!6YzmebRnXLK^n*=imC-4SQqZ5whFsdpA7t3j!h zCv$#UH?*Rm5r4H}RnO^U5~kB_TqX{u3#Dw0te9lwDBgE3H*s0p&E#uYEnQZtEt+L| zo7Rf~gmM+)+`UrePB8IOxvH0nMhldAwMut}UDT9qv7mMv71~<13ev9R?KM4 z4!UV{mQ~IUN9jmzvqagkkgzUB-WZHZXU93xMP&4meTE$l?#bR5YBStD!gQ$EH)3?{ zRo)nAqnpN*EjrjuHw!?wZZ^p4vw|Rvk8^to_q1);TKhd?(7qdF5Zba$qp;OLXx?JP zWn)LJXk_Ezi+g3ZKPoMax9gaO*@S&HP=7KiNbA#F{RO*oG9@x>$eyE+zC&&qY(^%- zi0r@2-HxwZXoJ=#)}+Rq^gO0yHnqlGwv;_SB1qHI+&fpTInYzVXCvzA4_~3#E&hF+ zd+j9qQeT}w-Q#lCJB7J*vl-IkyBn!zs$`^nTf%xQ%$9<^GiD%~GNxsN_cYtONtzqT zG16$OhQ}e#zPwdX8at&*m36AO(aKrA)6`(wN>~?>`UodT6BArrvxit&t5kK34l?Vi z&)dGnebU5c&)m>y+ZxiQuby_@Rr599vZKuRMWq)eIt(DosWcrz<~7M5$DShpJaKe{ z{40n@IPpF4>ta=WR{Xvg6OTN?{1T=?00ck)1V8`;KmY_l00ck)1VG@S6A-^2Iwl!v zPD@uyX_6Zkb1Hd&`}m=-A~v3@ut&CYkBi=WnPPSBLL{ubnkf|uNAzTenx2Z>=V87wtC{Qd(tOTO#jm<@HQ;h3AjfMf#X``I*g^%(pI#%^O;!pjVeF z>X!R*=B}sYZsNk`wMyxpq0PLqG?U#r77i;?Ca+Tu?rIf-JjYF3m`Nla>kE&KIXC9! z5|=`UN5fM`-HokQYTBUq0m=V=Ate5R_;nz!7~9U}eVqa1yrz_T*X1A*{R zc#N~21@sZvhujwF`~MN~iIDh*;#b67@dmlX4+ww&2!H?xfB*=900@8p2!H?xe1r(Z z2l>>|qu0(SFJD`pzjR@FKDlu1{PN{<=aZKf7v`^BynN~0+_i-pH`j0OEuW`<2gMsC|DOzrlj46A|B09vzxWaAVl)*5KmY_l00ck)1V8`;KmY_l00bNY zr^6|(OEx_hCE};MJu9G!y2%W>gxWqv7;p)-eRk0A5^DRWui^3;`d7&@*Ui8z3L`A|12MxP8D()a;S~@%T46rXSYg;D}3mTf`gJ~TxtSifwh8h5&N$n-o7CF71>E^FCt!pb>5B&CcD`RrXc z(dmR4qRf+S!pa=gn^(8At`uiyDUupF&~!Vk=5^KRrh9sZI#Rt)ny2Nu3D2IT+RLR{ zF`L$kw|Io+(QM{$J@jW=p^^M5wxG+56C zP(OFZaj0BTw+m`F6EjaaP0s7Zu1%hua$M3Xl~Sdff!Whe1r`|HD$GqfZ3s3mCQi|@ zYjuNpre8NBvlG;-f|{w6(j=U^cifyzy)r8h@c^XaCuvt_wY;{iR=aXlK1K(bc9(qk zw43koaTY`7`BV41NuPa^51l0Qp_(tzqhPy<$H%Dhf?8&0z;%;8^8_C{Or^UCUO2&r z5~;?Cx@ontU85iM?N;yXaq4}eUN`Z$M7_VOR&;t!aaZYMR618HX6TuX-K1xZ(vf1$ zZCNeTbuOGgLRVl@o*qQ3r-c^wgEv3jwg7k<=@~Lq9f}H(EaFDgRtAbIFf?}zd zrYi+KsVe%4$`g*+#vGKIO#gANXAY@ zt#+S$$vfk8$EX#yy3?K)*p89Orh^WOxhS0$YsEXo(!F9j$gnsYp)GFg&AI`K@qFkg zYjHP8B}`|~S~0*-e!7ohAz@t0bd6$#qiO>UltGenT;=;iLmNX+4*#0??;{^Xe3ve#M`5p!RLnl(eNMf=lTE1|E~B{xH12bMtq1VW)WKb9h&JPMTmU!CSpq_|-F+>bNaVEO-Z z3;cA7+h_j&>^c67?M%S@|H>>sVOI%~13$y$gXjO3=lIDKw~ze)(gl7t#qB%)zi2jn zKl%R!=Gs2;|MLlciUvmT{C|OkQo#Iw{%KxHae?yxb5@83%KsBgZ=n2tjv?7c{y)nc z37G%SFzx%u|Ie~u3!eYSJ@Vc2fAScGbm)Ha|EJIL<4%pPTN+n`1LglQmqWqw|1+*8 z2g(1Ra=8>J|DSSI2$cVycC{hcyg13m?!NN>Q_QP<}uWmVu*EJ;QW7(bzSiM zf56-^2Fm~Y%^l9}oZm5C8!X009sH0T2KI5C8!X0D(tI;B_uCP)qgGZwpwv^>fLk zjrGkN$+gtV#?sp2wbqy9>j7S)!b{2I{Dn*BE?v8R>H5OLa`JL=esOtu{&HgRQu4xu zYYX)Ke@^_%5cz{25C8!X009sH0T2KI5C8!X009sHfgU6f4i5~N`Tw^<;i71RwXNFrUwu_C&Y$ZG^AUXhzjrzvEdc=# z009sH0T2KI5C8!X009sHfmQ^Z&;R56zgB#34+KB}1V8`;KmY_l00ck)1V8`;dVzqG z|NkH){-76Hgf@Tx2!H?xfB*=900@8p2!H?xfB*pv34DN#f2!H?xfB*=900@8p2!H?xfI#mM zz~}#ar!UbG5C8!X009sH0T2KI5C8!X009taM_@$!ap-8@cS7R4_yoQ$4`d?j*%e=E%QeW&lUeF;(tKOap3;*a~DPe`1h7qi-~ zzMIbJdCf@YH8qzmss)X`JifG+T-->?E33=N*W_j)`PQn;1Y?bQ%IdMGbVlL|OmFQi zOK(G^WU0Kpy7KaETeC;ZQA#T*%F(FwoWzBgKI5&tUe(fSty*HQE$s>B#Ie0Mj|h?^ zaqrGo)vdhQ1Xgm?GP<#N?Pk*SSdPhhR^HOL^QjdW=@V02{Z+fC%azibTBe$=XgMRDDHU`2c3RJpmqsanSIe@n za?2cP6AEsbc6w-#xaH(n!(iNFP^tGnE=Vcj=QQ(^1hKoG`vs%qE^0kRpN=*(vwC5T=k5{JG#)Q(qy|)qU?na3DVRQ_s$9%6mI42>&HAp#9cy0 zN<*7JvRlTbJjN8NBT?z%827ecp$$*ZTGdC}sdXgX%ALE)yT^wFX>5%9oWO#{ z(r5iQ>(QK7A6s~}CR({(|sXvK`y z?4X-QXIbU!aFmYZHv0iP782IQ$Qy%E>FhX1x`>QEvd^%?!9Cd1F}w*3AZaeO3^p@o{c1;hwe)TWh~(4BB^t3_@GBX%w~^2+jM3 zxNPjG6^-mBd~vVL_D7|q@pc{4Fq^Qi2I@~n1!;YntG{4(PNqbL4cXuo(s#%$gU!ff z7?DktxrOnS3vJN)#G2HYlb*+v%%;}3%a*doM+9klntSJ}H3xbs_-sTy{oyM#yT!jb zbFZCbv+1i7sC!)QdZ#eAZZ<=De0L-DOqGnZf1ei%v!!6~j2VcgjA_~6J{b*=`k-#!jhHWu5A6v~rg3G&R_^64pheKEeso!~|E@>>*axDpg&hgUq_> z^R};XpER-AGdFbFwuZFntEXLe)qKsj>?renQR#(=4g<(?DouxwbsZ+jYd=k3M+Uza zdW`#OX!Q8VH~QCwKNd>S?}YzZ^ef@DzCRtw48K3TJ@kd46Jmw?>hSc?Z--tQN_S3s z2fx?%OBctTO$N=Q(gn5bZ0qbhGZrIHlIRts1I`wAHtGtok)R#0S7RNZ8b&FTrYo#- z^^~Urocp8$oB`8yhv3w64Kq^9voW=XNZXA9osybnJ+fDzX__asQhxrFASFp$jk9@; zWE>4wjdXeM@>7B|ImvzIva8WXSuGl-m+Vq$(QH=m)3lpabbO#Q%fo0m$$`l*%#`xA zLa{~WX%^@8qSm5ND`wl*q=hZ(DR)i^(iH+$Xa$z6N=33|Bb#5lCr|k!(ZkCnP=6>0n{m6FWz{4oc=I(SC!_;j{ zre!c%S~^BD)6q7Z+4CZ`kj`p3wU)1@oi(9lNlS-2NxI!`E!XUPd%0HHinYQPNs5eu znkU61G0K@NM7HFEq98{IOtI`RO(9ubXf-h2Y+ezs(ZD1|4;OhlY=%|z^Zm+4dsSH_!X zh~^6B9O2xL+2)KBz9&bmTj0L?WJiORB$+>ajdd;0?;bhMo*aGljx{woj=2q;@J&yS z6kDv=%g*yuBZs%1xH`^x)+wr~?doii@~4*WJ(=H(UBzXD-fC+?FO>6oMki72OMT+x zaj%`)HkV~eg)&jwYVTkdSq~FhC~ipXXlj-Wty;0A{f1aMv(rM0w%gY8HrZTU-qwm* zMXk2++v?;ZdCJ|~Vs|$0?~u40^x;CAX9~*bPH1)?Z(LMQlP7;<&v4k@Gql+ZI=9dG zM~+iOCJQ^0GKY>+L5E2A+9>g|<@`v!cVlDRCs4 zYtzVd%ISv0nQP3kfzSVUex89+5C8!X009sH0T2KI5C8!X009u_1p>(bd!ZN61`q%N z5C8!X009sH0T2KI5C8!X=u80le`h`@1pyEM0T2KI5C8!X009sH0T2LzULb(;|9hbq z(FPCz0T2KI5C8!X009sH0T2KI5a>(*@Beq^gHjLx0T2KI5C8!X009sH0T2KI5a8x5C8!X009sH0T2KI5C8!X009u_OaR~i*O?DWK>!3m00ck) z1V8`;KmY_l00cmw7YN|}|6b@tv;hP_00ck)1V8`;KmY_l00ck)1UeHy{@_y2pL7tsa~009sH0T2KI5C8!X009sH0TAd+0Pp{I z=7UlY009sH0T2KI5C8!X009sH0TAc~0?7Y+p%>8x5C8!X009sH0T2KI5C8!X009u_ zOaQ!3m00ck)1V8`;KmY_l00cmw7YN|}|6b@tv;hP_00ck)1V8`;KmY_l z00ck)1UeHy{@oW=1V8`;KmY_l z00ck)1V8`;K%g@L!Mc;rj`uZF)F{<*$?%Kbs#E>{kHnY$wX$^o>wK6OcuCMLMO zqg8b)ukGl?YC5OqHN&}jc4;lSxRI1M7O&k*%FcZ`ChJ*wOW!7SU ztLqzUiz}-ea`{f$kxmoYm)2HNi)*jTFD74)%T>Lg8CA7V_N$1?MYW*GRc+T%tIS`H zN-s=sA-$N@c8$04dR0rSwQ7mIcAH5!CZ9`Rj7sy9+&w0w@1~6%wW4LyRC9VqHFjLD zuUL&-SzS)PCby84Z>`EM`53LQ)K8xmq{&HcPh|95<*v)i9)7NZAOKzBO$4b6&F~mE zFTC>?qSDG_J9usr3D=8fhQ1K$ry*EBv>-??PIL8(3}o#s79nXv-!7`vT17L=*LpU6 zhX~NlK++2q?r1!e71BgfbcJsYPsl>767v`9QUY zedTtw77p0JM zUea4zs-dMbdDSq|Wip0}Rl~Yf%4%sc77HXet&()cb+5VJ`8M&;EE&uKZEtg3IcDB- zxe_-W_qeCjl{rC5C%F18JCK@n1#8!WKeU1=YVCfkrLU8-g7gXE>kA!xZEAM28}k7? zZBo<5&n7u#@4}2AB@*0cBsM&n%{SjU=NTl;R+40YN?sbQZm}Vu-(c~UJjhJO) zC@#0)zkiFZl(>eh*QQc``m7+S<6J#s4>~qE6x6aOWXX$SPT!{E&6+%HvAH%Q%P#Mt zhW4;6D#sem5kv8Y$+%@w*^HBwb=+B5?TUV%X8p0|7j#=~OtD+jQR(71_qHA9jY^n1U^*2O478-Yz{L_N;9HBkb!iY1c*>=u$u;WPkOgRNgJ*o)OI5C%Y77fy~w)(iG z-rQo@cS+p4yTKoTaZ+l9LUR$*(Y&xIbulJLsY$MW#f~yErCv0B74FK*PhU9A=_KKqn7Onow8WRC~95X=|r}z^>m`S9ju-v z+d<+?V>{>=^w4&0pJ^RIO%8Z%drT|!+mmMQWbOnV<<8YpEuzP4M8~1sGI*MOdl<39 zykY;?DV*~2rvxcUXpY+>veBRR?vJiMbnrR_Q+}RKk-dMi>QJIip=@t;?4A9mCIp)3 z)larCo-f_gDruS>vW(Bsa=L?cUnQ^km|dEAhU{91S&o?{J66y)! zDy8j;T9B{ndCibxyO%B~n<+h0DH)|)RenV`YHD6yuhz19Y4hA%;xes}(+oo|71jLa zD@&UjrBdG5Os$j4#KO$E%QN$HiTU|Wy_m_@vf7mo(S%yf7jl`0YH6{ytLJsKa)0xc z^=^t^C12NBx@X8Q>Q!SiF}Jy5wwPSb%*`Y&&n+Zo<~D1YO0^w>UrGxg1={YI5WHYwOD|ZZ6DRYA~%8t}H*Zc{fwOvrW2%e5r6guVpt$u%0D~ zX)oBFMS{9M9wvdlHy!@*t*?##-~6}#%WOuTT{#u3{waC()dczb+F5xvdBY$zQ_ER- zHdlV8T+uSRp~v@kMDTB1;vxglU(0;^`dbrUPrRvp zh=a=>zwJWjVayS}0sK%*wH(3iGzS}jbO>L0FclcYS#6UH;tw%kxAfxXjgnD)?b3x! zwNkjSKqlh(`Gc~!d(iesD-W!Fq-_UG)UP zF^HnJvy(GRlNHt2v~+FWr3w}|sVcVI%4DsT*+%Zc$IDWwP$(5IZdU2O@ZqV`+wUcB zY@}|=H?(|NezjD&W9*d5gl8L?4v=nJ+tm8Cnx4;YF4Za(tytZ>rWdKwS65b-Z@s$i z?2%^(!OiPr3$5KF);I4iklo+>+&tMS|Nr8H4TtH!eSP8Zz<`G20a6zBgxq%TEk z#3;`H5Bd#L_SXsE{QsZ&I|y+8|9-!`thdhpXJ5od^M7vOPeSAmen0>OKmY_l00ck) z1bTqL=f@-DTTlPibi_U+ihSp(eCk7dI$%8=Ctr2?;o9*&Bz$=0T0QFTs6FuW{)eEY z@ZdVkhlUTX=N|p~-v?J?zXuRpQ=2aWHWdW>%3LD5cp%YfeV^NhZ?E?Zd2{WDNYFFD z199_@@V4K&*V)VNY6|rE?LL%vh644fW^1^%v@FxFwkF@o{-%~8UqpUGs}%I=QbpZz IUv8fIe`u>$WdHyG literal 0 HcmV?d00001 diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..2dab81a --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,40 @@ +{ + "configurations": [ + { + "name": "Arduino", + "includePath": [ + "${workspaceFolder}/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/cores/esp32/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/portable/xtensa/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/arch/xtensa/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/qio_qspi/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/variants/esp32/**", + + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/cores/esp8266/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/ESP8266WiFi/src/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/tools/SDK/include/**", + + "D:/Projects/libraries/ESP-TuyaBLE/src/**", + "D:/Projects/libraries/NimBLE-Arduino/src/**", + "D:/Projects/libraries/NimBLE-Arduino/src/nimble/nimble/host/include/host/**" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE", + "ESP32" + ], + "windowsSdkVersion": "10.0.19041.0", + "compilerPath": "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2302/bin/xtensa-esp32-elf-gcc.exe", + "cStandard": "c11", + "cppStandard": "c++17", + "intelliSenseMode": "gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9de3be8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,69 @@ +{ + "files.associations": { + "\"*.ino\"": "\"cpp\"", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "list": "cpp", + "map": "cpp", + "set": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "netfwd": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "span": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "variant": "cpp" + }, + + "workbench.colorCustomizations": { + "editor.background": "#0f0f0f" // You can change this hex value to any darker shade you prefer + } + +} \ No newline at end of file diff --git a/AHT2x.cpp b/AHT2x.cpp new file mode 100644 index 0000000..b964ef6 --- /dev/null +++ b/AHT2x.cpp @@ -0,0 +1,207 @@ +#include "AHT2x.h" + +#define AHT_STATUS_BUSY 0x01 +#define AHT_STATUS_CALIBRATED 0x10 +#define AHT_CMD_INIT 0xBE +#define AHT_CMD_TRIGGER 0xAC +#define AHT_CMD_RESET 0xBA +#define AHT_CRC_POLYNOMIAL 0x31 +#define AHT_CRC_MSB 0x80 +#define AHT_CRC_INIT 0xFF + + +#ifndef DPRINTF +#define DPRINTF(...) +#endif + +AHT2x aht25(Wire); +AHT2x aht10_0x39(Wire); + +const uint8_t cmd_init[3] = { 0xBE, 0x08, 0x00 }; + +AHT2x::AHT2x(TwoWire &i2c) : _i2c(i2c) {}; + +bool AHT2x::setup(uint8_t address, bool crc) { + m_nAddress = address; + _active_crc = crc; + m_nErrorCount = 0; + m_nTemp = -9999; + m_nRH = -9999; + bRequested = false; + m_bSensor = false; + m_bScan = false; + tickRequested = millis(); + delay(40); + + if (scan()) { + while (!isCalibrated()) { + calibrate(); + } + + requestMeasurement(tickRequested); + delay(82); + humid[3] = -9999; + temp[3] = -9999; + if (readSensor(tickRequested + 82)) { + for (int i = 0; i < 3; i++) { + humid[i] = humid[3]; + temp[i] = temp[3]; + } + } + return true; + } + return false; +} + +bool AHT2x::scan() { + //DPRINTF("AHTx0 - Scanning AHTx0 Device at %X\n", m_nAddress); + int i; + for (i = 0; i < 5; i++) { + Wire.beginTransmission(m_nAddress); + if (Wire.endTransmission() == 0) { + DPRINTF("AHTx0 - FOUND I2C Device at %X\n", m_nAddress); + bool ret = false; + //sendCommand(m_nAddress, 0xBE, 0x08, 0x00)) { // AHT20 init command + uint8_t cmd[3] = { 0xBE, 0x08, 0x00 }; + _i2c.beginTransmission(m_nAddress); + _i2c.write((uint8_t *)cmd_init, 3); + if (_i2c.endTransmission() == 0) { + m_bSensor = true; + DPRINTF("AHT2x - Found AHT2x at %X\n", m_nAddress); + break; + } + } + delay(2); + } + if (i == 5) { + DPRINTF("AHTx0 - I2C Transmission Error at %X\n", m_nAddress); + } + return m_bSensor; +} + +bool AHT2x::isReady() { + if (status() & AHT_STATUS_BUSY) { + return false; + } + return measure(); +} + +bool AHT2x::isCalibrated() { + return status() & AHT_STATUS_CALIBRATED; +} + +void AHT2x::reset() { + uint8_t cmd = AHT_CMD_RESET; + _i2c.beginTransmission(m_nAddress); + _i2c.write(cmd); + _i2c.endTransmission(); + delay(20); + while (!isCalibrated()) { + calibrate(); + } +} + +uint8_t AHT2x::status() { + _i2c.requestFrom(m_nAddress, (uint8_t)6); + for (int i = 0; i < 6; i++) { + _buf[i] = _i2c.read(); + } + if (!_active_crc || (crc8() == _buf[6])) { + return _buf[0]; + } + return AHT_STATUS_BUSY; +} + +void AHT2x::calibrate() { + uint8_t cmd[] = {AHT_CMD_INIT, 0x08, 0x00}; + _i2c.beginTransmission(m_nAddress); + _i2c.write(cmd, 3); + _i2c.endTransmission(); + delay(10); +} + +void AHT2x::requestMeasurement(unsigned long tick) { + uint8_t cmd[] = {AHT_CMD_TRIGGER, 0x33, 0x00}; + _i2c.beginTransmission(m_nAddress); + _i2c.write(cmd, 3); + _i2c.endTransmission(); + tickRequested = tick; + bRequested = true; +} + +bool AHT2x::readSensor(unsigned long tick) { + if (m_bScan) { + // Re-Scan sensor for any hardware change or failure + scan(); + m_bScan = false; + } + + bool ret = false; + if (bRequested) { + if (tick - tickRequested > 80) { + _i2c.requestFrom(m_nAddress, (uint8_t)6); + ret = true; + for (int i = 0; i < 6; i++) { + if (Wire.available()) { + _buf[i] = _i2c.read(); + } else { + ret = false; // If data is unavailable, return false + break; + } + } + + if (ret && (!_active_crc || (crc8() == _buf[6]))) { + humid[0] = humid[1]; + humid[1] = humid[2]; + humid[2] = humid[3]; + uint32_t hum32 = (_buf[1] << 12) | (_buf[2] << 4) | (_buf[3] >> 4); + humid[3] = (int16_t) roundf(hum32 * 10000.0f / 0x100000); + if (m_nRH > 2000 && m_nRH <= 10000) { + if (humid[3] - m_nRH < -1500 || humid[3] - m_nRH> 1500) + humid[3] = m_nRH; + } + m_nRH = (int16_t)((humid[0] + humid[1] + humid[2] + humid[3]) / 4); + + temp[0] = temp[1]; + temp[1] = temp[2]; + temp[2] = temp[3]; + uint32_t temp32 = ((_buf[3] & 0xF) << 16) | (_buf[4] << 8) | _buf[5]; + temp[3] = (int16_t) roundf(temp32 * 20000.0f / 0x100000 - 5000); + if (m_nTemp > 2000 && m_nTemp <= 3000) { + if (temp[3] - m_nTemp < -1000 || temp[3] - m_nTemp> 1000) + temp[3] = m_nTemp; + } + m_nTemp = (int16_t)((temp[0] + temp[1] + temp[2] + temp[3]) / 4); + m_nErrorCount = 0; + ret = true; + } else { + if (++m_nErrorCount > 4) { + m_nTemp = -9999; + m_nRH = -9999; + m_nErrorCount = 0; + } + } + } else { + // tick - tickRequested <= 80 + // do nothing; + return false; + } + } + requestMeasurement(tick); + return ret; +} + +uint8_t AHT2x::crc8() { + uint8_t crc = AHT_CRC_INIT; + for (int i = 0; i < 6; i++) { + crc ^= _buf[i]; + for (int j = 0; j < 8; j++) { + if (crc & AHT_CRC_MSB) { + crc = (crc << 1) ^ AHT_CRC_POLYNOMIAL; + } else { + crc <<= 1; + } + } + } + return crc; +} diff --git a/AHT2x.h b/AHT2x.h new file mode 100644 index 0000000..e35cde2 --- /dev/null +++ b/AHT2x.h @@ -0,0 +1,43 @@ +#ifndef __AHT2x_H +#define __AHT2x_H +#include + +#define AHT_I2C_ADDR 0x38 + +class AHT2x { +public: + AHT2x(TwoWire &i2c); + bool setup(uint8_t address = AHT_I2C_ADDR, bool crc = false); + bool scan(); + bool isReady(); + bool isCalibrated(); + void reset(); + bool readSensor(unsigned long tick); + inline int16_t getHumidity() const { return m_nRH; } + inline int16_t getTemperature() const { return m_nTemp; } + inline void setScanFlag(bool bScan) { m_bScan = bScan; } + inline bool sensor() { return m_bSensor; } + +private: + TwoWire &_i2c; + uint8_t m_nAddress; + bool _active_crc; + uint8_t _buf[7]; + int16_t m_nRH, m_nTemp; + int16_t humid[4], temp[4]; + + uint8_t status(); + void calibrate(); + bool measure(); + uint8_t crc8(); + void requestMeasurement(unsigned long tick); + + bool bRequested; + bool m_bSensor; + bool m_bScan; + unsigned long tickRequested; + uint8_t m_nErrorCount; +}; + +extern class AHT2x aht25, aht10_0x39; +#endif \ No newline at end of file diff --git a/BLEScan.cpp b/BLEScan.cpp new file mode 100644 index 0000000..f710691 --- /dev/null +++ b/BLEScan.cpp @@ -0,0 +1,433 @@ +#include +#include +#include +#include "Config.h" +#include "HermitCrab.h" +#include "ConnectWiFi.h" + +CBLEScan ble; +std::string serviceData; +char BLE_SSID[32]; +char BLE_PW[32]; + +#define TAG_BLE "BLE_SCAN" + +#define ADVERTISE_INTERVAL_MS 5000 // Expected advertisement interval +#define SCAN_SHORT_MS 300 // Short scan time +#define SCAN_LONG_MS 12000 // Longer scan if missed + +// Xiaomi - The remote service and characteristic UUIDs +static BLEUUID serviceUUID("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6"); +static BLEUUID charUUID("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6"); + +//#define TUYA_BLE_NAME "THB1-xxxxxx" // sensor name +//#define SENSOR_BLE_NAME +//#define SENSOR_BLE_MAC "38:1F:8D:77:73:34" // Tuya sensor MAC address +#define WIFI_BLE_NAME "HC_" // Name of PC/Android BLE advertiser + +// UUIDs for environmental sensing and characteristics +#define ENV_SENSING_UUID "181A" // Environmental Sensing UUID +#define TEMP_UUID "2A6E" // Temperature UUID +#define HUMIDITY_UUID "2A6F" // Humidity UUID + +// Tuya sensor advertisement data structure (in network order) +typedef struct __attribute__((packed)) _bthome_t { + //uint8_t flag[3]; // Advertise type flags + uint8_t info; // = 0x40 BtHomeID_Info (not encrypted) + uint8_t p_id; // = BtHomeID_PacketId + uint8_t pid; // PacketId (measurement count) + + uint8_t b_id; // = BtHomeID_battery + uint8_t battery_level; // 0..100 % + uint8_t t_id; // = BtHomeID_temperature + int16_t temp; // x 0.001 degree + uint8_t h_id; // = BtHomeID_humidity + uint16_t humid; // x 0.01 % + uint8_t v_id; // = BtHomeID_voltage + uint16_t battery_mv; // x 0.001 V +} bthome_t; + +// Callback when a device is found during scan +class HCScanCallbacks : public NimBLEScanCallbacks { + /** + * @brief Called when a new device is discovered, before the scan result is received (if applicable). + * @param [in] advertisedDevice The device which was discovered. + */ + void onDiscovered(const NimBLEAdvertisedDevice* advertisedDevice) override { + static uint8_t flag = 0; + static char ssid[32]; + static char pw[32]; + if (isWiFiConnected()) return; + + // Retrieve manufacturer data (SSID or password) + const std::string& manufacturerData = advertisedDevice->getManufacturerData(); + + // Check for specific manufacturer data signatures (SIGNATURE1 for SSID, SIGNATURE2 for password) + uint16_t manufacturerID = * (uint16_t *) &manufacturerData[0]; + if (manufacturerID == SIGNATURE2 - 1) { // For SSID (SIGNATURE1) + strncpy(ssid, (char *) &manufacturerData[2], std::min(sizeof(ssid) - 1, manufacturerData.length() - 1)); + ssid[sizeof(ssid) - 1] = '\0'; // Ensure null termination + flag |= 1; + //DPRINTF("BLE Scan - SSID(\"%s\")\n", (char *) &manufacturerData[2]); + } else if (manufacturerID == SIGNATURE2) { // For password (SIGNATURE2) + strncpy(pw, (char *)&manufacturerData[2], std::min(sizeof(pw) - 1, manufacturerData.length() - 1)); + pw[sizeof(pw) - 1] = '\0'; // Ensure null termination + flag |= 2; + //DPRINTF("BLE Scan - PW(\"%s\")\n", (char *) &manufacturerData[2]); + } else { + flag = 0; + } + + if (flag == 3) { + strncpy(BLE_SSID, ssid, sizeof(BLE_SSID) - 1); + BLE_SSID[sizeof(BLE_SSID) - 1] = 0; + strncpy(BLE_PW, pw, sizeof(BLE_PW) - 1); + BLE_PW[sizeof(BLE_PW) - 1] = 0; + flag = 0; + } + }; + + + /** + * @brief Called when a new scan result is complete, including scan response data (if applicable). + * @param [in] advertisedDevice The device for which the complete result is available. + */ + void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override { + uint16_t len; + bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData(&len); + // Tuya + if (len == sizeof(bthome_t) && pTuyaData->info == 0x40) { + if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) { + // Look for the custom Tuya sensor data structure + ble.setData(pTuyaData->temp, pTuyaData->humid, pTuyaData->battery_level); + } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) { + // Look for the custom Tuya sensor data structure + ble.setData2(pTuyaData->temp, pTuyaData->humid, pTuyaData->battery_level); + } + } else + // InkBird + if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD || + config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { + // Process Inkbird Sensors + std::string manufacturerData = advertisedDevice->getManufacturerData(); + if (manufacturerData.length() >= 7) { + const uint8_t* data = reinterpret_cast(manufacturerData.data()); + + // Inkbird data format: [unknown][unknown][temp_LSB][temp_MSB][humidity][voltage_LSB][voltage_MSB] + int16_t temp = (data[2] | (data[3] << 8)); // Little-endian, scale by 0.01 + uint16_t humi = data[4]; // Humidity byte (0-100%) + uint16_t batteryLevel = map(data[5] | (data[6] << 8), 2300, 3200, 0, 100); // Convert voltage to battery % + + if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { + ble.setData(temp, humi, batteryLevel); + } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { + ble.setData2(temp, humi, batteryLevel); + } + } + } + }; + + /** + * @brief Called when a scan operation ends. + * @param [in] scanResults The results of the scan that ended. + * @param [in] reason The reason code for why the scan ended. + */ + void onScanEnd(const NimBLEScanResults& scanResults, int reason) override {}; +} hcScanCallbacks; + +// Connection based Notification from Xiaomi devices +static void notifyCallback( + BLERemoteCharacteristic* pBLERemoteCharacteristic, + uint8_t* pData, + size_t length, + bool isNotify) +{ + uint16_t voltage; + if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { + voltage = pData[3] | (pData[4] << 8); // little endian + ble.setData(pData[0] | (pData[1] << 8), pData[2] * 100, map(voltage, 2300, 3200, 0, 100)); + } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { + voltage = pData[3] | (pData[4] << 8); // little endian + ble.setData2(pData[0] | (pData[1] << 8), pData[2] * 100, map(voltage, 2300, 3200, 0, 100)); + } +} + +// Function to setup BLE +void CBLEScan::setupConnect(uint64_t addr, uint64_t addr2) { + m_nAddr = addr; + m_nAddr2 = addr2; + + m_nLatestBLEAdvertise = + m_nLatestBLEAdvertise2 = millis(); + m_nLastReceive = + m_nLastReceive2 = m_nLatestBLEAdvertise; + m_bDataReceived = + m_bDataReceived2 = false; + + NimBLEDevice::init(""); + status.nFlags &= ~FLAG_BLE_BATT; + status.nFlags &= ~FLAG_BLE_NODATA; + status.nFlags &= ~FLAG_BLE_LOST; + + m_nTemp = -9999; + m_nHumid = -9999; + m_nBatteryLevel = 0; + m_nTemp2 = -9999; + m_nHumid2 = -9999; + m_nBatteryLevel2 = 0; + m_bConnected = false; + m_bConnectionInProgress = false; + pClient = nullptr; + + if ( config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { + connect(NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC), false); + } else if ( config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { + connect(NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC), false); + } +} + +void CBLEScan::setupScan() { + NimBLEAddress cTargetAddress; + + if (config.nBLEScanInterval == 143 || config.nBLEScanInterval == 77 || + config.nBLEScanInterval == 139 || config.nBLEScanInterval == 91) + m_nInterval = config.nBLEScanInterval; + else + m_nInterval = 143; + + serviceData.reserve(256); + NimBLEScan* pBLEScan = NimBLEDevice::getScan(); + pBLEScan->setScanCallbacks((NimBLEScanCallbacks *) &hcScanCallbacks, true); + pBLEScan->setActiveScan(false); + pBLEScan->setDuplicateFilter(false); + pBLEScan->setMaxResults(0); + m_bContinuousScan = true; + m_bScanStarted = false; + + if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA || + config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { + cTargetAddress = NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC); + pBLEScan->setTargetAddress(cTargetAddress); + startScan(); + } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA || + config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { + cTargetAddress = NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC); + pBLEScan->setTargetAddress(cTargetAddress); + startScan(); + } else if (!isWiFiConnected()) { + startScan(); + } +} + +void CBLEScan::startScan() { + DPRINTF("Starting BLE scan... %d(%d)\n", m_nInterval, 5000 % m_nInterval); + NimBLEScan* pBLEScan = NimBLEDevice::getScan(); + pBLEScan->setInterval(m_nInterval); // Interval between scan windows in ms + pBLEScan->setWindow(m_nInterval - 3); // Length of time the scanner listens in ms + if (m_bContinuousScan) { + // 85(15), 77(5), 82(2), 61(2) 122(2), 139(4), 91(5), 143(5), 135(-5) + pBLEScan->start(0, false, false); // Continuous scan + } else { + pBLEScan->start(12000, false, true); // Continuous scan + } + m_bScanStarted = true; +} + +bool CBLEScan::connect(NimBLEAddress pAddress, bool bAsync) { + DPRINTLN("BLE Connect: Connecting..."); + if (pClient == nullptr) { + pClient = NimBLEDevice::createClient(); + pClient->setConnectTimeout(30000); + } + + m_bConnectionInProgress = false; + bool ret = pClient->connect(pAddress, true, bAsync, true); + + if (bAsync) { + m_bConnectionInProgress = true; + m_bConnected = false; + } else { + m_bConnected = ret; + } + + if (!bAsync && m_bConnected) { + NimBLERemoteService* pRemoteService = pClient->getService(serviceUUID); + if (!pRemoteService) { + DPRINTLN("BLE Connect: Service not found"); + return false; + } + + pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID); + if (!pRemoteCharacteristic) { + DPRINTLN("BLE Connect: Characteristic not found"); + return false; + } + + pRemoteCharacteristic->subscribe(true, notifyCallback); + DPRINTF("BLE Connect: Connected to %s\n", pAddress.toString().c_str()); + } + return m_bConnected; +} + +void CBLEScan::loop(unsigned long clock) { + unsigned long gap; + + if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA || + config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { + if (!m_bConnected) { + if (pClient->isConnected()) { + m_bConnectionInProgress = false; + NimBLERemoteService* pRemoteService = pClient->getService(serviceUUID); + if (!pRemoteService) { + DPRINTLN("BLE Connect: Service not found"); + return; + } + + pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID); + if (!pRemoteCharacteristic) { + DPRINTLN("BLE Connect: Characteristic not found"); + return; + } + + pRemoteCharacteristic->subscribe(true, notifyCallback); + DPRINTLN("BLE Connect: Connected within the Loop"); + m_bConnected = true; + } else if (!m_bConnectionInProgress) { + if ( config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { + connect(NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC), true); + } else if ( config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { + connect(NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC), true); + } + } + } + } else { + if (m_bConnected) { + pClient->disconnect(); + m_bConnected = false; + m_bConnectionInProgress = false; + } + } + + if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA || + config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { + if (m_bScanStarted) { + gap = clock - m_nLatestBLEAdvertise; + if (gap < 6050) { + status.nFlags &= ~FLAG_BLE_NODATA; + status.nFlags &= ~FLAG_BLE_LOST; + } + else if (gap < 8150) { + status.nFlags |= FLAG_BLE_NODATA; + status.nFlags &= ~FLAG_BLE_LOST; + if (config.bBLETest) m_nHumid = 0; + } + else if (gap > 60100 && gap < 63200) { + //DPRINTF("GAP Reached... %d", gap); + status.nFlags |= (FLAG_BLE_NODATA | FLAG_BLE_LOST); + m_nTemp = -9999; + m_nHumid = -9999; + m_nBatteryLevel = 0; + } + } else { + startScan(); + } + } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA || + config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { + if (m_bScanStarted) { + gap = clock - m_nLatestBLEAdvertise2; + if (gap > 6050 && gap < 8150) { + if (config.bBLETest) m_nHumid2 = 0; + } + else if (gap > 60100 && gap < 63200) { + m_nTemp2 = -9999; + m_nHumid2 = -9999; + m_nBatteryLevel2 = 0; + } + } else { + startScan(); + } + } +#ifdef BLE_DEBUG + { + static int col = 0; + static int hit = 0; + static int miss = 0; + static int hit_total = 0; + static int miss_total = 0; + + + if (m_bDataReceived) { + gap = m_nLatestBLEAdvertise - m_nLastReceive; + int cnt = (gap + 2500)/5000 - 1; + for (int i = 0; i < cnt; i++) + { + Serial.print(cnt); + miss++; + if (++col >=60) { + hit_total += hit; + miss_total += miss; + DPRINTF(" Hit: %2d, Miss: %2d Total(Hit: %3d, Miss: %3d, Ratio %.1f%%) Mem: %d\n", + hit, miss, hit_total, miss_total, (float)hit_total * 100.0f / (hit_total + miss_total), + ESP.getFreeHeap()); + col = 0; + hit = 0; + miss = 0; + } + } + Serial.print('.'); + hit++; + if (++col >=60) { + hit_total += hit; + miss_total += miss; + DPRINTF(" Hit: %d, Miss: %d Total(Hit: %d, Miss: %d, Ratio %.1f%%) Mem: %d\n", + hit, miss, hit_total, miss_total, (float)hit_total * 100.0f / (hit_total + miss_total), + ESP.getFreeHeap()); + col = 0; + hit = 0; + miss = 0; + } + + m_bDataReceived = false; + status.nFlags &= ~FLAG_BLE_NODATA; + } + /* + // Interval Scan + // { + // NimBLEScan* pScan = NimBLEDevice::getScan(); + // if (g_bBLEDataReceived) { + // Serial.printf("BLE(%d) - Temp: %.2f°C Hum: %.2f%% Batt: %d Mem: %d (Stop)\n", + // tickMillis - lastReceived, + // g_nBLETemp / 100.0f, + // g_nBLEHumid / 100.0f, + // g_nBLEBatt, + // ESP.getFreeHeap() ); + // status.nFlags &= ~FLAG_BLE_NODATA; + // g_bBLEDataReceived = false; + // if (pScan->isScanning()) { + // pScan->stop(); + // pScan->clearResults(); + // } + // lastErrorMessage = tickMillis; + // lastReceived = lastTickMillis; + // } else { + // unsigned long gap = tickMillis - g_lastBLEAdvertise; + // if (!pScan->isScanning()) { + // if (gap > 4910 && gap < 4960) { + // pScan->start(200, false, true); + // Serial.printf("Starting Scan for 200ms at %d\n", tickMillis - g_lastBLEAdvertise); + // } else if (gap > 9500) { + // pScan->start(5200, false, true); + // Serial.printf("Starting Scan for 5200ms at %d\n", tickMillis - g_lastBLEAdvertise); + // } + // } + // if (gap > 10050 && tickMillis - lastErrorMessage > 10050) { + // Serial.printf("No BLE Data for % seconds\n", tickMillis - lastErrorMessage); + // status.nFlags |= FLAG_BLE_NODATA; + // lastErrorMessage = tickMillis; + // } + // } + // } + */ + } +#endif +} diff --git a/BLEScan.h b/BLEScan.h new file mode 100644 index 0000000..5526be5 --- /dev/null +++ b/BLEScan.h @@ -0,0 +1,72 @@ +#ifndef __BLE_SCAN_H +#define __BLE_SCAN_H + +class NimBLEClient; +class NimBLERemoteCharacteristic; +class NimBLEAddress; + +class CBLEScan { +public: + void setupConnect(uint64_t addr, uint64_t addr2); + void setupScan(); + void loop(unsigned long clock); + void startScan(); + bool connect(NimBLEAddress addr, bool bAsync); + inline int16_t getTemp() { return m_nTemp; }; + inline int16_t getHumid() { return m_nHumid; }; + inline uint8_t getBatteyLevel() { return m_nBatteryLevel; }; + inline int16_t getTemp2() { return m_nTemp2; }; + inline int16_t getHumid2() { return m_nHumid2; }; + inline uint8_t getBatteyLevel2() { return m_nBatteryLevel2; }; + + inline void setData(int16_t temp, uint16_t humid, uint8_t bLevel) { + m_nTemp = temp; + m_nHumid = humid; + m_nBatteryLevel = bLevel; + m_bDataReceived = true; + m_nLastReceive = m_nLatestBLEAdvertise; + m_nLatestBLEAdvertise = millis(); + } + + inline void setData2(int16_t temp, uint16_t humid, uint8_t bLevel) { + m_nTemp2 = temp; + m_nHumid2 = humid; + m_nBatteryLevel2 = bLevel; + m_bDataReceived2 = true; + m_nLastReceive2 = m_nLatestBLEAdvertise2; + m_nLatestBLEAdvertise2 = millis(); + } + + inline void setScanType(bool bContinuous) { m_bContinuousScan = bContinuous; }; + +private: + bool m_bContinuousScan; + bool m_bScanStarted; + bool m_bConnected; + bool m_bConnectionInProgress; + int16_t m_nInterval; + uint64_t m_nAddr, m_nAddr2; + + int16_t m_nTemp; + int16_t m_nHumid; + uint8_t m_nBatteryLevel; + + int16_t m_nTemp2; + int16_t m_nHumid2; + uint8_t m_nBatteryLevel2; + + unsigned long m_nLatestBLEAdvertise; + unsigned long m_nLastReceive; + bool m_bDataReceived; + + unsigned long m_nLatestBLEAdvertise2; + unsigned long m_nLastReceive2; + bool m_bDataReceived2; + + NimBLEClient *pClient; + NimBLERemoteCharacteristic *pRemoteCharacteristic; +}; + +extern CBLEScan ble; + +#endif \ No newline at end of file diff --git a/BearSSL.h b/BearSSL.h new file mode 100644 index 0000000..8e7a31f --- /dev/null +++ b/BearSSL.h @@ -0,0 +1,1346 @@ +/* + * Copyright (c) 2016 Thomas Pornin + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef BR_BEARSSL_HASH_H__ +#define BR_BEARSSL_HASH_H__ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** \file bearssl_hash.h + * + * # Hash Functions + * + * This file documents the API for hash functions. + * + * + * ## Procedural API + * + * For each implemented hash function, of name "`xxx`", the following + * elements are defined: + * + * - `br_xxx_vtable` + * + * An externally defined instance of `br_hash_class`. + * + * - `br_xxx_SIZE` + * + * A macro that evaluates to the output size (in bytes) of the + * hash function. + * + * - `br_xxx_ID` + * + * A macro that evaluates to a symbolic identifier for the hash + * function. Such identifiers are used with HMAC and signature + * algorithm implementations. + * + * NOTE: for the "standard" hash functions defined in [the TLS + * standard](https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1), + * the symbolic identifiers match the constants used in TLS, i.e. + * 1 to 6 for MD5, SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512, + * respectively. + * + * - `br_xxx_context` + * + * Context for an ongoing computation. It is allocated by the + * caller, and a pointer to it is passed to all functions. A + * context contains no interior pointer, so it can be moved around + * and cloned (with a simple `memcpy()` or equivalent) in order to + * capture the function state at some point. Computations that use + * distinct context structures are independent of each other. The + * first field of `br_xxx_context` is always a pointer to the + * `br_xxx_vtable` structure; `br_xxx_init()` sets that pointer. + * + * - `br_xxx_init(br_xxx_context *ctx)` + * + * Initialise the provided context. Previous contents of the structure + * are ignored. This calls resets the context to the start of a new + * hash computation; it also sets the first field of the context + * structure (called `vtable`) to a pointer to the statically + * allocated constant `br_xxx_vtable` structure. + * + * - `br_xxx_update(br_xxx_context *ctx, const void *data, size_t len)` + * + * Add some more bytes to the hash computation represented by the + * provided context. + * + * - `br_xxx_out(const br_xxx_context *ctx, void *out)` + * + * Complete the hash computation and write the result in the provided + * buffer. The output buffer MUST be large enough to accommodate the + * result. The context is NOT modified by this operation, so this + * function can be used to get a "partial hash" while still keeping + * the possibility of adding more bytes to the input. + * + * - `br_xxx_state(const br_xxx_context *ctx, void *out)` + * + * Get a copy of the "current state" for the computation so far. For + * MD functions (MD5, SHA-1, SHA-2 family), this is the running state + * resulting from the processing of the last complete input block. + * Returned value is the current input length (in bytes). + * + * - `br_xxx_set_state(br_xxx_context *ctx, const void *stb, uint64_t count)` + * + * Set the internal state to the provided values. The 'stb' and + * 'count' values shall match that which was obtained from + * `br_xxx_state()`. This restores the hash state only if the state + * values were at an appropriate block boundary. This does NOT set + * the `vtable` pointer in the context. + * + * Context structures can be discarded without any explicit deallocation. + * Hash function implementations are purely software and don't reserve + * any resources outside of the context structure itself. + * + * + * ## Object-Oriented API + * + * For each hash function that follows the procedural API described + * above, an object-oriented API is also provided. In that API, function + * pointers from the vtable (`br_xxx_vtable`) are used. The vtable + * incarnates object-oriented programming. An introduction on the OOP + * concept used here can be read on the BearSSL Web site:
+ *    [https://www.bearssl.org/oop.html](https://www.bearssl.org/oop.html) + * + * The vtable offers functions called `init()`, `update()`, `out()`, + * `set()` and `set_state()`, which are in fact the functions from + * the procedural API. That vtable also contains two informative fields: + * + * - `context_size` + * + * The size of the context structure (`br_xxx_context`), in bytes. + * This can be used by generic implementations to perform dynamic + * context allocation. + * + * - `desc` + * + * A "descriptor" field that encodes some information on the hash + * function: symbolic identifier, output size, state size, + * internal block size, details on the padding. + * + * Users of this object-oriented API (in particular generic HMAC + * implementations) may make the following assumptions: + * + * - Hash output size is no more than 64 bytes. + * - Hash internal state size is no more than 64 bytes. + * - Internal block size is a power of two, no less than 16 and no more + * than 256. + * + * + * ## Implemented Hash Functions + * + * Implemented hash functions are: + * + * | Function | Name | Output length | State length | + * | :-------- | :------ | :-----------: | :----------: | + * | MD5 | md5 | 16 | 16 | + * | SHA-1 | sha1 | 20 | 20 | + * | SHA-224 | sha224 | 28 | 32 | + * | SHA-256 | sha256 | 32 | 32 | + * | SHA-384 | sha384 | 48 | 64 | + * | SHA-512 | sha512 | 64 | 64 | + * | MD5+SHA-1 | md5sha1 | 36 | 36 | + * + * (MD5+SHA-1 is the concatenation of MD5 and SHA-1 computed over the + * same input; in the implementation, the internal data buffer is + * shared, thus making it more memory-efficient than separate MD5 and + * SHA-1. It can be useful in implementing SSL 3.0, TLS 1.0 and TLS + * 1.1.) + * + * + * ## Multi-Hasher + * + * An aggregate hasher is provided, that can compute several standard + * hash functions in parallel. It uses `br_multihash_context` and a + * procedural API. It is configured with the implementations (the vtables) + * that it should use; it will then compute all these hash functions in + * parallel, on the same input. It is meant to be used in cases when the + * hash of an object will be used, but the exact hash function is not + * known yet (typically, streamed processing on X.509 certificates). + * + * Only the standard hash functions (MD5, SHA-1, SHA-224, SHA-256, SHA-384 + * and SHA-512) are supported by the multi-hasher. + * + * + * ## GHASH + * + * GHASH is not a generic hash function; it is a _universal_ hash function, + * which, as the name does not say, means that it CANNOT be used in most + * places where a hash function is needed. GHASH is used within the GCM + * encryption mode, to provide the checked integrity functionality. + * + * A GHASH implementation is basically a function that uses the type defined + * in this file under the name `br_ghash`: + * + * typedef void (*br_ghash)(void *y, const void *h, const void *data, size_t len); + * + * The `y` pointer refers to a 16-byte value which is used as input, and + * receives the output of the GHASH invocation. `h` is a 16-byte secret + * value (that serves as key). `data` and `len` define the input data. + * + * Three GHASH implementations are provided, all constant-time, based on + * the use of integer multiplications with appropriate masking to cancel + * carry propagation. + */ + +/** + * \brief Class type for hash function implementations. + * + * A `br_hash_class` instance references the methods implementing a hash + * function. Constant instances of this structure are defined for each + * implemented hash function. Such instances are also called "vtables". + * + * Vtables are used to support object-oriented programming, as + * described on [the BearSSL Web site](https://www.bearssl.org/oop.html). + */ +typedef struct br_hash_class_ br_hash_class; +struct br_hash_class_ { + /** + * \brief Size (in bytes) of the context structure appropriate for + * computing this hash function. + */ + size_t context_size; + + /** + * \brief Descriptor word that contains information about the hash + * function. + * + * For each word `xxx` described below, use `BR_HASHDESC_xxx_OFF` + * and `BR_HASHDESC_xxx_MASK` to access the specific value, as + * follows: + * + * (hf->desc >> BR_HASHDESC_xxx_OFF) & BR_HASHDESC_xxx_MASK + * + * The defined elements are: + * + * - `ID`: the symbolic identifier for the function, as defined + * in [TLS](https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1) + * (MD5 = 1, SHA-1 = 2,...). + * + * - `OUT`: hash output size, in bytes. + * + * - `STATE`: internal running state size, in bytes. + * + * - `LBLEN`: base-2 logarithm for the internal block size, as + * defined for HMAC processing (this is 6 for MD5, SHA-1, SHA-224 + * and SHA-256, since these functions use 64-byte blocks; for + * SHA-384 and SHA-512, this is 7, corresponding to their + * 128-byte blocks). + * + * The descriptor may contain a few other flags. + */ + uint32_t desc; + + /** + * \brief Initialisation method. + * + * This method takes as parameter a pointer to a context area, + * that it initialises. The first field of the context is set + * to this vtable; other elements are initialised for a new hash + * computation. + * + * \param ctx pointer to (the first field of) the context. + */ + void (*init)(const br_hash_class **ctx); + + /** + * \brief Data injection method. + * + * The `len` bytes starting at address `data` are injected into + * the running hash computation incarnated by the specified + * context. The context is updated accordingly. It is allowed + * to have `len == 0`, in which case `data` is ignored (and could + * be `NULL`), and nothing happens. + * on the input data. + * + * \param ctx pointer to (the first field of) the context. + * \param data pointer to the first data byte to inject. + * \param len number of bytes to inject. + */ + void (*update)(const br_hash_class **ctx, const void *data, size_t len); + + /** + * \brief Produce hash output. + * + * The hash output corresponding to all data bytes injected in the + * context since the last `init()` call is computed, and written + * in the buffer pointed to by `dst`. The hash output size depends + * on the implemented hash function (e.g. 16 bytes for MD5). + * The context is _not_ modified by this call, so further bytes + * may be afterwards injected to continue the current computation. + * + * \param ctx pointer to (the first field of) the context. + * \param dst destination buffer for the hash output. + */ + void (*out)(const br_hash_class *const *ctx, void *dst); + + /** + * \brief Get running state. + * + * This method saves the current running state into the `dst` + * buffer. What constitutes the "running state" depends on the + * hash function; for Merkle-Damgård hash functions (like + * MD5 or SHA-1), this is the output obtained after processing + * each block. The number of bytes injected so far is returned. + * The context is not modified by this call. + * + * \param ctx pointer to (the first field of) the context. + * \param dst destination buffer for the state. + * \return the injected total byte length. + */ + uint64_t (*state)(const br_hash_class *const *ctx, void *dst); + + /** + * \brief Set running state. + * + * This methods replaces the running state for the function. + * + * \param ctx pointer to (the first field of) the context. + * \param stb source buffer for the state. + * \param count injected total byte length. + */ + void (*set_state)(const br_hash_class **ctx, + const void *stb, uint64_t count); +}; + +#ifndef BR_DOXYGEN_IGNORE +#define BR_HASHDESC_ID(id) ((uint32_t)(id) << BR_HASHDESC_ID_OFF) +#define BR_HASHDESC_ID_OFF 0 +#define BR_HASHDESC_ID_MASK 0xFF + +#define BR_HASHDESC_OUT(size) ((uint32_t)(size) << BR_HASHDESC_OUT_OFF) +#define BR_HASHDESC_OUT_OFF 8 +#define BR_HASHDESC_OUT_MASK 0x7F + +#define BR_HASHDESC_STATE(size) ((uint32_t)(size) << BR_HASHDESC_STATE_OFF) +#define BR_HASHDESC_STATE_OFF 15 +#define BR_HASHDESC_STATE_MASK 0xFF + +#define BR_HASHDESC_LBLEN(ls) ((uint32_t)(ls) << BR_HASHDESC_LBLEN_OFF) +#define BR_HASHDESC_LBLEN_OFF 23 +#define BR_HASHDESC_LBLEN_MASK 0x0F + +#define BR_HASHDESC_MD_PADDING ((uint32_t)1 << 28) +#define BR_HASHDESC_MD_PADDING_128 ((uint32_t)1 << 29) +#define BR_HASHDESC_MD_PADDING_BE ((uint32_t)1 << 30) +#endif + +/* + * Specific hash functions. + * + * Rules for contexts: + * -- No interior pointer. + * -- No pointer to external dynamically allocated resources. + * -- First field is called 'vtable' and is a pointer to a + * const-qualified br_hash_class instance (pointer is set by init()). + * -- SHA-224 and SHA-256 contexts are identical. + * -- SHA-384 and SHA-512 contexts are identical. + * + * Thus, contexts can be moved and cloned to capture the hash function + * current state; and there is no need for any explicit "release" function. + */ + +/** + * \brief Symbolic identifier for MD5. + */ +#define br_md5_ID 1 + +/** + * \brief MD5 output size (in bytes). + */ +#define br_md5_SIZE 16 + +/** + * \brief Constant vtable for MD5. + */ +extern const br_hash_class br_md5_vtable; + +/** + * \brief MD5 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[64]; + uint64_t count; + uint32_t val[4]; +#endif +} br_md5_context; + +/** + * \brief MD5 context initialisation. + * + * This function initialises or resets a context for a new MD5 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_md5_init(br_md5_context *ctx); + +/** + * \brief Inject some data bytes in a running MD5 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_md5_update(br_md5_context *ctx, const void *data, size_t len); + +/** + * \brief Compute MD5 output. + * + * The MD5 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_md5_out(const br_md5_context *ctx, void *out); + +/** + * \brief Save MD5 running state. + * + * The running state for MD5 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_md5_state(const br_md5_context *ctx, void *out); + +/** + * \brief Restore MD5 running state. + * + * The running state for MD5 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_md5_set_state(br_md5_context *ctx, const void *stb, uint64_t count); + +/** + * \brief Symbolic identifier for SHA-1. + */ +#define br_sha1_ID 2 + +/** + * \brief SHA-1 output size (in bytes). + */ +#define br_sha1_SIZE 20 + +/** + * \brief Constant vtable for SHA-1. + */ +extern const br_hash_class br_sha1_vtable; + +/** + * \brief SHA-1 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[64]; + uint64_t count; + uint32_t val[5]; +#endif +} br_sha1_context; + +/** + * \brief SHA-1 context initialisation. + * + * This function initialises or resets a context for a new SHA-1 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha1_init(br_sha1_context *ctx); + +/** + * \brief Inject some data bytes in a running SHA-1 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha1_update(br_sha1_context *ctx, const void *data, size_t len); + +/** + * \brief Compute SHA-1 output. + * + * The SHA-1 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha1_out(const br_sha1_context *ctx, void *out); + +/** + * \brief Save SHA-1 running state. + * + * The running state for SHA-1 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha1_state(const br_sha1_context *ctx, void *out); + +/** + * \brief Restore SHA-1 running state. + * + * The running state for SHA-1 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha1_set_state(br_sha1_context *ctx, const void *stb, uint64_t count); + +/** + * \brief Symbolic identifier for SHA-224. + */ +#define br_sha224_ID 3 + +/** + * \brief SHA-224 output size (in bytes). + */ +#define br_sha224_SIZE 28 + +/** + * \brief Constant vtable for SHA-224. + */ +extern const br_hash_class br_sha224_vtable; + +/** + * \brief SHA-224 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[64]; + uint64_t count; + uint32_t val[8]; +#endif +} br_sha224_context; + +/** + * \brief SHA-224 context initialisation. + * + * This function initialises or resets a context for a new SHA-224 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha224_init(br_sha224_context *ctx); + +/** + * \brief Inject some data bytes in a running SHA-224 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha224_update(br_sha224_context *ctx, const void *data, size_t len); + +/** + * \brief Compute SHA-224 output. + * + * The SHA-224 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha224_out(const br_sha224_context *ctx, void *out); + +/** + * \brief Save SHA-224 running state. + * + * The running state for SHA-224 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha224_state(const br_sha224_context *ctx, void *out); + +/** + * \brief Restore SHA-224 running state. + * + * The running state for SHA-224 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha224_set_state(br_sha224_context *ctx, + const void *stb, uint64_t count); + +/** + * \brief Symbolic identifier for SHA-256. + */ +#define br_sha256_ID 4 + +/** + * \brief SHA-256 output size (in bytes). + */ +#define br_sha256_SIZE 32 + +/** + * \brief Constant vtable for SHA-256. + */ +extern const br_hash_class br_sha256_vtable; + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief SHA-256 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +} br_sha256_context; +#else +typedef br_sha224_context br_sha256_context; +#endif + +/** + * \brief SHA-256 context initialisation. + * + * This function initialises or resets a context for a new SHA-256 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha256_init(br_sha256_context *ctx); + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Inject some data bytes in a running SHA-256 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha256_update(br_sha256_context *ctx, const void *data, size_t len); +#else +#define br_sha256_update br_sha224_update +#endif + +/** + * \brief Compute SHA-256 output. + * + * The SHA-256 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha256_out(const br_sha256_context *ctx, void *out); + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Save SHA-256 running state. + * + * The running state for SHA-256 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha256_state(const br_sha256_context *ctx, void *out); +#else +#define br_sha256_state br_sha224_state +#endif + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Restore SHA-256 running state. + * + * The running state for SHA-256 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha256_set_state(br_sha256_context *ctx, + const void *stb, uint64_t count); +#else +#define br_sha256_set_state br_sha224_set_state +#endif + +/** + * \brief Symbolic identifier for SHA-384. + */ +#define br_sha384_ID 5 + +/** + * \brief SHA-384 output size (in bytes). + */ +#define br_sha384_SIZE 48 + +/** + * \brief Constant vtable for SHA-384. + */ +extern const br_hash_class br_sha384_vtable; + +/** + * \brief SHA-384 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[128]; + uint64_t count; + uint64_t val[8]; +#endif +} br_sha384_context; + +/** + * \brief SHA-384 context initialisation. + * + * This function initialises or resets a context for a new SHA-384 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha384_init(br_sha384_context *ctx); + +/** + * \brief Inject some data bytes in a running SHA-384 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha384_update(br_sha384_context *ctx, const void *data, size_t len); + +/** + * \brief Compute SHA-384 output. + * + * The SHA-384 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha384_out(const br_sha384_context *ctx, void *out); + +/** + * \brief Save SHA-384 running state. + * + * The running state for SHA-384 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha384_state(const br_sha384_context *ctx, void *out); + +/** + * \brief Restore SHA-384 running state. + * + * The running state for SHA-384 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha384_set_state(br_sha384_context *ctx, + const void *stb, uint64_t count); + +/** + * \brief Symbolic identifier for SHA-512. + */ +#define br_sha512_ID 6 + +/** + * \brief SHA-512 output size (in bytes). + */ +#define br_sha512_SIZE 64 + +/** + * \brief Constant vtable for SHA-512. + */ +extern const br_hash_class br_sha512_vtable; + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief SHA-512 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +} br_sha512_context; +#else +typedef br_sha384_context br_sha512_context; +#endif + +/** + * \brief SHA-512 context initialisation. + * + * This function initialises or resets a context for a new SHA-512 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha512_init(br_sha512_context *ctx); + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Inject some data bytes in a running SHA-512 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha512_update(br_sha512_context *ctx, const void *data, size_t len); +#else +#define br_sha512_update br_sha384_update +#endif + +/** + * \brief Compute SHA-512 output. + * + * The SHA-512 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha512_out(const br_sha512_context *ctx, void *out); + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Save SHA-512 running state. + * + * The running state for SHA-512 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha512_state(const br_sha512_context *ctx, void *out); +#else +#define br_sha512_state br_sha384_state +#endif + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Restore SHA-512 running state. + * + * The running state for SHA-512 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha512_set_state(br_sha512_context *ctx, + const void *stb, uint64_t count); +#else +#define br_sha512_set_state br_sha384_set_state +#endif + +/* + * "md5sha1" is a special hash function that computes both MD5 and SHA-1 + * on the same input, and produces a 36-byte output (MD5 and SHA-1 + * concatenation, in that order). State size is also 36 bytes. + */ + +/** + * \brief Symbolic identifier for MD5+SHA-1. + * + * MD5+SHA-1 is the concatenation of MD5 and SHA-1, computed over the + * same input. It is not one of the functions identified in TLS, so + * we give it a symbolic identifier of value 0. + */ +#define br_md5sha1_ID 0 + +/** + * \brief MD5+SHA-1 output size (in bytes). + */ +#define br_md5sha1_SIZE 36 + +/** + * \brief Constant vtable for MD5+SHA-1. + */ +extern const br_hash_class br_md5sha1_vtable; + +/** + * \brief MD5+SHA-1 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[64]; + uint64_t count; + uint32_t val_md5[4]; + uint32_t val_sha1[5]; +#endif +} br_md5sha1_context; + +/** + * \brief MD5+SHA-1 context initialisation. + * + * This function initialises or resets a context for a new SHA-512 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_md5sha1_init(br_md5sha1_context *ctx); + +/** + * \brief Inject some data bytes in a running MD5+SHA-1 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_md5sha1_update(br_md5sha1_context *ctx, const void *data, size_t len); + +/** + * \brief Compute MD5+SHA-1 output. + * + * The MD5+SHA-1 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_md5sha1_out(const br_md5sha1_context *ctx, void *out); + +/** + * \brief Save MD5+SHA-1 running state. + * + * The running state for MD5+SHA-1 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_md5sha1_state(const br_md5sha1_context *ctx, void *out); + +/** + * \brief Restore MD5+SHA-1 running state. + * + * The running state for MD5+SHA-1 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_md5sha1_set_state(br_md5sha1_context *ctx, + const void *stb, uint64_t count); + +/** + * \brief Aggregate context for configurable hash function support. + * + * The `br_hash_compat_context` type is a type which is large enough to + * serve as context for all standard hash functions defined above. + */ +typedef union { + const br_hash_class *vtable; + br_md5_context md5; + br_sha1_context sha1; + br_sha224_context sha224; + br_sha256_context sha256; + br_sha384_context sha384; + br_sha512_context sha512; + br_md5sha1_context md5sha1; +} br_hash_compat_context; + +/* + * The multi-hasher is a construct that handles hashing of the same input + * data with several hash functions, with a single shared input buffer. + * It can handle MD5, SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 + * simultaneously, though which functions are activated depends on + * the set implementation pointers. + */ + +/** + * \brief Multi-hasher context structure. + * + * The multi-hasher runs up to six hash functions in the standard TLS list + * (MD5, SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512) in parallel, over + * the same input. + * + * The multi-hasher does _not_ follow the OOP structure with a vtable. + * Instead, it is configured with the vtables of the hash functions it + * should run. Structure fields are not supposed to be accessed directly. + */ +typedef struct { +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[128]; + uint64_t count; + uint32_t val_32[25]; + uint64_t val_64[16]; + const br_hash_class *impl[6]; +#endif +} br_multihash_context; + +/** + * \brief Clear a multi-hasher context. + * + * This should always be called once on a given context, _before_ setting + * the implementation pointers. + * + * \param ctx the multi-hasher context. + */ +void br_multihash_zero(br_multihash_context *ctx); + +/** + * \brief Set a hash function implementation. + * + * Implementations shall be set _after_ clearing the context (with + * `br_multihash_zero()`) but _before_ initialising the computation + * (with `br_multihash_init()`). The hash function implementation + * MUST be one of the standard hash functions (MD5, SHA-1, SHA-224, + * SHA-256, SHA-384 or SHA-512); it may also be `NULL` to remove + * an implementation from the multi-hasher. + * + * \param ctx the multi-hasher context. + * \param id the hash function symbolic identifier. + * \param impl the hash function vtable, or `NULL`. + */ +static inline void +br_multihash_setimpl(br_multihash_context *ctx, + int id, const br_hash_class *impl) +{ + /* + * This code relies on hash functions ID being values 1 to 6, + * in the MD5 to SHA-512 order. + */ + ctx->impl[id - 1] = impl; +} + +/** + * \brief Get a hash function implementation. + * + * This function returns the currently configured vtable for a given + * hash function (by symbolic ID). If no such function was configured in + * the provided multi-hasher context, then this function returns `NULL`. + * + * \param ctx the multi-hasher context. + * \param id the hash function symbolic identifier. + * \return the hash function vtable, or `NULL`. + */ +static inline const br_hash_class * +br_multihash_getimpl(const br_multihash_context *ctx, int id) +{ + return ctx->impl[id - 1]; +} + +/** + * \brief Reset a multi-hasher context. + * + * This function prepares the context for a new hashing computation, + * for all implementations configured at that point. + * + * \param ctx the multi-hasher context. + */ +void br_multihash_init(br_multihash_context *ctx); + +/** + * \brief Inject some data bytes in a running multi-hashing computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_multihash_update(br_multihash_context *ctx, + const void *data, size_t len); + +/** + * \brief Compute a hash output from a multi-hasher. + * + * The hash output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `dst`. The hash + * function to use is identified by `id` and must be one of the standard + * hash functions. If that hash function was indeed configured in the + * multi-hasher context, the corresponding hash value is written in + * `dst` and its length (in bytes) is returned. If the hash function + * was _not_ configured, then nothing is written in `dst` and 0 is + * returned. + * + * The context itself is not modified, so extra bytes may be injected + * afterwards to continue the hash computations. + * + * \param ctx pointer to the context structure. + * \param id the hash function symbolic identifier. + * \param dst destination buffer for the hash output. + * \return the hash output length (in bytes), or 0. + */ +size_t br_multihash_out(const br_multihash_context *ctx, int id, void *dst); + +/** + * \brief Type for a GHASH implementation. + * + * GHASH is a sort of keyed hash meant to be used to implement GCM in + * combination with a block cipher (with 16-byte blocks). + * + * The `y` array has length 16 bytes and is used for input and output; in + * a complete GHASH run, it starts with an all-zero value. `h` is a 16-byte + * value that serves as key (it is derived from the encryption key in GCM, + * using the block cipher). The data length (`len`) is expressed in bytes. + * The `y` array is updated. + * + * If the data length is not a multiple of 16, then the data is implicitly + * padded with zeros up to the next multiple of 16. Thus, when using GHASH + * in GCM, this method may be called twice, for the associated data and + * for the ciphertext, respectively; the zero-padding implements exactly + * the GCM rules. + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +typedef void (*br_ghash)(void *y, const void *h, const void *data, size_t len); + +/** + * \brief GHASH implementation using multiplications (mixed 32-bit). + * + * This implementation uses multiplications of 32-bit values, with a + * 64-bit result. It is constant-time (if multiplications are + * constant-time). + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_ctmul(void *y, const void *h, const void *data, size_t len); + +/** + * \brief GHASH implementation using multiplications (strict 32-bit). + * + * This implementation uses multiplications of 32-bit values, with a + * 32-bit result. It is usually somewhat slower than `br_ghash_ctmul()`, + * but it is expected to be faster on architectures for which the + * 32-bit multiplication opcode does not yield the upper 32 bits of the + * product. It is constant-time (if multiplications are constant-time). + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_ctmul32(void *y, const void *h, const void *data, size_t len); + +/** + * \brief GHASH implementation using multiplications (64-bit). + * + * This implementation uses multiplications of 64-bit values, with a + * 64-bit result. It is constant-time (if multiplications are + * constant-time). It is substantially faster than `br_ghash_ctmul()` + * and `br_ghash_ctmul32()` on most 64-bit architectures. + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_ctmul64(void *y, const void *h, const void *data, size_t len); + +/** + * \brief GHASH implementation using the `pclmulqdq` opcode (part of the + * AES-NI instructions). + * + * This implementation is available only on x86 platforms where the + * compiler supports the relevant intrinsic functions. Even if the + * compiler supports these functions, the local CPU might not support + * the `pclmulqdq` opcode, meaning that a call will fail with an + * illegal instruction exception. To safely obtain a pointer to this + * function when supported (or 0 otherwise), use `br_ghash_pclmul_get()`. + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_pclmul(void *y, const void *h, const void *data, size_t len); + +/** + * \brief Obtain the `pclmul` GHASH implementation, if available. + * + * If the `pclmul` implementation was compiled in the library (depending + * on the compiler abilities) _and_ the local CPU appears to support the + * opcode, then this function will return a pointer to the + * `br_ghash_pclmul()` function. Otherwise, it will return `0`. + * + * \return the `pclmul` GHASH implementation, or `0`. + */ +br_ghash br_ghash_pclmul_get(void); + +/** + * \brief GHASH implementation using the POWER8 opcodes. + * + * This implementation is available only on POWER8 platforms (and later). + * To safely obtain a pointer to this function when supported (or 0 + * otherwise), use `br_ghash_pwr8_get()`. + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_pwr8(void *y, const void *h, const void *data, size_t len); + +/** + * \brief Obtain the `pwr8` GHASH implementation, if available. + * + * If the `pwr8` implementation was compiled in the library (depending + * on the compiler abilities) _and_ the local CPU appears to support the + * opcode, then this function will return a pointer to the + * `br_ghash_pwr8()` function. Otherwise, it will return `0`. + * + * \return the `pwr8` GHASH implementation, or `0`. + */ +br_ghash br_ghash_pwr8_get(void); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/CommSerial.cpp b/CommSerial.cpp new file mode 100644 index 0000000..4506746 --- /dev/null +++ b/CommSerial.cpp @@ -0,0 +1,154 @@ +#include "HermitCrab.h" +#include "Config.h" +#include "History.h" +#include "zcd.h" + +const char *strDeviceType[] = { + "None", + "Test", + "ESP8266", + "ON_OFF", + "ZCD", + "CAM", + "Beta", + "Beta_BLE", + "End" +}; + +MY_IRAM_ATTR void checkSerial(unsigned long tick) +{ + static char buffer[256]; + static short idx = 0; + static unsigned long val; + + while (Serial.available() > 0) { + if (idx > 254) { + idx = 0; + ESP_LOGI(TAG,"SrialHost: Buffer OverFlow"); + } + + buffer[idx] = Serial.read(); + if (buffer[idx] == '\n') { + if (idx >= 1) { + buffer[idx] = 0; + + switch(buffer[0]) + { + case 'T': // Temp Target + if (idx > 1) { + val = atoi(&buffer[1]); + if (val >= 25 && val <= 35) { + config.nTempTarget = val * 10; + } else if (val >= 250 && val <= 350) { + config.nTempTarget = val; + } + } + Serial.printf("%s SerialSet: Temp Target %d.%d°C Duty(%.2f%%)\n", + printStatus(tick, false), config.nTempTarget / 10, config.nTempTarget % 10, + status.nHeater1Duty / 100.0f); + break; + case 't': // Temp Target Night + if (idx > 1) { + val = atoi(&buffer[1]); + if (val >= 25 && val <= 35) { + config.nTempTargetNight = val * 10; + } else if (val >= 250 && val <= 350) { + config.nTempTargetNight = val; + } + } + Serial.printf("%s SerialSet: Temp Target Night %d.%d°C Duty(%.2f%%)\n", + printStatus(tick, false), + config.nTempTargetNight / 10, config.nTempTargetNight % 10, + status.nHeater1Duty/ 100.0f); + break; + case 'H': // Humidity Target + case 'h': + if (idx > 1) { + val = atoi(&buffer[1]); + if (val >= 20 && val <= 95) { + config.nHumidTarget = val * 10; + } else if (val >= 200 && val <= 950) { + config.nHumidTarget = val; + } + } + Serial.printf("%s SerialSet: Humid Target(%d.%d%%) Mist %s (Duty: %d)\n", + printStatus(tick), + config.nHumidTarget / 10, config.nHumidTarget % 10, + status.nMistDuty > 0 ? "ON" : "OFF", + status.nMistDuty); + break; + case 'M': // MistOn time + case 'm': // MistDelay time + if (idx > 1) { + val = atoi(&buffer[1]); + if (val >= 0 && val <= 1023) { + status.nMistDuty = val; + } + } + Serial.printf("%s SerialSet: Mist %s (Duty: %d)\n", + printStatus(tick), status.nMistDuty > 0 ? "ON" : "OFF", status.nMistDuty); + break; + case 'l': //Light1 + case 'L': + if (idx > 2) { + val = atoi(&buffer[1]); + if (val >= PWM_OFF && val <= PWM_FULL) { + status.nLightTargetDuty = val; + } + } + Serial.printf("%s SerialSet: Light1 %s (%d --> %d)\n", + printStatus(tick), status.nLightDuty > 0 ? "ON" : "OFF", + status.nLightDuty, status.nLightTargetDuty); + break; + case 'd': // Display Sensor + if (idx < 2) { + bShowSensor = !bShowSensor; + } else { + val = atoi(&buffer[1]); + bShowSensor = val == 0 ? false : true; + } + Serial.printf("%s SerialSet: DisplaySensor %s\n", printStatus(tick), bShowSensor ? "On" : "Off"); + break; + case 'p': // Print + case 'P': + break; + case 's': // Save Config + case 'S': + history.savePID(); + config.save(); + Serial.printf("%s Config Saved\n", printStatus(tick)); + break; + case 'Y': // Device Type + case 'y': + if (idx > 1) { + val = atoi(&buffer[1]); + char *sz = NULL; + if (val > TYPE_NONE && val < TYPE_DEVICE_END) { + config.m_nDeviceType = val; + Serial.printf("%s SeriaSet: DeviceType %s\n", printStatus(tick), strDeviceType[val]); + } else { + Serial.printf("%s SerialSet: Invalid DeviceType\n"); + } + } + break; + case 'Z': + case 'z': + if (idx > 1) { + val = atoi(&buffer[1]); + if (val >= 0 && val <= 10000) { + //ESP_LOGI(TAG,"%s SerialSet: Set Heater Duty %.1f%%\n", printStatus(tick), dutyPercent); + status.nHeater1Duty = val; + setHeater1Duty(status.nHeater1Duty); + } + } + break; + } + } + + // Clear Buffer + idx = 0; + } else { + idx++; + } + } +} \ No newline at end of file diff --git a/Config.cpp b/Config.cpp new file mode 100644 index 0000000..4c0a467 --- /dev/null +++ b/Config.cpp @@ -0,0 +1,158 @@ +#include "HermitCrab.h" +#include "AHT2x.h" +#include "Config.h" +#include "TimeManager.h" +#include + +#ifndef SIGNATURE1 +#define SIGNATURE1 ((uint16_t) 0xC8AB) +#endif +#ifndef SIGNATURE2 +#define SIGNATURE2 ((uint16_t) 0x4E81) +#endif + +Preferences preferences; +CONFIG_TYPE config; + +// Function to initialize default values for the config structure +void CONFIG_TYPE::init() { + m_nSignature1 = SIGNATURE1; + m_nSignature2 = SIGNATURE2; + + // Block 1 - Control and Environment + bSmartControl = false; + bNightControl = false; + bControlTemperature = true; + bControlHumidity = true; + bEnableIO = true; + bAC2_OnOff = false; + + nNightStartHour = 18; + nNightStartMin = 0; + nNightEndHour = 6; + nNightEndMin = 0; + + // Block 2 - Sensor and Temperature/Humidity Settings + nTempTarget = 260; + nTempTargetNight = 260; + nTemp1Offset = 0; + nTemp2Offset = 0; + nTemp3Offset = 0; + nHumidTarget = 880; + nHumidTargetNight = 880; + nHumid1Offset = 0; + nHumid2Offset = 0; + nTemp1SensorType = TEMP_SENSOR_TYPE::BLE_TUYA; + nTemp2SensorType = TEMP_SENSOR_TYPE::AHT20; + + Kp_Temp1 = 3.0f; // Load Kp for Temperature control + Kd_Temp1 = 750.0f; // Load Kd for Temperature control + LR_Temp1 = 0.05f; // Load learning rate for Temperature + + Kp_Humidity = 2.0f; // Load Kp for Humidity control + Kd_Humidity = 150.0f; // Load Kd for Humidity control + LR_Humidity = 0.08f; // Load learning rate for Humidity + + // Block 3 - AC1 and AC2 + // Day/Night Min/Max/Start On/Off THigh/Low Time B/E Period Hum H/L + config.ac1 = {CONTROL_TEMP_HEAT_PID, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,24*60-1, 60, 5, 920,880 }; + config.ac2 = {CONTROL_TEMP_HEAT, 0, 0,0, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 }; + + // Block 4 = Mist and Fan + // Day/Night Min/Max/Start On/Off THigh/Low Time B/E Period Hum H/L + config.mist = {CONTROL_HUMIDITY_INC_PID, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 }; + config.fan = {CONTROL_TEMP_COOL, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 }; + + // Block 5 - Motor and Light + // Day/Night Min/Max/Start On/Off THigh/Low Time B/E Period Hum H/L + config.motor = {CONTROL_PERIOD, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 }; + config.light = {CONTROL_DAY_NIGHT, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 }; + + + // Block 6 - Environment and Operations + bSendStatusSerial = false; + bConfigSaved = false; + bStatusSaved = false; + + // + // Reserved + // + // TBD + for (int i = 0; i < 17; i = i + 8) { + m_nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; + } + m_nPublicPort = (uint16_t)(m_nChipId & 0xFFFF); + m_nDeviceType = THIS_DEVICE_TYPE; + + // + // WiFi Client Only + // + m_nDisplayTime = 1800; + m_nDisplayTempHigh = 40; + m_nDisplayTempLow = 20; + m_nTempHigh = 20; + m_fShowRealTime = 0x000F; + + // Names + strcpy(m_sDeviceName, "Beta X"); + strcpy(m_sMake, "VisionSoft"); + strcpy(m_sModel, "HermitCrab"); + strncpy(m_sVersion, HC__VERSION, 11); + + + // WiFi - SSID and Password + strcpy(ssid, "RECALL"); + strcpy(pw, "BBBB9999"); + ESP_LOGI(TAG,"Config Initialized"); +} + +// Function to load the configuration block from preferences +bool CONFIG_TYPE::load() { + preferences.begin("HermitCrab", false); // Open preferences in read-write mode + + // Check if config has been saved previously + size_t len = preferences.getBytesLength("config_data"); + if (len != sizeof(config)) { + ESP_LOGI(TAG,"\nPreferences - First Time"); + // First time, initialize default config + ESP_LOGI(TAG, "Config Size Error: %d out of %d\n", len, sizeof(config)); + init(); + save(); // Save defaults + preferences.end(); + return true; // Indicates that it was initialized + } + + // Load the structure as a block of data + preferences.getBytes("config_data", &config, sizeof(config)); + if (m_nSignature1 != SIGNATURE1 || + m_nSignature2 != SIGNATURE2) { + ESP_LOGI(TAG, "Config Load: Signature Mismatch %X %X - %X %X\n", + m_nSignature1, m_nSignature2, + SIGNATURE1, SIGNATURE2); + } + preferences.end(); + ESP_LOGI(TAG,"Config Loaded"); + bConfigSaved = false; + bStatusSaved = false; + for (int i = 0; i < 17; i = i + 8) { + m_nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; + } + strncpy(m_sVersion, HC__VERSION, 11); + m_sVersion[11] = 0; + m_nDeviceType = THIS_DEVICE_TYPE; + return true; // Config loaded successfully +} + +// Function to save the entire configuration block to preferences +void CONFIG_TYPE::save() { + // Update before save + config.statusSave = status; + config.bStatusSaved = true; + + // Save the entire config structure as one block + preferences.begin("HermitCrab", false); // Open preferences in read-write mode + preferences.putBytes("config_data", &config, sizeof(config)); + preferences.end(); // Close preferences + bConfigSaved = true; + ESP_LOGI(TAG,"Config Saved"); +} diff --git a/Config.h b/Config.h new file mode 100644 index 0000000..54e7d7d --- /dev/null +++ b/Config.h @@ -0,0 +1,197 @@ +# ifndef __CONFIG_H +# define __CONFIG_H + +#include +#include "HermitCrab.h" + + +extern const char *strDeviceType[]; + + +enum ENUM_DEVICE_TYPE { + TYPE_NONE = 0, + TYPE_TEST, + TYPE_ESP8266, + TYPE_ON_OFF, + TYPE_ZCD, + TYPE_CAM, + TYPE_BETA, + TYPE_BETA_BLE, + TYPE_DEVICE_END, + TYPE_CLIENT = 1000, + TYPE_CLIENT_WIN, + TYPE_CLIENT_ANDROID +}; + +enum HEATER_TYPE { + TYPE_BULB, + TYPE_IR, + TYPE_CERAMIC, + TYPE_ZCD3, + TYPE_ZCD4, + TYPE_ZCD5, + TYPE_ZCD6, + TYPE_ZCD_CONTROL, + TYPE_FLUORESCENT = 16, + TYPE_LED, + TYPE_UAV, + TYPE_HEATER_END +}; + +enum DEVICE_CONTROL_TYPE { + CONTROL_NONE, + CONTROL_TEMP_HEAT, + CONTROL_TEMP_COOL, + CONTROL_HUMIDITY_INC, + CONTROL_HUMIDITY_DEC, + CONTROL_DAY_NIGHT, + CONTROL_TIME, + CONTROL_PERIOD, + CONTROL_TEMP_HEAT_PID, + CONTROL_TEMP_COOL_PID, + CONTROL_HUMIDITY_INC_PID, + CONTROL_HUMIDITY_DEC_PID +}; + +// Enumeration for the sensor +enum TEMP_SENSOR_TYPE { + SENSOR_NONE = 0, + AHT20, // AHT20 on 0x38 - default + AHT2x, // AHT25 on 0x38 - default + AHT10_0x39, // AHT10 on 0x39 - altanative + NTC, + BLE_TUYA, + BLE_XIAOMI_MIJIA, + BLE_INKBIRD + }; + +#pragma pack(push) /* push current alignment to stack */ +#pragma pack(1) /* set alignment to 1 byte boundary */ + +typedef struct CONFIG_STRUCT { + uint16_t m_nSignature1; + + // Block 1 - Control and EnvironMent + // Offset 2 + bool bSmartControl; + bool bNightControl; + bool bControlTemperature; + bool bControlHumidity; + bool bEnableIO; + bool bCheckAC; + bool bAC2_OnOff; + bool bdummy; + uint8_t nNightStartHour, nNightStartMin, nNightEndHour, nNightEndMin; + float Kp_Temp2; // Load Kp for Temperature control + float Kd_Temp2; // Load Kd for Temperature control + float LR_Temp2; // Load learning rate for Temperature + float Kp_Temp3; // Load Kp for Temperature control + float Kd_Temp3; // Load Kd for Temperature control + float LR_Temp3; // Load learning rate for Temperature + union { + uint64_t nBLESensorAddr2; + uint8_t nBLESensorAddrBytes2[8]; + }; + char bExtra[64 - 8 * sizeof(bool) - 4 * sizeof(uint8_t) - 6 * sizeof(float) - sizeof(uint64_t)]; + + // Block 2 - Sensor and TargetTemperature and Himidity + // Offset 64 + 2 + int16_t nTempTarget, nTempTargetNight; // Target Temperature + int16_t nTemp1Offset, nTemp2Offset, nTemp3Offset; + uint16_t nHumidTarget, nHumidTargetNight; + int16_t nHumid1Offset, nHumid2Offset; + uint8_t nTemp1SensorType; // TempSensor Type enum + uint8_t nTemp2SensorType; // TempSensor Type enum + float Kp_Temp1; // Load Kp for Temperature control + float Kd_Temp1; // Load Kd for Temperature control + float LR_Temp1; // Load learning rate for Temperature + float Kp_Humidity; // Load Kp for humidity control + float Kd_Humidity; // Load Kd for humidity control + float LR_Humidity; // Load learning rate for humidity + int16_t nTempSafety; + union { + uint64_t nBLESensorAddr; + uint8_t nBLESensorAddrBytes[8]; + }; + uint8_t bNTCNegativePolarity; + uint8_t nBLEScanInterval; + uint8_t bBLETest; + char nTempExtra[64 - 10 * sizeof(int16_t) - 6 * sizeof(float) - 5 * sizeof(uint8_t) - sizeof(uint64_t)]; + + // Block 3 - AC1 and AC2 + // Offset 128 + 2 + DEVICE_PARAM_TYPE ac1; + DEVICE_PARAM_TYPE ac2; + + // Block 4 - Mist and Fan + // Offset 192 + 2 + DEVICE_PARAM_TYPE mist; + DEVICE_PARAM_TYPE fan; + + // Block 5 - Motor and Light + // Offset 256 + 2 + DEVICE_PARAM_TYPE motor; + DEVICE_PARAM_TYPE light; + + // Block 6 - Environment and Operations + // Offset 320 + 2 + bool bSendStatusSerial; + bool bConfigSaved; + bool bStatusSaved; + char nEnvExtra[32 - 3 * sizeof(bool)]; + // Offset 352 + 2 + STATUS_TYPE statusSave; + + // Block 7 - ID and Client Display + // Offset 384 + 2 + uint32_t m_nChipId; + uint16_t m_nDeviceType; + uint16_t m_nPublicPort; + uint16_t m_nExtraTBD[4]; + // WiFi Client Only + uint16_t m_nDisplayTime; + int16_t m_nDisplayTempHigh; + int16_t m_nDisplayTempLow; + int16_t m_nTempHigh; + uint16_t m_fShowRealTime; + uint16_t m_fShowHistory; + uint32_t m_nEpochTime; + uint32_t m_nTimeOffset; + uint8_t m_bFahrenheit; + char nLongExtra[64 - 3 * sizeof(uint32_t) - 12 * sizeof(uint16_t)- 1 * sizeof(uint8_t)]; + + // Block 8 and 9 - WiFi AP ssid and pw + // Offset 448 + 2 & 512 + 2 + char ssid[64], pw[64]; + + // Block 10 + // Offset 576 + 2 + char m_sDeviceName[32]; + char m_sMake[32]; + + // Block 11 + // Offset 640 + 2 + char m_sModel[32]; + char m_sVersion[12]; + char nModelExtra[64 - 44]; + + // Offset 704 + 2 + uint16_t m_nSignature2; + +#ifdef ESP32 + // ConfigStruct Size: 708 +public: + void init(); + bool load(); + void save(); + //bool saveToServer(); +#endif +} CONFIG_TYPE; +#pragma pack(pop) // Restore previous alignment setting + +extern class Preferences preferences; +extern CONFIG_TYPE config; +extern char BLE_SSID[32]; +extern char BLE_PW[32]; + +#endif \ No newline at end of file diff --git a/ConnectWiFi.cpp b/ConnectWiFi.cpp new file mode 100644 index 0000000..dbf5453 --- /dev/null +++ b/ConnectWiFi.cpp @@ -0,0 +1,99 @@ +#include "ConnectWiFi.h" +#include "Config.h" + +extern bool g_bWiFiSetupExecuted; +extern bool g_bWiFiHasBeenConnected; +void setupPostWiFi(bool bBoot = false); + +void WiFiEvent(WiFiEvent_t event) { + switch (event) { + case IP_EVENT_STA_GOT_IP: + DPRINTF("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str()); + g_bWiFiHasBeenConnected = true; + ledcWrite(PIN_LED_WIFI, PWM_FULL * 9 / 10); // LED_OFF + if (!g_bWiFiSetupExecuted) setupPostWiFi(false); + break; + case WIFI_EVENT_STA_DISCONNECTED: + DPRINTLN("WiFi disconnected."); + ledcWrite(PIN_LED_WIFI, PWM_OFF); // LED_ON + break; + default: + break; + } +} + +// Function to compare current Wi-Fi credentials with the ones in config +void checkAndUpdateWiFiCredentials() { + // Check if the SSID or PW are different from the current Wi-Fi credentials + if (strncmp(BLE_SSID, config.ssid, sizeof(BLE_SSID)) || + strncmp(BLE_PW, config.pw, sizeof(BLE_PW))) { + DPRINTF("BLE Credentials SSID(%s) PW(%S)\n", BLE_SSID, BLE_PW); + DPRINTF("Cfg Credentials SSID(%s) PW(%S)\n", config.ssid, config.pw); + DPRINTLN("Wi-Fi credentials changed! Saving new credentials..."); + strncpy(config.ssid, BLE_SSID, sizeof(BLE_SSID)); + strncpy(config.pw, BLE_PW, sizeof(BLE_PW)); + // Save the new credentials + config.save(); + + // Restart ESP32 to apply the new credentials + ESP.restart(); + } +} + +IRAM_ATTR void checkWiFi(unsigned long tickMillis) { + static unsigned long lastAttempt = 0; + static bool bConnecting = false; + + wl_status_t status = WiFi.status(); + + // Connected + if (status == WL_CONNECTED) { + bConnecting = false; + g_bWiFiHasBeenConnected = true; + return; // Already connected, no need to proceed further + } + + checkAndUpdateWiFiCredentials(); + + // Connecting + if (bConnecting) { + if (tickMillis - lastAttempt < 60000) { + // give 30 seconds for connection try + return; + } + + // Connection Failure + DPRINTF("Connection timed out! Status: %d\n", status); + //WiFi.disconnect(false, true); + bConnecting = false; + g_bWiFiHasBeenConnected = false; + } + + // Not Connected 1 - Reconnect + if (status == WL_DISCONNECTED && g_bWiFiHasBeenConnected) { + DPRINTLN("Attempting WiFi reconnection..."); + WiFi.reconnect(); + ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink + lastAttempt = tickMillis; + bConnecting = true; + } + + + // Not Conneccted 2 - Connect + if (tickMillis - lastAttempt > 300000) { // Retry every 5 minutes + DPRINTF("Loop: Connecting to WiFi: SSID: '%s', PW: '%s'\n", config.ssid, config.pw); + WiFi.disconnect(); // Stop Wi-Fi connection attempt + // delay(50); + // WiFi.mode(WIFI_OFF); + // delay(100); + // WiFi.mode(WIFI_STA); + // delay(50); + wl_status_t ret = WiFi.begin(config.ssid, config.pw); + DPRINTF(" WiFi.Begin(%s,%s) returned %d\n", config.ssid, config.pw, ret); + lastAttempt = tickMillis; + + ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink + lastAttempt = tickMillis; + bConnecting = true; + } +} \ No newline at end of file diff --git a/ConnectWiFi.h b/ConnectWiFi.h new file mode 100644 index 0000000..69c3ba4 --- /dev/null +++ b/ConnectWiFi.h @@ -0,0 +1,13 @@ +#ifndef __CONNECT_WIFI_H +#define __CONNECT_WIFI_H +#ifdef ESP8266 +#include +#else +#include +#endif + +void checkWiFi(unsigned long tick); +void WiFiEvent(WiFiEvent_t event); +inline bool isWiFiConnected() { return WiFi.status() == WL_CONNECTED; }; +#endif + diff --git a/HCUpdate.h b/HCUpdate.h new file mode 100644 index 0000000..16c8c8c --- /dev/null +++ b/HCUpdate.h @@ -0,0 +1,291 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#if defined(ESP32) +#ifndef ESP32UPDATER_H +#define ESP32UPDATER_H + +#include +#include +#include +#include +#include "esp_partition.h" + +#define UPDATE_ERROR_OK (0) +#define UPDATE_ERROR_WRITE (1) +#define UPDATE_ERROR_ERASE (2) +#define UPDATE_ERROR_READ (3) +#define UPDATE_ERROR_SPACE (4) +#define UPDATE_ERROR_SIZE (5) +#define UPDATE_ERROR_STREAM (6) +#define UPDATE_ERROR_MD5 (7) +#define UPDATE_ERROR_MAGIC_BYTE (8) +#define UPDATE_ERROR_ACTIVATE (9) +#define UPDATE_ERROR_NO_PARTITION (10) +#define UPDATE_ERROR_BAD_ARGUMENT (11) +#define UPDATE_ERROR_ABORT (12) +#define UPDATE_ERROR_DECRYPT (13) + +#define UPDATE_SIZE_UNKNOWN 0xFFFFFFFF + +#define U_FLASH 0 +#define U_SPIFFS 100 +#define U_AUTH 200 + +#define ENCRYPTED_BLOCK_SIZE 16 +#define ENCRYPTED_TWEAK_BLOCK_SIZE 32 +#define ENCRYPTED_KEY_SIZE 32 + +#define U_AES_DECRYPT_NONE 0 +#define U_AES_DECRYPT_AUTO 1 +#define U_AES_DECRYPT_ON 2 +#define U_AES_DECRYPT_MODE_MASK 3 +#define U_AES_IMAGE_DECRYPTING_BIT 4 + +#define SPI_SECTORS_PER_BLOCK 16 // usually large erase block is 32k/64k +#define SPI_FLASH_BLOCK_SIZE (SPI_SECTORS_PER_BLOCK * SPI_FLASH_SEC_SIZE) + +enum HTTPUpdateResult { + HTTP_UPDATE_FAILED, + HTTP_UPDATE_NO_UPDATES, + HTTP_UPDATE_OK +}; + +class UpdateClass { +public: + typedef std::function THandlerFunction_Progress; + + UpdateClass(); + UpdateClass(int httpClientTimeout); + + int update(WiFiClientSecure& client, String &url, uint16_t port, String& uri, + String ¤tVersion, short nDeviceType, bool bForceUpdate); + int handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType); + int http_downloadUpdate(HTTPClient &httpClient); + + + /* + This callback will be called when Update is receiving data + */ + UpdateClass &onProgress(THandlerFunction_Progress fn); + + /* + Call this to check the space needed for the update + Will return false if there is not enough space + */ + bool begin(size_t size = UPDATE_SIZE_UNKNOWN, int command = U_FLASH, int ledPin = -1, uint8_t ledOn = LOW, const char *label = NULL); + + /* + Setup decryption configuration + Crypt Key is 32bytes(256bits) block of data, use the same key as used to encrypt image file + Crypt Address, use the same value as used to encrypt image file + Crypt Config, use the same value as used to encrypt image file + Crypt Mode, used to select if image files should be decrypted or not + */ + bool setupCrypt(const uint8_t *cryptKey = 0, size_t cryptAddress = 0, uint8_t cryptConfig = 0xf, int cryptMode = U_AES_DECRYPT_AUTO); + + /* + Writes a buffer to the flash and increments the address + Returns the amount written + */ + size_t write(uint8_t *data, size_t len); + + /* + Writes the remaining bytes from the Stream to the flash + Uses readBytes() and sets UPDATE_ERROR_STREAM on timeout + Returns the bytes written + Should be equal to the remaining bytes when called + Usable for slow streams like Serial + */ + size_t writeStream(Stream &data); + + /* + If all bytes are written + this call will write the config to eboot + and return true + If there is already an update running but is not finished and !evenIfRemaining + or there is an error + this will clear everything and return false + the last error is available through getError() + evenIfRemaining is helpful when you update without knowing the final size first + */ + bool end(bool evenIfRemaining = false); + + /* + sets AES256 key(32 bytes) used for decrypting image file + */ + bool setCryptKey(const uint8_t *cryptKey); + + /* + sets crypt mode used on image files + */ + bool setCryptMode(const int cryptMode); + + /* + sets address used for decrypting image file + */ + void setCryptAddress(const size_t cryptAddress) { + _cryptAddress = cryptAddress & 0x00fffff0; + } + + /* + sets crypt config used for decrypting image file + */ + void setCryptConfig(const uint8_t cryptConfig) { + _cryptCfg = cryptConfig & 0x0f; + } + + /* + Aborts the running update + */ + void abort(); + + /* + Prints the last error to an output stream + */ + void printError(Print &out); + + const char *errorString(); + + /* + sets the expected MD5 for the firmware (hexString) + */ + bool setMD5(const char *expected_md5); + + /* + returns the MD5 String of the successfully ended firmware + */ + String md5String(void) { + return _md5.toString(); + } + + /* + populated the result with the md5 bytes of the successfully ended firmware + */ + void md5(uint8_t *result) { + return _md5.getBytes(result); + } + + //Helpers + uint8_t getError() { + return _error; + } + void clearError() { + _error = UPDATE_ERROR_OK; + } + bool hasError() { + return _error != UPDATE_ERROR_OK; + } + bool isRunning() { + return _size > 0; + } + bool isFinished() { + return _progress == _size; + } + size_t size() { + return _size; + } + size_t progress() { + return _progress; + } + size_t remaining() { + return _size - _progress; + } + + /* + Template to write from objects that expose + available() and read(uint8_t*, size_t) methods + faster than the writeStream method + writes only what is available + */ + template size_t write(T &data) { + size_t written = 0; + if (hasError() || !isRunning()) { + return 0; + } + + size_t available = data.available(); + while (available) { + if (_bufferLen + available > remaining()) { + available = remaining() - _bufferLen; + } + if (_bufferLen + available > 4096) { + size_t toBuff = 4096 - _bufferLen; + data.read(_buffer + _bufferLen, toBuff); + _bufferLen += toBuff; + if (!_writeBuffer()) { + return written; + } + written += toBuff; + } else { + data.read(_buffer + _bufferLen, available); + _bufferLen += available; + written += available; + if (_bufferLen == remaining()) { + if (!_writeBuffer()) { + return written; + } + } + } + if (remaining() == 0) { + return written; + } + available = data.available(); + } + return written; + } + + /* + check if there is a firmware on the other OTA partition that you can bootinto + */ + bool canRollBack(); + /* + set the other OTA partition as bootable (reboot to enable) + */ + bool rollBack(); + +private: + void _reset(); + void _abort(uint8_t err); + void _cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key); + bool _decryptBuffer(); + bool _writeBuffer(); + bool _verifyHeader(uint8_t data); + bool _verifyEnd(); + bool _enablePartition(const esp_partition_t *partition); + bool _chkDataInBlock(const uint8_t *data, size_t len) const; // check if block contains any data or is empty + + int _httpClientTimeout; + followRedirects_t _followRedirects = HTTPC_DISABLE_FOLLOW_REDIRECTS; + uint8_t _error; + uint8_t *_cryptKey; + uint8_t *_cryptBuffer; + uint8_t *_buffer; + uint8_t *_skipBuffer; + size_t _bufferLen; + size_t _size; + THandlerFunction_Progress _progress_callback; + uint32_t _progress; + uint32_t _paroffset; + uint32_t _command; + const esp_partition_t *_partition; + + String _target_md5; + MD5Builder _md5; + + int _ledPin; + uint8_t _ledOn; + + uint8_t _cryptMode; + size_t _cryptAddress; + uint8_t _cryptCfg; +}; + +#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE) +extern UpdateClass Update; +#endif + +#endif // defined(ESP32UPDATER_H) +#endif // defined(ESP32) diff --git a/HCUpdater.cpp b/HCUpdater.cpp new file mode 100644 index 0000000..3ebbb21 --- /dev/null +++ b/HCUpdater.cpp @@ -0,0 +1,787 @@ +#if defined(ESP32) +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "HermitCrab.h" + +#include "Arduino.h" +#include "spi_flash_mmap.h" +#include "esp_ota_ops.h" +#include "esp_image_format.h" +#include "mbedtls/aes.h" +#include +#include +#include +#include "HCUpdate.h" + +#define TAG_FW "FW Upate" + +static const char *_err2str(uint8_t _error) { + if (_error == UPDATE_ERROR_OK) { + return ("No Error"); + } else if (_error == UPDATE_ERROR_WRITE) { + return ("Flash Write Failed"); + } else if (_error == UPDATE_ERROR_ERASE) { + return ("Flash Erase Failed"); + } else if (_error == UPDATE_ERROR_READ) { + return ("Flash Read Failed"); + } else if (_error == UPDATE_ERROR_SPACE) { + return ("Not Enough Space"); + } else if (_error == UPDATE_ERROR_SIZE) { + return ("Bad Size Given"); + } else if (_error == UPDATE_ERROR_STREAM) { + return ("Stream Read Timeout"); + } else if (_error == UPDATE_ERROR_MD5) { + return ("MD5 Check Failed"); + } else if (_error == UPDATE_ERROR_MAGIC_BYTE) { + return ("Wrong Magic Byte"); + } else if (_error == UPDATE_ERROR_ACTIVATE) { + return ("Could Not Activate The Firmware"); + } else if (_error == UPDATE_ERROR_NO_PARTITION) { + return ("Partition Could Not be Found"); + } else if (_error == UPDATE_ERROR_BAD_ARGUMENT) { + return ("Bad Argument"); + } else if (_error == UPDATE_ERROR_ABORT) { + return ("Aborted"); + } else if (_error == UPDATE_ERROR_DECRYPT) { + return ("Decryption error"); + } + return ("UNKNOWN"); +} + +int UpdateClass::update(WiFiClientSecure& client, String& host, uint16_t port, String&uri, String& currentVersion, short nDeviceType, bool bForceUpdate) +{ + HTTPClient http; + if (!http.begin(client, host, port, uri)) { + ESP_LOGI(TAG_FW,"OTA - httpClient begin failed\n"); + return HTTP_UPDATE_FAILED; + } + int size = handleUpdate(http, currentVersion, nDeviceType); + + ESP_LOGI(TAG_FW,"OTA - size(%d) Server Version: %s Mode: %s\n", size, http.header("version").c_str(), + http.header("update") && http.header("update").toInt() == 1 ? "Download" : "Check Only" ); + + //is there an image to download + if (size >= 0) { + if (!http.header("update") || http.header("update").toInt() == 0) { + ESP_LOGI(TAG_FW,"OTA - No Firmware available"); + } else if (!http.header("version") || strcmp(http.header("version").c_str(), HC__VERSION) <= 0) { + ESP_LOGI(TAG_FW,"OTA - Firmware is upto Date"); + } else { + //image avaliabe to download & update + if (!bForceUpdate) { + ESP_LOGI(TAG_FW,"OTA - Found V%s Firmware\n", http.header("version").c_str()); + } else { + ESP_LOGI(TAG_FW,"OTA - Downloading & Installing V%s Firmware\n", http.header("version").c_str()); + } + + if (bForceUpdate) { + if (http_downloadUpdate(http) == 0) { + http.end(); //end connection + ESP_LOGI(TAG_FW,"OTA - Firmware Update Successful, rebooting"); + ESP.restart(); + } + } + } + } + + http.end(); //end connection + return 0; +} + +int UpdateClass::handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType) +{ + HTTPUpdateResult ret = HTTP_UPDATE_FAILED; + + // use HTTP/1.0 for update since the update handler not support any transfer Encoding + http.useHTTP10(true); + http.setTimeout(_httpClientTimeout); + http.setFollowRedirects(_followRedirects); + http.setUserAgent("ESP32-http-Update"); + http.addHeader("Cache-Control", "no-cache"); + http.addHeader("X-ESP32-DEVICE-TYPE", String(nDeviceType)); + http.addHeader("X-ESP32-VERSION", HC__VERSION); + http.addHeader("X-ESP32-STA-MAC", WiFi.macAddress()); + + unsigned long nChipId = 0; + for (int i = 0; i < 17; i = i + 8) { + nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; + } + http.addHeader(F("X-ESP32-Chip-ID"), String(nChipId)); + http.addHeader(F("x-ESP32-free-space"), String(ESP.getFreeSketchSpace())); + http.addHeader(F("x-ESP32-sketch-size"), String(ESP.getSketchSize())); + http.addHeader(F("x-ESP32-sketch-md5"), String(ESP.getSketchMD5())); + //http.addHeader(F("x-ESP32-chip-size"), String(ESP.getFlashChipRealSize())); + http.addHeader(F("x-ESP32-sdk-version"), ESP.getSdkVersion()); + + //set headers to look for to get returned values in servers http response to our http request + const char *headerkeys[] = {"update", "version"}; //server returns update 0=no update found, 1=update found, version=version of update found + size_t headerkeyssize = sizeof(headerkeys) / sizeof(char *); + http.collectHeaders(headerkeys, headerkeyssize); + + int code = http.GET(); + int len = http.getSize(); + + if (code == HTTP_CODE_OK) { + return (len > 0 ? len : 0); //return 0 or length of image to download + } else if (code < 0) { + ESP_LOGI(TAG_FW,"Error: %s\n", http.errorToString(code).c_str()); + ESP_LOGI(TAG_FW, "%s", http.getString()); + return code; //error code should be minus between -1 to -11 + } else { + ESP_LOGI(TAG_FW,"Error: HTTP Server response code %i\n", code); + ESP_LOGI(TAG_FW, "%s", http.getString()); + return -code; //return code should be minus between -100 to -511 + } + + return ret; +} + +int UpdateClass::http_downloadUpdate(HTTPClient &httpClient) { + WiFiClient *stream = httpClient.getStreamPtr(); + int written = 0; + + // Check content length and begin update + int fileSize = httpClient.getSize(); + if (fileSize <= 0) { + ESP_LOGI(TAG_FW,"Invalid content length"); + return 1; + } + if (!Update.begin(fileSize, U_FLASH)) { + ESP_LOGI(TAG_FW,"Update begin failed!"); + return 1; + } + + // Download and write the update + while (httpClient.connected() && written < fileSize) { + size_t availableBytes = stream->available(); + if (availableBytes) { + uint8_t buffer[128]; // Buffer to hold incoming data + int bytesRead = stream->readBytes(buffer, min(availableBytes, sizeof(buffer))); + int bytesWritten = Update.write(buffer, bytesRead); + if (bytesWritten != bytesRead) { + ESP_LOGI(TAG_FW,"Update write failed!"); + Update.end(); + return 1; + } + written += bytesWritten; + } + } + + // Finalize update + if (Update.end()) { + if (Update.isFinished()) { + ESP_LOGI(TAG_FW,"Update successful!"); + return 0; + } else { + ESP_LOGI(TAG_FW,"Update not finished!"); + return 1; + } + } else { + ESP_LOGI(TAG_FW,"Update end failed!"); + return 1; + } +} + + +static bool _partitionIsBootable(const esp_partition_t *partition) { + uint8_t buf[ENCRYPTED_BLOCK_SIZE]; + if (!partition) { + return false; + } + if (!ESP.partitionRead(partition, 0, (uint32_t *)buf, ENCRYPTED_BLOCK_SIZE)) { + return false; + } + + if (buf[0] != ESP_IMAGE_HEADER_MAGIC) { + return false; + } + return true; +} + +bool UpdateClass::_enablePartition(const esp_partition_t *partition) { + if (!partition) { + return false; + } + return ESP.partitionWrite(partition, 0, (uint32_t *)_skipBuffer, ENCRYPTED_BLOCK_SIZE); +} + +UpdateClass::UpdateClass() + : _error(0) + , _cryptKey(0) + , _cryptBuffer(0) + , _buffer(0) + , _skipBuffer(0) + , _bufferLen(0) + , _size(0) + , _progress_callback(NULL) + , _progress(0) + , _paroffset(0) + , _command(U_FLASH) + , _partition(NULL) + , _cryptMode(U_AES_DECRYPT_AUTO) + , _cryptAddress(0) + , _cryptCfg(0xf) + , _httpClientTimeout(8000) + {} + +UpdateClass::UpdateClass(int httpClientTimeout) + : _error(0) + , _cryptKey(0) + , _cryptBuffer(0) + , _buffer(0) + , _skipBuffer(0) + , _bufferLen(0) + , _size(0) + , _progress_callback(NULL) + , _progress(0) + , _paroffset(0) + , _command(U_FLASH) + , _partition(NULL) + , _cryptMode(U_AES_DECRYPT_AUTO) + , _cryptAddress(0) + , _cryptCfg(0xf) + , _httpClientTimeout(httpClientTimeout) + {} + +UpdateClass &UpdateClass::onProgress(THandlerFunction_Progress fn) { + _progress_callback = fn; + return *this; +} + +void UpdateClass::_reset() { + if (_buffer) { + delete[] _buffer; + } + if (_skipBuffer) { + delete[] _skipBuffer; + } + + _cryptBuffer = nullptr; + _buffer = nullptr; + _skipBuffer = nullptr; + _bufferLen = 0; + _progress = 0; + _size = 0; + _command = U_FLASH; + + if (_ledPin != -1) { + digitalWrite(_ledPin, !_ledOn); // off + } +} + +bool UpdateClass::canRollBack() { + if (_buffer) { //Update is running + return false; + } + const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL); + return _partitionIsBootable(partition); +} + +bool UpdateClass::rollBack() { + if (_buffer) { //Update is running + return false; + } + const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL); + return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition); +} + +bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, const char *label) { + if (_size > 0) { + log_w("already running"); + return false; + } + + _ledPin = ledPin; + _ledOn = !!ledOn; // 0(LOW) or 1(HIGH) + + _reset(); + _error = 0; + _target_md5 = emptyString; + _md5 = MD5Builder(); + + if (size == 0) { + _error = UPDATE_ERROR_SIZE; + return false; + } + + if (command == U_FLASH) { + _partition = esp_ota_get_next_update_partition(NULL); + if (!_partition) { + _error = UPDATE_ERROR_NO_PARTITION; + return false; + } + log_d("OTA Partition: %s", _partition->label); + } else if (command == U_SPIFFS) { + _partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, label); + _paroffset = 0; + if (!_partition) { + _partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, NULL); + _paroffset = 0x1000; //Offset for ffat, assuming size is already corrected + if (!_partition) { + _error = UPDATE_ERROR_NO_PARTITION; + return false; + } + } + } else { + _error = UPDATE_ERROR_BAD_ARGUMENT; + log_e("bad command %u", command); + return false; + } + + if (size == UPDATE_SIZE_UNKNOWN) { + size = _partition->size; + } else if (size > _partition->size) { + _error = UPDATE_ERROR_SIZE; + log_e("too large %u > %u", size, _partition->size); + return false; + } + + //initialize + _buffer = new (std::nothrow) uint8_t[SPI_FLASH_SEC_SIZE]; + if (!_buffer) { + log_e("_buffer allocation failed"); + return false; + } + _size = size; + _command = command; + _md5.begin(); + return true; +} + +bool UpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8_t cryptConfig, int cryptMode) { + if (setCryptKey(cryptKey)) { + if (setCryptMode(cryptMode)) { + setCryptAddress(cryptAddress); + setCryptConfig(cryptConfig); + return true; + } + } + return false; +} + +bool UpdateClass::setCryptKey(const uint8_t *cryptKey) { + if (!cryptKey) { + if (_cryptKey) { + delete[] _cryptKey; + _cryptKey = 0; + log_d("AES key unset"); + } + return false; //key cleared, no key to decrypt with + } + //initialize + if (!_cryptKey) { + _cryptKey = new (std::nothrow) uint8_t[ENCRYPTED_KEY_SIZE]; + } + if (!_cryptKey) { + log_e("new failed"); + return false; + } + memcpy(_cryptKey, cryptKey, ENCRYPTED_KEY_SIZE); + return true; +} + +bool UpdateClass::setCryptMode(const int cryptMode) { + if (cryptMode >= U_AES_DECRYPT_NONE && cryptMode <= U_AES_DECRYPT_ON) { + _cryptMode = cryptMode; + } else { + log_e("bad crypt mode argument %i", cryptMode); + return false; + } + return true; +} + +void UpdateClass::_abort(uint8_t err) { + _reset(); + _error = err; +} + +void UpdateClass::abort() { + _abort(UPDATE_ERROR_ABORT); +} + +void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) { + memcpy(tweaked_key, _cryptKey, ENCRYPTED_KEY_SIZE); + if (_cryptCfg == 0) { + return; //no tweaking needed, use crypt key as-is + } + + const uint8_t pattern[] = {23, 23, 23, 14, 23, 23, 23, 12, 23, 23, 23, 10, 23, 23, 23, 8}; + int pattern_idx = 0; + int key_idx = 0; + int bit_len = 0; + uint32_t tweak = 0; + cryptAddress &= 0x00ffffe0; //bit 23-5 + cryptAddress <<= 8; //bit23 shifted to bit31(MSB) + while (pattern_idx < sizeof(pattern)) { + tweak = cryptAddress << (23 - pattern[pattern_idx]); //bit shift for small patterns + // alternative to: tweak = rotl32(tweak,8 - bit_len); + tweak = (tweak << (8 - bit_len)) | (tweak >> (24 + bit_len)); //rotate to line up with end of previous tweak bits + bit_len += pattern[pattern_idx++] - 4; //add number of bits in next pattern(23-4 = 19bits = 23bit to 5bit) + while (bit_len > 7) { + tweaked_key[key_idx++] ^= tweak; //XOR byte + // alternative to: tweak = rotl32(tweak, 8); + tweak = (tweak << 8) | (tweak >> 24); //compiler should optimize to use rotate(fast) + bit_len -= 8; + } + tweaked_key[key_idx] ^= tweak; //XOR remaining bits, will XOR zeros if no remaining bits + } + if (_cryptCfg == 0xf) { + return; //return with fully tweaked key + } + + //some of tweaked key bits need to be restore back to crypt key bits + const uint8_t cfg_bits[] = {67, 65, 63, 61}; + key_idx = 0; + pattern_idx = 0; + while (key_idx < ENCRYPTED_KEY_SIZE) { + bit_len += cfg_bits[pattern_idx]; + if ((_cryptCfg & (1 << pattern_idx)) == 0) { //restore crypt key bits + while (bit_len > 0) { + if (bit_len > 7 || ((_cryptCfg & (2 << pattern_idx)) == 0)) { //restore a crypt key byte + tweaked_key[key_idx] = _cryptKey[key_idx]; + } else { //MSBits restore crypt key bits, LSBits keep as tweaked bits + tweaked_key[key_idx] &= (0xff >> bit_len); + tweaked_key[key_idx] |= (_cryptKey[key_idx] & (~(0xff >> bit_len))); + } + key_idx++; + bit_len -= 8; + } + } else { //keep tweaked key bits + while (bit_len > 0) { + if (bit_len < 8 && ((_cryptCfg & (2 << pattern_idx)) == 0)) { //MSBits keep as tweaked bits, LSBits restore crypt key bits + tweaked_key[key_idx] &= (~(0xff >> bit_len)); + tweaked_key[key_idx] |= (_cryptKey[key_idx] & (0xff >> bit_len)); + } + key_idx++; + bit_len -= 8; + } + } + pattern_idx++; + } +} + +bool UpdateClass::_decryptBuffer() { + if (!_cryptKey) { + log_w("AES key not set"); + return false; + } + if (_bufferLen % ENCRYPTED_BLOCK_SIZE != 0) { + log_e("buffer size error"); + return false; + } + if (!_cryptBuffer) { + _cryptBuffer = new (std::nothrow) uint8_t[ENCRYPTED_BLOCK_SIZE]; + } + if (!_cryptBuffer) { + log_e("new failed"); + return false; + } + uint8_t tweaked_key[ENCRYPTED_KEY_SIZE]; //tweaked crypt key + int done = 0; + + /* + Mbedtls functions will be replaced with esp_aes functions when hardware acceleration is available + + To Do: + Replace mbedtls for the cases where there's no hardware acceleration + */ + + mbedtls_aes_context ctx; //initialize AES + mbedtls_aes_init(&ctx); + while ((_bufferLen - done) >= ENCRYPTED_BLOCK_SIZE) { + for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) { + _cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i] = _buffer[i + done]; //reverse order 16 bytes to decrypt + } + if (((_cryptAddress + _progress + done) % ENCRYPTED_TWEAK_BLOCK_SIZE) == 0 || done == 0) { + _cryptKeyTweak(_cryptAddress + _progress + done, tweaked_key); //update tweaked crypt key + if (mbedtls_aes_setkey_enc(&ctx, tweaked_key, 256)) { + return false; + } + if (mbedtls_aes_setkey_dec(&ctx, tweaked_key, 256)) { + return false; + } + } + if (mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, _cryptBuffer, _cryptBuffer)) { //use MBEDTLS_AES_ENCRYPT to decrypt flash code + return false; + } + for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) { + _buffer[i + done] = _cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i]; //reverse order 16 bytes from decrypt + } + done += ENCRYPTED_BLOCK_SIZE; + } + return true; +} + +bool UpdateClass::_writeBuffer() { + //first bytes of loading image, check to see if loading image needs decrypting + if (!_progress) { + _cryptMode &= U_AES_DECRYPT_MODE_MASK; + if ((_cryptMode == U_AES_DECRYPT_ON) || ((_command == U_FLASH) && (_cryptMode & U_AES_DECRYPT_AUTO) && (_buffer[0] != ESP_IMAGE_HEADER_MAGIC))) { + _cryptMode |= U_AES_IMAGE_DECRYPTING_BIT; //set to decrypt the loading image + log_d("Decrypting OTA Image"); + } + } + //check if data in buffer needs decrypting + if (_cryptMode & U_AES_IMAGE_DECRYPTING_BIT) { + if (!_decryptBuffer()) { + _abort(UPDATE_ERROR_DECRYPT); + return false; + } + } + //first bytes of new firmware + uint8_t skip = 0; + if (!_progress && _command == U_FLASH) { + //check magic + if (_buffer[0] != ESP_IMAGE_HEADER_MAGIC) { + _abort(UPDATE_ERROR_MAGIC_BYTE); + return false; + } + + //Stash the first 16 bytes of data and set the offset so they are + //not written at this point so that partially written firmware + //will not be bootable + skip = ENCRYPTED_BLOCK_SIZE; + _skipBuffer = new (std::nothrow) uint8_t[skip]; + if (!_skipBuffer) { + log_e("_skipBuffer allocation failed"); + return false; + } + memcpy(_skipBuffer, _buffer, skip); + } + if (!_progress && _progress_callback) { + _progress_callback(0, _size); + } + size_t offset = _partition->address + _progress; + bool block_erase = + (_size - _progress >= SPI_FLASH_BLOCK_SIZE) && (offset % SPI_FLASH_BLOCK_SIZE == 0); // if it's the block boundary, than erase the whole block from here + bool part_head_sectors = + _partition->address % SPI_FLASH_BLOCK_SIZE + && offset < (_partition->address / SPI_FLASH_BLOCK_SIZE + 1) * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition heading block + bool part_tail_sectors = + offset >= (_partition->address + _size) / SPI_FLASH_BLOCK_SIZE * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition tailing block + if (block_erase || part_head_sectors || part_tail_sectors) { + if (!ESP.partitionEraseRange(_partition, _progress, block_erase ? SPI_FLASH_BLOCK_SIZE : SPI_FLASH_SEC_SIZE)) { + _abort(UPDATE_ERROR_ERASE); + return false; + } + } + + // try to skip empty blocks on unencrypted partitions + if ((_partition->encrypted || _chkDataInBlock(_buffer + skip / sizeof(uint32_t), _bufferLen - skip)) + && !ESP.partitionWrite(_partition, _progress + skip, (uint32_t *)_buffer + skip / sizeof(uint32_t), _bufferLen - skip)) { + _abort(UPDATE_ERROR_WRITE); + return false; + } + + //restore magic or md5 will fail + if (!_progress && _command == U_FLASH) { + _buffer[0] = ESP_IMAGE_HEADER_MAGIC; + } + _md5.add(_buffer, _bufferLen); + _progress += _bufferLen; + _bufferLen = 0; + if (_progress_callback) { + _progress_callback(_progress, _size); + } + return true; +} + +bool UpdateClass::_verifyHeader(uint8_t data) { + if (_command == U_FLASH) { + if (data != ESP_IMAGE_HEADER_MAGIC) { + _abort(UPDATE_ERROR_MAGIC_BYTE); + return false; + } + return true; + } else if (_command == U_SPIFFS) { + return true; + } + return false; +} + +bool UpdateClass::_verifyEnd() { + if (_command == U_FLASH) { + if (!_enablePartition(_partition) || !_partitionIsBootable(_partition)) { + _abort(UPDATE_ERROR_READ); + return false; + } + + if (esp_ota_set_boot_partition(_partition)) { + _abort(UPDATE_ERROR_ACTIVATE); + return false; + } + _reset(); + return true; + } else if (_command == U_SPIFFS) { + _reset(); + return true; + } + return false; +} + +bool UpdateClass::setMD5(const char *expected_md5) { + if (strlen(expected_md5) != 32) { + return false; + } + _target_md5 = expected_md5; + _target_md5.toLowerCase(); + return true; +} + +bool UpdateClass::end(bool evenIfRemaining) { + if (hasError() || _size == 0) { + return false; + } + + if (!isFinished() && !evenIfRemaining) { + log_e("premature end: res:%u, pos:%u/%u\n", getError(), progress(), _size); + _abort(UPDATE_ERROR_ABORT); + return false; + } + + if (evenIfRemaining) { + if (_bufferLen > 0) { + _writeBuffer(); + } + _size = progress(); + } + + _md5.calculate(); + if (_target_md5.length()) { + if (_target_md5 != _md5.toString()) { + _abort(UPDATE_ERROR_MD5); + return false; + } + } + + return _verifyEnd(); +} + +size_t UpdateClass::write(uint8_t *data, size_t len) { + if (hasError() || !isRunning()) { + return 0; + } + + if (len > remaining()) { + _abort(UPDATE_ERROR_SPACE); + return 0; + } + + size_t left = len; + + while ((_bufferLen + left) > SPI_FLASH_SEC_SIZE) { + size_t toBuff = SPI_FLASH_SEC_SIZE - _bufferLen; + memcpy(_buffer + _bufferLen, data + (len - left), toBuff); + _bufferLen += toBuff; + if (!_writeBuffer()) { + return len - left; + } + left -= toBuff; + } + memcpy(_buffer + _bufferLen, data + (len - left), left); + _bufferLen += left; + if (_bufferLen == remaining()) { + if (!_writeBuffer()) { + return len - left; + } + } + return len; +} + +size_t UpdateClass::writeStream(Stream &data) { + size_t written = 0; + size_t toRead = 0; + int timeout_failures = 0; + + if (hasError() || !isRunning()) { + return 0; + } + + if (_command == U_FLASH && !_cryptMode) { + if (!_verifyHeader(data.peek())) { + _reset(); + return 0; + } + } + + if (_ledPin != -1) { + pinMode(_ledPin, OUTPUT); + } + + while (remaining()) { + if (_ledPin != -1) { + digitalWrite(_ledPin, _ledOn); // Switch LED on + } + size_t bytesToRead = SPI_FLASH_SEC_SIZE - _bufferLen; + if (bytesToRead > remaining()) { + bytesToRead = remaining(); + } + + /* + Init read&timeout counters and try to read, if read failed, increase counter, + wait 100ms and try to read again. If counter > 300 (30 sec), give up/abort + */ + toRead = 0; + timeout_failures = 0; + while (!toRead) { + toRead = data.readBytes(_buffer + _bufferLen, bytesToRead); + if (toRead == 0) { + timeout_failures++; + if (timeout_failures >= 300) { + _abort(UPDATE_ERROR_STREAM); + return written; + } + delay(100); + } + } + + if (_ledPin != -1) { + digitalWrite(_ledPin, !_ledOn); // Switch LED off + } + _bufferLen += toRead; + if ((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer()) { + return written; + } + written += toRead; + +#if CONFIG_FREERTOS_UNICORE + delay(1); // Fix solo WDT +#endif + } + return written; +} + +void UpdateClass::printError(Print &out) { + out.println(_err2str(_error)); +} + +const char *UpdateClass::errorString() { + return _err2str(_error); +} + +bool UpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const { + // check 32-bit aligned blocks only + if (!len || len % sizeof(uint32_t)) { + return true; + } + + size_t dwl = len / sizeof(uint32_t); + + do { + if (*(uint32_t *)data ^ 0xffffffff) { // for SPI NOR flash empty blocks are all one's, i.e. filled with 0xff byte + return true; + } + + data += sizeof(uint32_t); + } while (--dwl); + return false; +} + +#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE) +UpdateClass Update; +#endif + +#endif // defined(ESP32) \ No newline at end of file diff --git a/HCesp.ino b/HCesp.ino new file mode 100644 index 0000000..a9f9d6a --- /dev/null +++ b/HCesp.ino @@ -0,0 +1,683 @@ +#include "HermitCrab.h" +#include "Config.h" +#include "AHT2x.h" +#include "NTC_10K.h" +#include "ZCD.h" +#include "History.h" +#include "TimeManager.h" +#include "ConnectWiFi.h" +#include "WiFiHost.h" +#include "OTA.h" +#include "UI.h" +#include "BLEScan.h" + +#if defined(ESP32) +#include "esp_wifi.h" +#endif +#define TAG_MAIN "Main" +STATUS_TYPE status; + + +// Time +volatile unsigned short g_nYear, g_nMonth, g_nDay, g_nHour, g_nMinute, g_nSecond; + +// Environment +bool bShowSensor = false; +time_t now; +struct tm timeinfo; + +void readSensors(); +void controlAC1(short temp, unsigned long tick); +void controlAC2(short temp, unsigned long tick); +void controlMist(short humid, unsigned long tick); +void controlFan(short temp, unsigned long tick); +void controlMotor(short hour, short min, unsigned long tick); +void controlLight(short hour, short min, unsigned long tick); +void controlFanDuty(); +void controlMotorDuty(); +void controlLightDuty(); + + +// ================================================================================== +// +// Arduino Loop - controls +// +// ================================================================================== +MY_IRAM_ATTR void loop() { + static unsigned long lastTickSecond = 0; + static uint8_t lastSecond = -1; + unsigned long tickMillis = millis(); + unsigned long tickSecond = tickMillis / 1000; + + + // Un-Conditional Loop + { + //ESP_LOGI(TAG_MAIN,"Checking WiFi2"); + checkWiFi(tickMillis); + + //ESP_LOGI(TAG_MAIN,"Host Loop"); + host.Loop(tickMillis); + + // UI Button Check + ui.loopButton(tickMillis); + } + + // Every Second + if (tickSecond != lastTickSecond) + { + // Time and ZCD + setZCD(); + setTime(); + + // Temperature and Humidity + readSensors(); + //ble.loop(tickMillis); + + // Fan, Motor, Light Duties + controlFanDuty(); + controlMotorDuty(); + controlLightDuty(); + + // Add to History - every minutes + if (g_nSecond == 0 && lastSecond != g_nSecond) { + history.add(status); + } + lastSecond = g_nSecond; + + // Every 10 Second + switch(tickSecond % 10) { + case 1: // Every 5 second - xx:xx-x7 + if (bShowSensor) { + ESP_LOGI(TAG_MAIN, "%s\n", printStatus(tickSecond, true)); + } + break; + case 2: // AC1 + controlAC1(status.nTemp1, tickSecond); + break; + case 3: // AC2 + controlAC2(status.nTemp1, tickSecond); + break; + case 4: // Mist + controlMist(status.nHumid1, tickSecond); + break; + case 5: // Fan Control + controlFan(status.nTemp1, tickSecond); + break; + case 6: // Motor Control + controlMotor(g_nHour, g_nMinute, tickSecond); + break; + case 7: // Light Control + controlLight(g_nHour, g_nMinute, tickSecond); + break; + default: + break; + } + lastTickSecond = tickSecond; + } + yield(); +} +// ================================================================================== +// End of Main Loop +// ================================================================================== + +void readSensors() { + switch (config.nTemp1SensorType) { + case TEMP_SENSOR_TYPE::AHT20: + case TEMP_SENSOR_TYPE::AHT2x: + status.nTemp1 = (aht25.getTemperature() + 5) / 10 + config.nTemp1Offset; + status.nHumid1 = (aht25.getHumidity() + 5) / 10 + config.nHumid1Offset; + break; + case TEMP_SENSOR_TYPE::AHT10_0x39: + status.nTemp1 = (aht10_0x39.getTemperature() + 5) / 10 + config.nTemp1Offset; + status.nHumid1 = (aht10_0x39.getHumidity() + 5) / 10 + config.nHumid1Offset; + break; + case TEMP_SENSOR_TYPE::NTC: + status.nTemp1 = ntc.getTemp() + config.nTemp1Offset; + status.nHumid1 = 0; + break; + case TEMP_SENSOR_TYPE::BLE_TUYA: + case TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA: + status.nTemp1 = (ble.getTemp() + 5) / 10 + config.nTemp1Offset; + status.nHumid1 = (ble.getHumid() + 5) / 10 + config.nHumid1Offset; + if (ble.getBatteyLevel() > 30) status.nFlags &= ~FLAG_BLE_BATT; + else status.nFlags |= FLAG_BLE_BATT; + break; + default: + status.nTemp1 = 0; + status.nHumid1 = 0; + break; + } + + switch (config.nTemp2SensorType) { + case TEMP_SENSOR_TYPE::AHT20: + case TEMP_SENSOR_TYPE::AHT2x: + status.nTemp2 = (aht25.getTemperature() + 5) / 10 + config.nTemp2Offset; + status.nHumid2 = (aht25.getHumidity() + 5) / 10 + config.nHumid2Offset; + break; + case TEMP_SENSOR_TYPE::AHT10_0x39: + status.nTemp2 = (aht10_0x39.getTemperature() + 5) / 10 + config.nTemp2Offset; + status.nHumid2 = (aht10_0x39.getHumidity() + 5) / 10 + config.nHumid2Offset; + break; + case TEMP_SENSOR_TYPE::NTC: + status.nTemp2 = ntc.getTemp() + config.nTemp2Offset; + status.nHumid2 = 0; + break; + case TEMP_SENSOR_TYPE::BLE_TUYA: + case TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA: + status.nTemp2 = (ble.getTemp2() + 5) / 10 + config.nTemp2Offset; + status.nHumid2 = (ble.getHumid2() + 5) / 10 + config.nHumid2Offset; + if (ble.getBatteyLevel2() > 30) status.nFlags &= ~FLAG_BLE_BATT; + else status.nFlags |= FLAG_BLE_BATT; + break; + default: + status.nTemp2 = 0; + status.nHumid2 = 0; + break; + } + + status.nTemp3 = ntc.getTemp() + config.nTemp3Offset; +} + +// ================================================================================== +// Device Control Functions +// ================================================================================== +MY_IRAM_ATTR void controlAC1(short temp, unsigned long tick) { + uint16_t max = config.ac1.dutyMax * 10; + // Check Safety + if (temp >= config.nTempSafety) { + setHeater1Duty(0); + status.nHeater1Duty = 0; + return; + } + + // Manual Control + if (status.nFlags & FLAG_MANUAL_HEATER1) { + if (status.nHeater1Duty > max) status.nHeater1Duty = max; + setHeater1Duty(status.nHeater1Duty); + return; + } + + // Day & Night + bool bNight = isNight(g_nHour, g_nMinute); + if (!config.ac1.bDay && !bNight || !config.ac1.bNight && bNight) { + setHeater1Duty(0); + status.nHeater1Duty = 0; + return; + } + + // No Sensor + if ((status.nTemp1 - config.nTemp1Offset) == 0 && + (status.nHumid1 - config.nHumid1Offset) == 0) { + setHeater1Duty(0); + status.nHeater1Duty = 0; + return; + } + + uint16_t setPoint; + int16_t duty; + + //DPRINTF("AC1 Start: %d°C set: %d duty: %d\n", temp, setPoint, status.nHeater1Duty); + switch(config.ac1.nControlType) { + case CONTROL_TEMP_HEAT_PID: + { + setPoint = config.bNightControl && isNight(g_nHour, g_nMinute) ? + config.nTempTargetNight: config.nTempTarget; + duty = history.calculateDutyForTemp1(setPoint, temp, status.nHeater1Duty); + //DPRINTF("AC1: %d°C set: %d duty: %d --> %d\n", temp, setPoint, status.nHeater1Duty, duty); + status.nHeater1Duty = duty; + if (duty > 0) + duty = map(duty, 1, 10000, config.ac1.dutyMin * 10, config.ac1.dutyMax * 10); + setHeater1Duty(duty); + } + break; + case CONTROL_TEMP_COOL_PID: + case CONTROL_HUMIDITY_INC_PID: + case CONTROL_HUMIDITY_DEC_PID: + break; + default: + duty = controlDevice(&config.ac1, 0) * 10; + status.nHeater1Duty = duty; + if (duty > 0) + duty = map(duty, 1, 10000, config.ac1.dutyMin * 10, config.ac1.dutyMax * 10); + setHeater1Duty(duty); + break; + } + //DPRINTF("AC1 End: %d°C set: %d duty: %d\n", temp, setPoint, status.nHeater1Duty); + +} + +MY_IRAM_ATTR void controlAC2(short temp, unsigned long tick) { + uint16_t max = config.ac2.dutyMax * 10; + + // Check Safety + if (temp >= config.nTempSafety) { + setHeater2Duty(0); + status.nHeater2Duty = 0; + //DPRINTLN("Safety Temp"); + return; + } + + // Manual Control + if (status.nFlags & FLAG_MANUAL_HEATER2) { + if (status.nHeater2Duty > max) status.nHeater2Duty = max; + setHeater2Duty(status.nHeater2Duty); + return; + } + + // Day & Night + bool bNight = isNight(g_nHour, g_nMinute); + if (!config.ac2.bDay && !bNight || !config.ac2.bNight && bNight) { + setHeater2Duty(0); + status.nHeater2Duty = 0; + //DPRINTLN("Day/Night Reject"); + return; + } + + // No Sensor + if ((status.nTemp1 - config.nTemp1Offset) == 0 && + (status.nHumid1 - config.nHumid1Offset) == 0) { + setHeater2Duty(0); + status.nHeater2Duty = 0; + //DPRINTLN("No Sensor"); + return; + } + + uint16_t setPoint; + int16_t duty; + switch(config.ac2.nControlType) { + case CONTROL_TEMP_HEAT_PID: + { + setPoint = config.bNightControl && isNight(g_nHour, g_nMinute) ? + config.nTempTargetNight: config.nTempTarget; + duty = history.calculateDutyForTemp1(setPoint, temp, status.nHeater2Duty); + //DPRINTF("AC2: %d°C set: %d duty: %d --> %d\n", temp, setPoint, status.nHeater2Duty, duty); + status.nHeater2Duty = duty; + if (duty > 0) + duty = map(duty, 1, 10000, config.ac1.dutyMin * 10, config.ac1.dutyMax * 10); + setHeater2Duty(duty); + } + break; + case CONTROL_TEMP_COOL_PID: + case CONTROL_HUMIDITY_INC_PID: + case CONTROL_HUMIDITY_DEC_PID: + break; + default: + status.nHeater2Duty = controlDevice(&config.ac2, 1) * 10; + if (status.nHeater2Duty > 0) + duty = map(status.nHeater2Duty, 1, 10000, config.ac2.dutyMin * 10, config.ac2.dutyMax * 10); + setHeater2Duty(duty); + break; + } +} + +MY_IRAM_ATTR void controlMist(short humid, unsigned long tick) { + uint16_t duty = 0; + bool bNight = isNight(g_nHour, g_nMinute); + + // Manual Control + if (status.nFlags & FLAG_MANUAL_MIST) { + duty = status.nMistDuty; + } else + // Day & Night + if (!config.mist.bDay && !bNight || !config.mist.bNight && bNight) { + duty = 0; + } else { + if (status.nTemp1 != 0 && status.nHumid1 != 0) { + switch(config.mist.nControlType) { + case CONTROL_HUMIDITY_INC_PID: + { + uint16_t setPoint; + setPoint = config.bNightControl && isNight(g_nHour, g_nMinute) ? + config.nHumidTargetNight: config.nHumidTarget; + duty = history.calculateMistDuty(setPoint, humid, status.nMistDuty); + //DPRINTF("Mist: %d%% set: %d duty: %d --> %d\n", humid, setPoint, status.nMistDuty, duty); + } + break; + case CONTROL_TEMP_HEAT_PID: + case CONTROL_TEMP_COOL_PID: + case CONTROL_HUMIDITY_DEC_PID: + duty = status.nMistDuty = 0; + break; + default: + duty = controlDevice(&config.mist, 2) * 10; + break; + } + } else { + duty = 0; + } + } + status.nMistDuty = duty; + + // Control Duty + if (duty > PWM_OFF) + duty = map(duty, 1, 10000, config.mist.dutyMin, config.mist.dutyMax); + ledcWrite(PIN_MIST, duty); +} + +MY_IRAM_ATTR void controlFan(short temp, unsigned long tick) { + if (status.nFlags & FLAG_MANUAL_FAN ) return; + + status.nFanDuty = controlDevice(&config.fan, 3); +} + +MY_IRAM_ATTR void controlMotor(short hour, short min, unsigned long tick) { + if (status.nFlags & FLAG_MANUAL_MOTOR ) return; + + status.nMotorDuty = controlDevice(&config.motor, 4); +} + +MY_IRAM_ATTR void controlLight(short hour, short min, unsigned long tick) { + if (status.nFlags & FLAG_MANUAL_LIGHT ) return; + + status.nLightTargetDuty = controlDevice(&config.light, 5); +} + +MY_IRAM_ATTR uint16_t controlDevice(DEVICE_PARAM_TYPE *pDevice, int idx) { + uint16_t duty; + switch(idx) { + case 0: duty = status.nHeater1Duty / 10; break; // AC1 + case 1: duty = status.nHeater2Duty / 10; break; // AC2 + case 2: duty = status.nMistDuty / 10; break; // Mist + case 3: duty = status.nFanDuty; break; // Fan + case 4: duty = status.nMotorDuty; break; // Motor + case 5: duty = status.nLightDuty; break; // Light + default: duty = 0; break; + } + + bool bNight = isNight(g_nHour, g_nMinute); + if (!pDevice->bDay && !bNight || !pDevice->bNight && bNight) + return 0; + + switch (pDevice->nControlType) { + case CONTROL_TEMP_HEAT: + if (status.nTemp1 < pDevice->tempLow) { + duty = pDevice->dutyOn; + } + else if (status.nTemp1 > pDevice->tempHigh) { + duty = pDevice->dutyOff; + } + break; + case CONTROL_TEMP_COOL: + if (status.nTemp1 > pDevice->tempHigh) + duty = pDevice->dutyOn; + else if (status.nTemp1 < pDevice->tempLow) + duty = pDevice->dutyOff; + break; + case CONTROL_HUMIDITY_DEC: + if (status.nHumid1 > pDevice->humidHigh) + duty = pDevice->dutyOn; + else if (status.nHumid1 < pDevice->humidLow) + duty = pDevice->dutyOff; + break; + case CONTROL_HUMIDITY_INC: + if (status.nHumid1 < pDevice->humidLow) + duty = pDevice->dutyOn; + else if (status.nHumid1 > pDevice->humidHigh) + duty = pDevice->dutyOff; + break; + case CONTROL_DAY_NIGHT: + { + duty = isNight(g_nHour, g_nMinute) ? pDevice->dutyNight : pDevice->dutyDay; + } + break; + case CONTROL_TIME: + { + uint16_t time = g_nHour * 60 + g_nMinute; + if (pDevice->timeBegin < pDevice->timeEnd) { + if (time >= pDevice->timeBegin && time < pDevice->timeEnd) + duty = pDevice->dutyOn; + else + duty = pDevice->dutyOff; + } else { + if (time >= pDevice->timeEnd && time < pDevice->timeBegin) + duty = pDevice->dutyOff; + else + duty = pDevice->dutyOn; + } + } + break; + case CONTROL_PERIOD: + { + static unsigned long periodStartTime[6] = {0}; // Tracks start of the current period (in seconds) + static unsigned long totalElapsedSeconds[6] = {0}; // Tracks elapsed time within the current period + static unsigned long lastCurrentTime[6] = {0xFFFFFFFF}; + + unsigned long time = g_nHour * 3600UL + g_nMinute * 60UL + g_nSecond; + unsigned long period = pDevice->periodPeriod * 60; + if (lastCurrentTime[idx] == 0xFFFFFFFF) { + lastCurrentTime[idx] = time; + } + if (time < lastCurrentTime[idx]) { + // Handle day rollover (23:59:59 -> 00:00:00) + totalElapsedSeconds[idx] += (24UL * 3600) + time - lastCurrentTime[idx]; // Normal time increment + } else { + totalElapsedSeconds[idx] += time - lastCurrentTime[idx]; // Normal time increment + } + lastCurrentTime[idx] = time; + + // Reset totalElapsedSeconds when it exceeds the period + if (totalElapsedSeconds[idx] >= periodStartTime[idx] + period) { + totalElapsedSeconds[idx] -= period; // Wrap around to start a new period + periodStartTime[idx] = totalElapsedSeconds[idx]; // Reset the start time + duty = pDevice->dutyOn; // Start of the new period + } + + // Turn off LED if the onDuration has passed within the current period + if (totalElapsedSeconds[idx] >= periodStartTime[idx] + pDevice->periodOn) { + duty = pDevice->dutyOff; // Start of the new period + } else { + duty = pDevice->dutyOn; + } + } + break; + default: + duty = 0; + } + return duty; +} + +MY_IRAM_ATTR void controlFanDuty() { + static uint16_t nFanDuty = 0; + + if (nFanDuty != status.nFanDuty) { + if (nFanDuty == 0) { + // Start-up + nFanDuty = status.nFanDuty < config.fan.dutyStart ? config.fan.dutyStart : status.nFanDuty; + } else { + nFanDuty = status.nFanDuty; + } + + int duty = 0; + if (nFanDuty > 0) { + duty = map(nFanDuty, 1, 1000, config.fan.dutyMin, config.fan.dutyMax); + } + ledcWrite(PIN_FAN, duty); + } +} + +MY_IRAM_ATTR void controlMotorDuty() { + static uint16_t nMotorDuty = 0; + + if (nMotorDuty != status.nMotorDuty) { + // Start-up + if (nMotorDuty == 0) { + nMotorDuty = status.nMotorDuty < config.motor.dutyStart ? config.motor.dutyStart : status.nMotorDuty; + } else { + nMotorDuty = status.nMotorDuty; + } + + int duty = 0; + if (nMotorDuty > 0) { + duty = map(nMotorDuty, 1, 1000, config.motor.dutyMin, config.motor.dutyMax); + } + ledcWrite(PIN_MOTOR, duty); + } +} + +MY_IRAM_ATTR void controlLightDuty() { + if (status.nLightDuty != status.nLightTargetDuty) { + int16_t step = status.nLightDuty < status.nLightTargetDuty ? 1 : -1; + status.nLightDuty += step; + int16_t duty = map(status.nLightDuty, 0, 1000, config.light.dutyMin, config.light.dutyMax); + ledcWrite(PIN_LIGHT, duty); + } +} + + +MY_IRAM_ATTR bool isNight(unsigned char currentHour, unsigned char currentMin) { + // Check if the current time is within the night range + + if (config.nNightStartHour < config.nNightEndHour || + (config.nNightStartHour == config.nNightEndHour && config.nNightStartMin < config.nNightEndMin)) { + // Case 1: Night starts and ends on the same day (e.g., 22:00 to 06:00) + if ((currentHour > config.nNightStartHour || (currentHour == config.nNightStartHour && currentMin >= config.nNightStartMin)) && + (currentHour < config.nNightEndHour || (currentHour == config.nNightEndHour && currentMin <= config.nNightEndMin))) { + return true; // It's night time + } + } else { + // Case 2: Night crosses midnight (e.g., 22:00 to 06:00) + if ((currentHour > config.nNightStartHour || (currentHour == config.nNightStartHour && currentMin >= config.nNightStartMin)) || + (currentHour < config.nNightEndHour || (currentHour == config.nNightEndHour && currentMin <= config.nNightEndMin))) { + return true; // It's night time + } + } + + return false; // It's day time +} + +// ====================================================================== +// +// Utilities +// +// ====================================================================== +MY_IRAM_ATTR char *printStatus(unsigned long tick, bool bLong) { + static char szStatus[256] = { 0 }; + //if (config.bSendStatusSerial) + { + // Build Status String + char strHeat1[32], strHeat2[32], strMist[32], strLight1[32], strLight2[32]; + strHeat1[0] = 0; + strHeat2[0] = 0; + strMist[0] = 0; + strLight1[0] = 0; + strLight2[0] = 0; + + sprintf(strHeat1, "T(%2d.%d/%2d.%d°C H%2d.%d%%)", + status.nTemp1 / 10, status.nTemp1 % 10, + config.nTempTarget / 10, config.nTempTarget % 10, + status.nHeater1Duty / 100, status.nHeater1Duty % 100 ); + + sprintf(strMist, "H(%2d.%d/%2d.%d%% M%2d.%d%%)", + status.nHumid1 / 10, status.nHumid1 % 10, + config.nHumidTarget / 10, config.nHumidTarget % 10, + status.nMistDuty / 10, status.nMistDuty % 10); + + sprintf(strLight1, "L(%2d.%d%%)", status.nLightDuty / 10, status.nLightDuty % 10); + + sprintf(szStatus, "%s %s %s %s H(Kp %.2f, Kd %.2f) M(Kp %.2f, %.2f)", + printTime(bLong), strHeat1, strMist, strLight1, + history.getKpTemperature(), history.getKdTemperature(), + history.getKpHumidity(), history.getKdHumidity()); + + // Send out to clients + // ESP_LOGI(TAG_MAIN,"%s\n", szStatus); + } + return szStatus; +} + +MY_IRAM_ATTR char *printTime(bool bLong) { + static char szBuff[48]; + + // Get current time from the system clock + time_t now; + time(&now); // Get current system time in seconds + struct tm *timeinfo = localtime(&now); + + if (bLong) { + // Calculate uptime in seconds + /* + unsigned long uptime = now - timeManager.getFirstNTPTime(); + + // Calculate days, hours, minutes, seconds for uptime + int days = uptime / 86400; // Seconds in a day + uptime %= 86400; + int hours = uptime / 3600; // Seconds in an hour + uptime %= 3600; + int minutes = uptime / 60; // Seconds in a minute + int seconds = uptime % 60; + + // Format the current time + char szDays[8], szHours[8]; + if (days > 0) + sprintf(szDays, "%d ", days); + else + szDays[0] = 0; + + if (hours > 0) + sprintf(szHours, "%d:", hours); + else + szHours[0] = 0; + */ + + sprintf(szBuff, "%04d-%02d-%02d %02d:%02d:%02d", + timeinfo->tm_year + 1900, // Year + timeinfo->tm_mon + 1, // Month (0-11) + timeinfo->tm_mday, // Day of month + timeinfo->tm_hour, // Hour + timeinfo->tm_min, // Minute + timeinfo->tm_sec // Second + ); + } else { + sprintf(szBuff, "%02d:%02d:%02d", + timeinfo->tm_hour, // Hour + timeinfo->tm_min, // Minute + timeinfo->tm_sec); + } + return szBuff; +} + +inline void setTime() { + // Get time from System Clock - UTC + time(&now); // Get current system time in seconds (still in local time settings) + status.now = (uint32_t) now; // Store UTC time for external communication + + // UTC time_r(&now, &timeinfo); + //gmtime_r(&now, &timeinfo); + //ESP_LOGI(TAG_MAIN,"Time - UTC: %2d:%02d:%02d ", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, g_nSecond); + + // local time + now += config.m_nTimeOffset; + gmtime_r(&now, &timeinfo); + //ESP_LOGI(TAG_MAIN,"Local: %4d-%02d-%02d %2d:%02d:%02d (UTC%c%d)\n", + // g_nYear, g_nMonth, g_nDay, + // timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, + // config.m_nTimeOffset >= 0 ? '+' : '-', (uint16_t)(config.m_nTimeOffset / 3600)); + + g_nSecond = static_cast(timeinfo.tm_sec); + g_nMinute = static_cast(timeinfo.tm_min); + g_nHour = static_cast(timeinfo.tm_hour); + g_nDay = static_cast(timeinfo.tm_mday); + g_nMonth = static_cast(timeinfo.tm_mon) + 1; + g_nYear = timeinfo.tm_year + 1900; + +} + +inline void setZCD() { + // ZCD + status.zcdAC = zcdACCount; + zcdACCount = 0; + status.zcdLoad = zcdLoadCount; + zcdLoadCount = 0; + if (status.zcdAC < 118 || status.zcdAC > 122) { + status.nFlags |= FLAG_ZCD_AC; + } + else { + status.nFlags &= ~FLAG_ZCD_AC; + } + if (status.zcdLoad < 118 || status.zcdLoad > 122) { + status.nFlags |= FLAG_ZCD_LOAD; + } + else { + status.nFlags &= ~FLAG_ZCD_LOAD; + } +} \ No newline at end of file diff --git a/HermitCrab.h b/HermitCrab.h new file mode 100644 index 0000000..8bfb40f --- /dev/null +++ b/HermitCrab.h @@ -0,0 +1,243 @@ +#ifndef __HERMIT_CRAB_H +#define __HERMIT_CRAB_H +#include + +#ifndef ESP32 +#define ESP32 +#endif + +#define USE_BLE +#ifdef USE_BLE +#define THIS_DEVICE_TYPE ENUM_DEVICE_TYPE::TYPE_BETA_BLE +#define MY_IRAM_ATTR +#else +#define THIS_DEVICE_TYPE ENUM_DEVICE_TYPE::TYPE_BETA +#define MY_IRAM_ATTR IRAM_ATTR +#endif + + +// Define DEBUG flag +#ifndef DEBUG +#define DEBUG 1 // Set to 0 to disable debug output +#endif +#undef DEBUG + +//#define BLE_DEBUG +#ifdef BLE_DEBUG +#ifndef DEBUG +#define DEBUG 1 +#endif +#endif + +// Debug print macros +#if defined(DEBUG) + #define DPRINT(...) Serial.print(__VA_ARGS__) + #define DPRINTLN(...) Serial.println(__VA_ARGS__) + #define DPRINTF(...) Serial.printf(__VA_ARGS__) +#else + #define DPRINT(...) // Do nothing + #define DPRINTLN(...) // Do nothing + #define DPRINTF(...) // Do nothing +#endif + +#ifndef SIGNATURE1 +#define SIGNATURE1 ((uint16_t) 0xC8AB) +#endif +#ifndef SIGNATURE2 +#define SIGNATURE2 ((uint16_t) 0x4E81) +#endif + + +#define KALVIN (273.125f) +#define LIGHT_ON HIGH +#define LIGHT_OFF LOW +#define HUMID_ON HIGH +#define HUMID_OFF LOW +#define HEATER_ON HIGH +#define HEATER_OFF LOW +#define LED_ON LOW +#define LED_OFF HIGH +#define SEGMENT_ON LOW +#define SEGMENT_OFF HIGH + +// ======================= +// PIN Definitions +// +// LEFT +#define PIN_NTC 39 // Input Only +#define PIN_ZCD_LOAD 34 // Input Only +#define PIN_ZCD_AC 35 // Input Only +#define PIN_HEATER1 32 +#define PIN_HEATER2 33 +#define PIN_LIGHT 25 +#define PIN_MOTOR 26 +#define PIN_FAN 27 +#define PIN_MIST 14 // Outputs PWM signal at boot +#define PIN_NONE_1 12 // boot fails if pulled high, strapping pin + +// BOTTOM +#define PIN_NONE_2 13 +#define PIN_NONE_6 15 // Outputs PWM signal at Boot, strapping pin +#define PIN_NONE_5 2 + +// RIGHT +#define PIN_LED_WIFI 0 // pulled up outputs PWM signal at boot, must be LOW to enter flashing mode +#define PIN_NONE_4 4 +#define PIN_LED_HEATER2 16 +#define PIN_LED_HEATER1 17 +#define PIN_SW_DOWN 5 +#define PIN_SW_UP 18 +#define PIN_SW_SET 19 +#define PIN_SDA 21 +#define PIN_RX 3 +#define PIN_TX 1 +#define PIN_SCL 22 +#define PIN_NONE_3 23 +// End of PIN Definition +// ======================= + +#define PWM_RESOLUTION 10 // 10-bit resolution +#define PWM_MOTOR_RESOLUTION 8 + +#define PWM_AP_CHANNEL 9 +#define PWM_SC_CHANNEL 10 +#define PWM_AP_FREQ 1 +#define PWM_SC_FREQ 2 + +#define PWM_MIST_CHANNEL 8 // MIST +#define PWM_MIST_FREQ 1 // MIST + +#define PWM_HEATER1_CHANNEL 2 +#define PWM_HEATER2_CHANNEL 3 +#define PWM_LIGHT_CHANNEL 4 +#define PWM_MOTOR_CHANNEL 5 +#define PWM_FAN_CHANNEL 6 + +#define PWM_1KHZ_FREQ 1000 // 1 kHz frequency +#define PWM_25KHZ_FREQ 25000 // 25KHz + +#define PWM_FULL 1023 +#define PWM_OFF 0 +#define TAG "HC" + +enum EVENT_TYPE { + // Temperature + + // Humidity + + // Light + + // Fan + + // Connection + WiFi_Conneted, + WiFi_Disconnected, + BT_Connected, + BT_Disconnected +}; + +#define FLAG_MANUAL_HEATER1 (0x0001) +#define FLAG_MANUAL_HEATER2 (0x0002) +#define FLAG_MANUAL_MIST (0x0004) +#define FLAG_MANUAL_FAN (0x0008) + +#define FLAG_MANUAL_MOTOR (0x0010) +#define FLAG_MANUAL_LIGHT (0x0020) +#define FLAG_ZCD_AC (0x0040) +#define FLAG_ZCD_LOAD (0x0080) + +#define FLAG_ZCD (0x0100) +#define FLAG_UPNP (0x0200) +#define FLAG_BLE_BATT (0x0400) +#define FLAG_BLE_NODATA (0x0800) +#define FLAG_BLE_LOST (0x1000) + + +#pragma pack(push) /* push current alignment to stack */ +#pragma pack(1) /* set alignment to 1 byte boundary */ +typedef struct DEVICE_PARAM_STRUCT { + uint8_t nControlType; + uint8_t x; + uint8_t bDay, bNight; + //4 + uint16_t dutyMin, dutyMax, dutyStart; + //6 + 4 = 10 + union { + uint16_t dutyDay; + uint16_t dutyOn; + uint16_t dutyDayOrOn; + }; + union { + uint16_t dutyNight; + uint16_t dutyOff; + uint16_t dutyNightOrOff; + }; + //4 + 10 = 14 + int16_t tempHigh, tempLow; + uint16_t timeBegin, timeEnd; + uint16_t periodPeriod, periodOn; + uint16_t humidHigh, humidLow; + //16 + 14 = 30 + + uint8_t extra[32 - 30]; +} DEVICE_PARAM_TYPE; + +typedef struct STATUS_STRUCT { + // Sensor + int16_t nTemp1; + int16_t nTemp2; + int16_t nTemp3; + int16_t nHumid1; + int16_t nHumid2; + // 10 + + // Control + uint16_t nHeater1Duty; // (0..10000) + uint16_t nHeater2Duty; // (0..10000) + uint16_t nMistDuty; // + uint16_t nFanDuty; // + uint16_t nMotorDuty; + uint16_t nLightDuty; + uint16_t nLightTargetDuty; + // 24 + + // Current Time + uint32_t now; + // 28 + + uint16_t nFlags; + // 30 + + // AC status + uint8_t zcdAC; + uint8_t zcdLoad; + // 32 +} STATUS_TYPE; +#pragma pack(pop) /* restore original alignment from stack */ + +// ======================================================= + +char *printStatus(unsigned long tick, bool bLong = false); +char *printTime(bool bLong = false); +void checkSerial(unsigned long tick); +void checkWiFi(unsigned long tick); +void checkWiFiHost(unsigned long tick); + +void core0Task(void *pvParameters); + +// ======================================================= + +// Status +extern STATUS_TYPE status; + + +// Time +extern volatile unsigned short g_nYear, g_nMonth, g_nDay, g_nHour, g_nMinute, g_nSecond; + +// Environment +extern bool bShowSensor; +extern const char *COMPANY_NAME; +extern const char *SERVICE_NAME; +extern const char *HC__VERSION; + +#endif diff --git a/HermitCrab.vcxproj b/HermitCrab.vcxproj new file mode 100644 index 0000000..6441600 --- /dev/null +++ b/HermitCrab.vcxproj @@ -0,0 +1,72 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {9EFFA72D-8A81-44D2-B7B4-37C53EBAD29F} + + + + Application + true + v142 + + + Application + false + v142 + + + Application + true + v142 + + + Application + false + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HermitCrab.vcxproj.user b/HermitCrab.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/HermitCrab.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/History.cpp b/History.cpp new file mode 100644 index 0000000..450df18 --- /dev/null +++ b/History.cpp @@ -0,0 +1,214 @@ +#include "History.h" +#include "Config.h" + +extern class Preferences preferences; +CHistory history; + +CHistory::CHistory() + : head(0), tail(0), count(0), lastTemp(0), lastHumid(0) +{ + Kd_Humidity = 3.6; // Load Kd for humidity + Kd_Humidity = 1.8; + LR_Humidity = 0.25; + + Kp_Temp1 = 3.6; + Kd_Temp1 = 1.8; + LR_Temp1 = 0.25; + + head = 0; + tail = 0; + count = 0; + + lastTemp = 0; + lastHumid = 0; + for (int i = 0; i < RING_SIZE; i++) + ring[i] = {{0}}; +} + +void CHistory::init(int16_t _lastTemp, int16_t _lastHumid) { + lastTemp = _lastTemp; + lastHumid = _lastHumid; +} + +void CHistory::loadPID() { + Kp_Humidity = config.Kp_Humidity; + Kd_Humidity = config.Kd_Humidity; + LR_Humidity = config.LR_Humidity; + + Kp_Temp1 = config.Kp_Temp1; + Kd_Temp1 = config.Kd_Temp1; + LR_Temp1 = config.LR_Temp1; + + Kp_Temp2 = config.Kp_Temp2; + Kd_Temp2 = config.Kd_Temp2; + LR_Temp2 = config.LR_Temp2; + + Kp_Temp3 = config.Kp_Temp3; + Kd_Temp3 = config.Kd_Temp3; + LR_Temp3 = config.LR_Temp3; + +} + +void CHistory::savePID() { + config.Kp_Humidity = Kp_Humidity; + config.Kd_Humidity = Kd_Humidity; + config.LR_Humidity = LR_Humidity; + + config.Kp_Temp1 = Kp_Temp1; + config.Kd_Temp1 = Kd_Temp1; + config.LR_Temp1 = LR_Temp1; + + config.Kp_Temp2 = Kp_Temp2; + config.Kd_Temp2 = Kd_Temp2; + config.LR_Temp2 = LR_Temp2; + + config.Kp_Temp3 = Kp_Temp3; + config.Kd_Temp3 = Kd_Temp3; + config.LR_Temp3 = LR_Temp3; +} + +// Method to add status data to the history buffer +MY_IRAM_ATTR short CHistory::add(STATUS_TYPE &status) { + + ring[head] = status; + + if (++head >= RING_SIZE) head = 0; // Mask with 0xFF for wrap-around + if (count < RING_SIZE) { + count++; + } else { + if (++tail > RING_SIZE) tail = 0; // Advance tail if buffer is full + } + return count; +} + +// Main PD Control method for heater duty calculation +MY_IRAM_ATTR int16_t CHistory::calculateDutyForTemp1(int16_t setpoint, int16_t curTemp, int16_t lastDuty) { + // Calculate Proportional term + float currentError = setpoint - curTemp; + float P = Kp_Temp1 * currentError; + + // Calculate Derivative term based on temperature slope + float timeInterval = 10.0; // Assuming a 5-second interval between updates; adjust as needed + float slope = (curTemp - lastTemp) / timeInterval; + float D = Kd_Temp1 * (-slope); // Negative sign to counteract the rising trend + + // Limit magnitute of change + int16_t diff = roundf(P + D); + if (diff > 500) diff = 500; + else if (diff < -500) diff = -500; + + // Calculate new heater duty cycle + int16_t newDuty = lastDuty + diff; + + // Constrain the duty cycle within the range of 0 to 10000 + if (newDuty < 0) newDuty = 0; + else if (newDuty > 10000) newDuty = 10000; + + // Adjust Kp and Kd using gradient descent based on the current and previous errors + adjustGainsUsingGradientDescent(Kp_Temp1, Kd_Temp1, currentError, setpoint, lastTemp, 100.0f); + if (Kp_Temp1 < 0.5f * config.Kp_Temp1) Kp_Temp1 = 0.5f * config.Kp_Temp1; + else if (Kp_Temp1 > 2.0f * config.Kp_Temp1) Kp_Temp1 = 2.0f * config.Kp_Temp1; + if (Kd_Temp1 < 0.5f * config.Kd_Temp1) Kd_Temp1 = 0.5f * config.Kd_Temp1; + else if (Kd_Temp1 > 2.0f * config.Kd_Temp1) Kd_Temp1 = 2.0f * config.Kd_Temp1; + + lastTemp = curTemp; + return newDuty; +} + + +MY_IRAM_ATTR int16_t CHistory::calculateDutyForTemp2(int16_t setpoint, int16_t curTemp, int16_t lastDuty) { + // Calculate Proportional term + float currentError = setpoint - curTemp; + float P = Kp_Temp2 * currentError; + + // Calculate Derivative term based on temperature slope + float timeInterval = 10.0; // Assuming a 5-second interval between updates; adjust as needed + float slope = (curTemp - lastTemp) / timeInterval; + float D = Kd_Temp2 * (-slope); // Negative sign to counteract the rising trend + + // Limit magnitute of change + int16_t diff = roundf(P + D); + if (diff > 500) diff = 500; + else if (diff < -500) diff = -500; + + // Calculate new heater duty cycle + int16_t newDuty = lastDuty + diff; + + // Constrain the duty cycle within the range of 0 to 10000 + if (newDuty < 0) newDuty = 0; + else if (newDuty > 10000) newDuty = 10000; + + // Adjust Kp and Kd using gradient descent based on the current and previous errors + adjustGainsUsingGradientDescent(Kp_Temp2, Kd_Temp2, currentError, setpoint, lastTemp, 100.0f); + if (Kp_Temp2 < 0.5f * config.Kp_Temp2) Kp_Temp2 = 0.5f * config.Kp_Temp2; + else if (Kp_Temp2 > 2.0f * config.Kp_Temp2) Kp_Temp2 = 2.0f * config.Kp_Temp2; + if (Kd_Temp2 < 0.5f * config.Kd_Temp2) Kd_Temp2 = 0.5f * config.Kd_Temp2; + else if (Kd_Temp2 > 2.0f * config.Kd_Temp2) Kd_Temp2 = 2.0f * config.Kd_Temp2; + + lastTemp = curTemp; + return newDuty; +} + + +MY_IRAM_ATTR int16_t CHistory::calculateMistDuty(uint16_t setpoint, uint16_t curHumid, int16_t lastDuty) { + // Calculate Proportional (P) term based on the error (difference between setpoint and current humidity) + float currentError = setpoint - curHumid; + float P = Kp_Humidity * currentError; + + // Calculate Derivative (D) term based on humidity slope + float timeInterval = 10.0; // Assuming a 10-second interval between updates; adjust as needed + float slope = (curHumid - lastHumid) / timeInterval; + float D = Kd_Humidity * (-slope); // Negative sign to counteract the rising or falling trend + + // Limit magnitute of change + int16_t diff = roundf(P + D); + if (diff > 500) diff = 500; + else if (diff < -500) diff = -500; + + // Calculate the new mist duty cycle + int16_t newDuty = lastDuty + diff; + + // Constrain the duty cycle within the allowable range (0 to 511 for 50% duty cycle) + if (newDuty < 0) newDuty = 0; + else if (newDuty > 10000) newDuty = 10000; + + // Adjust Kp and Kd using gradient descent based on current and previous errors + adjustGainsUsingGradientDescent(Kp_Humidity, Kd_Humidity, currentError, setpoint, lastHumid, 300.0f); + if (Kp_Humidity < 0.5f * config.Kp_Humidity) Kp_Humidity = 0.5f * config.Kp_Humidity; + else if (Kp_Humidity > 2.0f * config.Kp_Humidity) Kp_Humidity = 2.0f * config.Kp_Humidity; + if (Kd_Humidity < 0.5f * config.Kd_Humidity) Kd_Humidity = 0.5f * config.Kd_Humidity; + else if (Kd_Humidity > 2.0f * config.Kd_Humidity) Kd_Humidity = 2.0f * config.Kd_Humidity; + + lastHumid = curHumid; + return newDuty; +} + +// Method to adjust Kp and Kd using gradient descent based on current and previous errors +MY_IRAM_ATTR void CHistory::adjustGainsUsingGradientDescent(float &Kp, float &Kd, float currentError, uint16_t setpoint, float prevTemperature, float MAX_EXPECTED_ERROR) { + // Normalize the error (assuming a max expected error for normalization) + float normalizedError = currentError / MAX_EXPECTED_ERROR; + + // Calculate change in temperature as the slope + float temperatureSlope = (setpoint - prevTemperature); + + // Define limits for adjusting Kp and Kd + const float proportionalLimitFactorKp = 0.25f; + const float proportionalLimitFactorKd = 0.25f; + + // Scale the gradients based on normalized error and temperature slope + float limitedProportionalGradient = proportionalLimitFactorKp * Kp; + float limitedDerivativeGradient = proportionalLimitFactorKd * Kd; + + // Update Kp and Kd using their respective gradients, ensuring adjustments are within limits + Kp -= LR_Temp1 * fmin(fabs(normalizedError), limitedProportionalGradient) * (normalizedError < 0 ? 1 : -1); + Kd -= LR_Temp1 * fmin(fabs(temperatureSlope), limitedDerivativeGradient) * (temperatureSlope < 0 ? 1 : -1); + + // Constrain Kp and Kd to avoid going below minimum threshold values + const float MIN_KP = 0.01f; + const float MIN_KD = 0.01f; + Kp = max(Kp, MIN_KP); + Kd = max(Kd, MIN_KD); +} + + + diff --git a/History.h b/History.h new file mode 100644 index 0000000..6079091 --- /dev/null +++ b/History.h @@ -0,0 +1,69 @@ +#ifndef __HISTORY_H +#define __HISTORY_H + +#include "HermitCrab.h" // Assuming CURRENT_STATUS structure is defined elsewhere + +#define RING_SIZE (256 * 7) +//#define RING_MASK (RING_SIZE - 1) + +class CHistory { + private: + int16_t head; + int16_t tail; + int16_t count; + STATUS_TYPE ring[RING_SIZE]; // Ring buffer + + float Kp_Humidity; + float Kd_Humidity; + float LR_Humidity; + + float Kp_Temp1; + float Kd_Temp1; + float LR_Temp1; + + float Kp_Temp2; + float Kd_Temp2; + float LR_Temp2; + + float Kp_Temp3; + float Kd_Temp3; + float LR_Temp3; + + int16_t lastTemp, lastHumid; + + public: + CHistory(); + + void loadPID(); + void savePID(); + void init(int16_t lastTemp, int16_t lastHumid); + + inline float getKpTemperature() { return Kp_Temp1;} + inline float getKdTemperature() { return Kd_Temp1;} + inline float getLRTemperature() { return LR_Temp1;} + inline float getKpHumidity() { return Kp_Humidity;} + inline float getKdHumidity() { return Kd_Humidity;} + inline float getLRHumidity() { return LR_Humidity;} + inline float getKpHeater1() { return Kp_Temp1; } + inline float getKdHeater1() { return Kd_Temp1; } + inline float getKpMist() { return Kp_Humidity; } + inline float getKdMist() { return Kd_Humidity; } + + int16_t add(STATUS_TYPE &status); + int16_t calculateDutyForTemp1(int16_t setPoint, int16_t curTemp, int16_t lastDuty); + int16_t calculateDutyForTemp2(int16_t setPoint, int16_t curTemp, int16_t lastDuty); + int16_t calculateMistDuty(uint16_t setPpoint, uint16_t curHumid, int16_t lastDuty); + inline int16_t getRingCount() { return count; }; + inline int16_t getRingSize() { return RING_SIZE; } + inline uint8_t *getRingData1() { return (uint8_t *) &ring[tail]; } + inline uint8_t *getRingData2() { return (uint8_t *) &ring[0]; } + inline int16_t getRingHead() { return head; } + inline int16_t getRingTail() { return tail; } + + private: + void adjustGainsUsingGradientDescent(float &Kp, float &Kd, float currentError, uint16_t setpoint, float prevTemperature, float MAX_EXPECTED_ERROR); +}; + +extern CHistory history; +#endif // CHISTORY_H + diff --git a/LED0.cpp b/LED0.cpp new file mode 100644 index 0000000..5bddf47 --- /dev/null +++ b/LED0.cpp @@ -0,0 +1,52 @@ +#include "LED0.h" + +CLED0 led0; + +void CLED0::setup(uint8_t _pin, uint16_t _freq, uint16_t _channel) { + freq = _freq; + channel = _channel; + pin = _pin; + bPWMMode = true; + bAC = false; + bLoad = false; + duty = 0; + + ledcAttachChannel(pin, _freq, PWM_RESOLUTION, channel); + setDuty(duty); +}; + +MY_IRAM_ATTR void CLED0::setFreq(uint16_t _freq) { + if (freq != _freq) { + if (_freq == 0) { + ledcDetach(pin); + pinMode(pin, OUTPUT); + digitalWrite(pin, LED_OFF); + bPWMMode = false; + } + else { + ledcAttachChannel(pin, _freq, PWM_RESOLUTION, channel); + bPWMMode = true; + } + freq = _freq; + } +} + +MY_IRAM_ATTR void CLED0::setDuty() { + uint16_t _duty; + + if (bAC) _duty = LED0_DUTY_AC; + else if (bLoad) _duty = LED0_DUTY_LOAD; + else _duty = duty; + + ledcWrite(PIN_LED_WIFI, PWM_FULL * (100 - _duty) / 100); // Light Blink +} + +MY_IRAM_ATTR void CLED0::setDuty(uint16_t _duty) { + if (duty != _duty) { + if (bPWMMode) + ledcWrite(PIN_LED_WIFI, PWM_FULL * (100 - _duty) / 100); // Light Blink + else + digitalWrite(pin, _duty ? LED_ON : LED_OFF); + duty = _duty; + } +}; diff --git a/LED0.h b/LED0.h new file mode 100644 index 0000000..1e91455 --- /dev/null +++ b/LED0.h @@ -0,0 +1,38 @@ +#ifndef __LED0_H +#define __LED0_H + +#ifndef __HERMIT_CRAB_H +#include "HermitCrab.h" +#endif + +#define LED0_DUTY_BOOT 20 +#define LED0_DUTY_CONNECTED 0 +#define LED0_DUTY_CONNECTING 5 +#define LED0_DUTY_SMART_CONFIG 100 +#define LED0_DUTY_CLIENT 1 +#define LED0_DUTY_AC 90 +#define LED0_DUTY_LOAD 80 + +class CLED0 { +public: + void setup(uint8_t _pin, uint16_t _freq, uint16_t _channel); + void loop(); + void setFreq(uint16_t _freq); + void setDuty(uint16_t _duty); + void setDuty(); + inline void setAC() { bAC = true; }; + inline void clearAC() { bAC = false; }; + inline void setLoad() { bLoad = true; }; + inline void clearLoad() { bLoad = false; }; + +private: + uint16_t channel; + uint16_t freq; + uint16_t duty; + uint8_t pin; + bool bPWMMode; + bool bAC, bLoad; +}; + +extern CLED0 led0; +#endif \ No newline at end of file diff --git a/NTC_10K.cpp b/NTC_10K.cpp new file mode 100644 index 0000000..85e14d7 --- /dev/null +++ b/NTC_10K.cpp @@ -0,0 +1,127 @@ +#include "Arduino.h" + +#include "HermitCrab.h" +#include "NTC_10K.h" +#include "Config.h" + +const float resistance[] = { + 3360850.37, // -40°C + 1973470.32, // -35°C + 1179560.43, // -30°C + 718858.73, // -25°C + 445267.47, // -20°C + 281046.96, // -15°C + 180321.36, // -10°C + 117081.11, // -5°C + 77147.90, // 0°C + 51471.97, // 5°C + 34838.43, // 10°C + 23847.63, // 15°C + + 16594.38, // 20°C + 12307.39, // 21°C + 11739.87, // 22°C + 11203.64, // 23°C + 10696.86, // 24°C + 10217.84, // 25°C + 9527.52, // 26°C + 9076.66, // 27°C + 8656.02, // 28°C + 8263.81, // 29°C + 7897.84, // 30°C + + 5868.86, // 35°C + 4383.72, // 40°C + 3315.12, // 45°C + 2527.73, // 50°C + 1942.15, // 55°C + 1505.11, // 60°C + 1174.71, // 65°C + 926.23, // 70°C + 735.99, // 75°C + 588.91, // 80°C + 474.43, // 85°C + 384.48, // 90°C + 313.88, // 95°C + 258.87, // 100°C + 215.64, // 105°C + 181.10, // 110°C + 153.01, // 115°C + 129.77, // 120°C + 110.27, // 125°C + 94.03, // 130°C + 80.13, // 135°C + 68.18, // 140°C + 57.99, // 145°C + 49.30 // 150°C + }; + +const int16_t temp_C[] = { + -40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, + 105, 110, 115, 120, 125, 130, 135, 140, 145, 150 }; + +NTC_10K ntc; + +void NTC_10K::setup(bool bNegativePolarity) { + m_bNegativePolarity = bNegativePolarity; + _vRef = 3.3f; + _RESO = 4095; + for (int i = 0; i < 16; i++) temps[i] = 0; + temp_idx = 0; + temp_sum = 0; + + pinMode(PIN_NTC, INPUT); // Set PIN_NTC as input + analogReadResolution(12); // Set ADC resolution to 12 bits (0-4095) + analogSetAttenuation(ADC_11db); // Set attenuation for full-scale 3.3V +} + +void NTC_10K::readSensor() { + float Vin; + int16_t temp; + static int16_t lastTemp = 0; + + int adcValue = analogRead(PIN_NTC); // Read ADC value from PIN_NTC + + // Calculate the input voltage from the ADC reading + Vin = (float)adcValue * _vRef / _RESO; + + // Calculate the resistance of the thermistor + // Calculate the resistance of the thermistor (adjusted for inverted ADC behavior) + + float r; + if (m_bNegativePolarity) { + // NTC is connected to Negative + r = (Vin / (_vRef - Vin)) * rRef; + } else { + // NTC is connected to Positive + r = ((_vRef - Vin) / Vin) * rRef; + } + + // Find the index of the resistance in the table where r is between resistance[i-1] and resistance[i] + int i = 0; + while (i < sizeof(resistance) / sizeof(resistance[0]) - 1 && resistance[i] > r) { + i++; + } + + // If r is out of range, return the closest extreme temperature + if (i == 0 || i == sizeof(resistance) / sizeof(resistance[0]) - 1) { + if (lastTemp != 0) temp = lastTemp; + else temp = 0; + } + else { + // Interpolate between resistance[i-1] and resistance[i] + float m = (temp_C[i] - temp_C[i - 1]) / (resistance[i] - resistance[i - 1]); // Slope + float b = temp_C[i - 1] - (m * resistance[i - 1]); // Intercept + temp = (int16_t) roundf((m * r + b) * 10.0f); + } + + // Return the temperature as an integer scaled by 10 (e.g., 25.3°C => 253) + temp_sum -= temps[temp_idx]; + temps[temp_idx++] = temp; + temp_idx &= NTC_MASK; + temp_sum += temp; + lastTemp = temp; + m_nTemp = temp_sum / NTC_COUNT; +} diff --git a/NTC_10K.h b/NTC_10K.h new file mode 100644 index 0000000..2aefd5b --- /dev/null +++ b/NTC_10K.h @@ -0,0 +1,32 @@ +#ifndef __NTC_H +#define __NTC_H + +#ifndef rRef +#define rRef 10000 +#endif + +#define NTC_COUNT 32 +#define NTC_MASK (NTC_COUNT - 1) + +class NTC_10K { +private: + //lookup table + //float resistance[64]; + //int16_t temp_C[64]; + int16_t temps[NTC_COUNT]; + int16_t temp_sum; + int16_t temp_idx; + + float _vRef; + int _RESO; + bool m_bNegativePolarity; + int16_t m_nTemp; + +public: + void setup(bool bNegativePolarity); + void readSensor(); + inline int16_t getTemp() { return m_nTemp; }; +}; + +extern NTC_10K ntc; +#endif \ No newline at end of file diff --git a/OTA.cpp b/OTA.cpp new file mode 100644 index 0000000..f194867 --- /dev/null +++ b/OTA.cpp @@ -0,0 +1,134 @@ +#define NO_GLOBAL_UPDATE +#include +#include +#include +#include +#include + +#include "HCUpdate.h" +#include "HermitCrab.h" +#include "Config.h" +#include "ConnectWiFi.h" + +#define TAG_OTA "OTA" + +// ============================================================== +// +// OTA +// +// ============================================================== +const char *HC__VERSION = "20250405001"; +#define UPDATE_PORT ((uint16_t) 443) +String url = "visionsoft.kr"; +String uri = "/hc/hc_firmware_update.php"; +const char *HTTPUPDATE_USERAGRENT = "ESP32-http-Update"; +const char *COMPANY_NAME = "VisionSoft"; +const char *SERVICE_NAME = "HermitCrab"; + +const char* rootCACertificate = \ +"-----BEGIN CERTIFICATE-----\n" \ +"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" \ +"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n" \ +"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n" \ +"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n" \ +"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n" \ +"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n" \ +"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n" \ +"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n" \ +"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n" \ +"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n" \ +"B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n" \ +"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n" \ +"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n" \ +"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n" \ +"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n" \ +"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n" \ +"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n" \ +"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n" \ +"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n" \ +"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n" \ +"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n" \ +"NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n" \ +"ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n" \ +"TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n" \ +"jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n" \ +"oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n" \ +"4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n" \ +"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n" \ +"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n" \ +"-----END CERTIFICATE-----\n"; + +String getSketchSHA256(); // Function to retrieve current sketch hash + +// Callback function for OTA progress +void onOTAProgress(int current, int total) { + ESP_LOGD(TAG_OTA,"OTA -- Progress: %d%%\n", (current * 100) / total); +} + +//========================================================================== +String urlEncode(const String &url, const char *safeChars = "-_.~") { + String encoded = ""; + char temp[4]; + + for (int i = 0; i < url.length(); i++) { + temp[0] = url.charAt(i); + if (temp[0] == 32) { //space + encoded.concat('+'); + } else if ((temp[0] >= 48 && temp[0] <= 57) /*0-9*/ + || (temp[0] >= 65 && temp[0] <= 90) /*A-Z*/ + || (temp[0] >= 97 && temp[0] <= 122) /*a-z*/ + || (strchr(safeChars, temp[0]) != NULL) /* "=&-_.~" */ + ) { + encoded.concat(temp[0]); + } else { //character needs encoding + snprintf(temp, 4, "%%%02X", temp[0]); + encoded.concat(temp); + } + } + return encoded; +} + +//========================================================================== +bool addQuery(String *query, const String name, const String value) { + if (name.length() && value.length()) { + if (query->length() < 3) { + *query = "?"; + } else { + query->concat('&'); + } + query->concat(urlEncode(name)); + query->concat('='); + query->concat(urlEncode(value)); + return true; + } + return false; +} + +//========================================================================== +bool checkOTA(bool bForceUpdate) +{ + + // Set callbacks + //Update.onStart(onOTAStart); + //Update.onEnd(onOTAEnd); + String query = ""; + addQuery(&query, "cmd", (bForceUpdate ? "download" : "check")); //action command + uri.concat(query); + String version = String(HC__VERSION); + ESP_LOGI(TAG_OTA,"OTA - URL: %s\n", url.c_str()); + ESP_LOGI(TAG_OTA,"OTA - URI: %s\n", uri.c_str()); + ESP_LOGI(TAG_OTA,"OTA - Ver: %s\n", HC__VERSION); + + + WiFiClientSecure client; + client.setCACert(rootCACertificate); + UpdateClass hcUpdate(5000); + hcUpdate.onProgress(onOTAProgress); + //int update(WiFiClient& client, String &url, uint16_t port, String& uri, + // String ¤tVersion, short nDeviceType, bool bForceUpdate); + int result = hcUpdate.update(client, url, UPDATE_PORT, uri, version, (short)config.m_nDeviceType, true); + + + + return false; +} \ No newline at end of file diff --git a/OTA.h b/OTA.h new file mode 100644 index 0000000..fc7c5b8 --- /dev/null +++ b/OTA.h @@ -0,0 +1,10 @@ +#ifndef __OTA_H +#define __OTA_H + +extern const char *COMPANY_NAME; +extern const char *SERVICE_NAME; +extern const char *HC__VERSION; +extern const char* rootCACertificate; +bool checkOTA(bool bForceUpdate); + +#endif \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..49cc8ef0e116cef009fe0bd72473a964bbd07f9b GIT binary patch literal 6 NcmezWkC%aq0RRg=0u=xN literal 0 HcmV?d00001 diff --git a/SSD1306.cpp b/SSD1306.cpp new file mode 100644 index 0000000..bae93c6 --- /dev/null +++ b/SSD1306.cpp @@ -0,0 +1,225 @@ +#include "HermitCrab.h" +#include "SSD1306.h" + +//#define DISPLAY_WIDTH SCREEN_WIDTH +//#define DISPLAY_HEIGHT SCREEN_HEIGHT + + +#define WIRE_WRITE Wire.write +#define SETWIRECLOCK Wire.setClock(400000UL) ///< Set before I2C transfer +#define RESWIRECLOCK Wire.setClock(100000UL) ///< Restore after I2C xfer +#define TRANSACTION_START SETWIRECLOCK +#define TRANSACTION_END RESWIRECLOCK +#define WIRE_MAX I2C_BUFFER_LENGTH + + +// Constructor for SSD1306 class +//SSD1306::SSD1306(Adafruit_SSD1306 &display) +SSD1306::SSD1306() + : Adafruit_GFX(SCREEN_WIDTH, SCREEN_HEIGHT) + , i2caddr(DISPLAY_I2C_ADDRESS) +{} + +void SSD1306::dim(bool dim) { + // the range of contrast to too small to be really useful + // it is useful to dim the display + TRANSACTION_START; + ssd1306_command1(SSD1306_SETCONTRAST); + ssd1306_command1(dim ? 0 : contrast); + TRANSACTION_END; +} + +void SSD1306::setContrast(uint8_t con) { + // the range of contrast to too small to be really useful + // it is useful to dim the display + TRANSACTION_START; + ssd1306_command1(SSD1306_SETCONTRAST); + ssd1306_command1(100 * con / contrast); + TRANSACTION_END; +} + +bool SSD1306::begin(uint8_t vcs, uint8_t addr) { + vccstate = vcs; + i2caddr = addr; + + TRANSACTION_START; + + // Init sequence + static const uint8_t init1[] = {SSD1306_DISPLAYOFF, // 0xAE + SSD1306_SETDISPLAYCLOCKDIV, // 0xD5 + 0x80, // the suggested ratio 0x80 + SSD1306_SETMULTIPLEX, + SCREEN_HEIGHT - 1}; // 0xA8 + ssd1306_commandList(init1, sizeof(init1)); + + static const uint8_t init2[] = {SSD1306_SETDISPLAYOFFSET, // 0xD3 + 0x0, // no offset + SSD1306_SETSTARTLINE | 0x0, // line #0 + SSD1306_CHARGEPUMP}; // 0x8D + ssd1306_commandList(init2, sizeof(init2)); + + ssd1306_command1((vccstate == SSD1306_EXTERNALVCC) ? 0x10 : 0x14); + + static const uint8_t init3[] = {SSD1306_MEMORYMODE, // 0x20 + 0x00, // 0x0 act like ks0108 + SSD1306_SEGREMAP | 0x1, + SSD1306_COMSCANDEC}; + ssd1306_commandList(init3, sizeof(init3)); + + uint8_t comPins = 0x02; + contrast = 0x8F; + + if ((WIDTH == 128) && (HEIGHT == 32)) { + comPins = 0x02; + contrast = 0x8F; + } else if ((WIDTH == 128) && (HEIGHT == 64)) { + comPins = 0x12; + contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x9F : 0xCF; + } else if ((WIDTH == 96) && (HEIGHT == 16)) { + comPins = 0x2; // ada x12 + contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x10 : 0xAF; + } else { + // Other screen varieties -- TBD + } + + ssd1306_command1(SSD1306_SETCOMPINS); + ssd1306_command1(comPins); + ssd1306_command1(SSD1306_SETCONTRAST); + ssd1306_command1(contrast); + + ssd1306_command1(SSD1306_SETPRECHARGE); // 0xd9 + ssd1306_command1((vccstate == SSD1306_EXTERNALVCC) ? 0x22 : 0xF1); + static const uint8_t init5[] = { + SSD1306_SETVCOMDETECT, // 0xDB + 0x40, + SSD1306_DISPLAYALLON_RESUME, // 0xA4 + SSD1306_NORMALDISPLAY, // 0xA6 + SSD1306_DEACTIVATE_SCROLL, + SSD1306_DISPLAYON}; // Main screen turn on + ssd1306_commandList(init5, sizeof(init5)); + + TRANSACTION_END; + clearDisplayBuffer(); + return true; +} + +MY_IRAM_ATTR void SSD1306::updateScreen() { + TRANSACTION_START; + uint8_t dlist1[6]; + dlist1[0] = SSD1306_PAGEADDR; + dlist1[1] = 0; + dlist1[2] = (height() / 8) - 1; + dlist1[3] = SSD1306_COLUMNADDR; + dlist1[4] = 0; + dlist1[5] = width() - 1; + ssd1306_commandList(dlist1, sizeof(dlist1)); + + uint16_t count = height() * width() / 8; + uint8_t *ptr = getBuffer(); + + Wire.beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x40); + uint16_t bytesOut = 1; + // Loop through each page in the range + while (count--) { + if (bytesOut >= WIRE_MAX) { + Wire.endTransmission(); + Wire.beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x40); + bytesOut = 1; + } + WIRE_WRITE(*ptr++); + bytesOut++; + } + Wire.endTransmission(); // End transmission for the page + + TRANSACTION_END; +} + +MY_IRAM_ATTR void SSD1306::updateRegion(uint8_t pageStart, uint8_t pageEnd, uint8_t colStart = 0, uint8_t colEnd = 127) { + TRANSACTION_START; + uint8_t dlist1[6]; + dlist1[0] = SSD1306_PAGEADDR; + dlist1[1] = pageStart; + dlist1[2] = pageEnd; + dlist1[3] = SSD1306_COLUMNADDR; + dlist1[4] = colStart; + dlist1[5] = colEnd; + ssd1306_commandList(dlist1, sizeof(dlist1)); + + uint16_t count = (colEnd - colStart + 1) * (pageEnd - pageStart + 1); + uint8_t *ptr = getBuffer() + pageStart * SCREEN_WIDTH + colStart; + uint16_t cols = colEnd - colStart + 1; + uint16_t offset = SCREEN_WIDTH - cols; + + Wire.beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x40); + uint16_t bytesOut = 1; + // Loop through each page in the range + for (uint8_t page = pageStart; page <= pageEnd; page++) { + // Send column data for the current page + for (uint8_t col = 0; col < cols; col++) { + if (bytesOut >= WIRE_MAX) { + Wire.endTransmission(); + Wire.beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x40); + bytesOut = 1; + } + WIRE_WRITE(*ptr++); + bytesOut++; + } + ptr += offset; + } + Wire.endTransmission(); // End transmission for the page + TRANSACTION_END; +} + +MY_IRAM_ATTR void SSD1306::ssd1306_hscroll(uint8_t dir, uint8_t pageStart, uint8_t pageEnd, uint8_t offset, uint8_t interval) { + ssd1306_command1(dir ? SSD1306_RIGHT_HORIZONTAL_SCROLL : SSD1306_LEFT_HORIZONTAL_SCROLL); + ssd1306_command1(0x00); // Dummy + ssd1306_command1(pageStart); + ssd1306_command1(interval); + ssd1306_command1(pageEnd); + ssd1306_command1(offset); + ssd1306_command1(SSD1306_ACTIVATE_SCROLL); +} + +MY_IRAM_ATTR void SSD1306::drawPixel(int16_t x, int16_t y, uint16_t color) { + if ((x >= 0) && (x < width()) && (y >= 0) && (y < height())) { + switch (color) { + case SSD1306_WHITE: + buffer[x + (y / 8) * WIDTH] |= (1 << (y & 7)); + break; + case SSD1306_BLACK: + buffer[x + (y / 8) * WIDTH] &= ~(1 << (y & 7)); + break; + case SSD1306_INVERSE: + buffer[x + (y / 8) * WIDTH] ^= (1 << (y & 7)); + break; + } + } +} + +MY_IRAM_ATTR void SSD1306::ssd1306_commandList(const uint8_t *c, uint8_t n) { + Wire.beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0 + uint16_t bytesOut = 1; + while (n--) { + if (bytesOut >= WIRE_MAX) { + Wire.endTransmission(); + Wire.beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0 + bytesOut = 1; + } + WIRE_WRITE(*c++); + bytesOut++; + } + Wire.endTransmission(); +} + +MY_IRAM_ATTR void SSD1306::ssd1306_command1(uint8_t c) { + Wire.beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0 + WIRE_WRITE(c); + Wire.endTransmission(); +} diff --git a/SSD1306.h b/SSD1306.h new file mode 100644 index 0000000..58e2531 --- /dev/null +++ b/SSD1306.h @@ -0,0 +1,86 @@ +#ifndef SSD1306_H +#define SSD1306_H + +#include +#include + +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 + +#define DISPLAY_I2C_ADDRESS_32 (0x3C) +#define DISPLAY_I2C_ADDRESS_64 (0x3D) +#define DISPLAY_I2C_ADDRESS DISPLAY_I2C_ADDRESS_32 + +#define SSD1306_BLACK 0 ///< Draw 'off' pixels +#define SSD1306_WHITE 1 ///< Draw 'on' pixels +#define SSD1306_INVERSE 2 ///< Invert pixels +#define SSD1306_MEMORYMODE 0x20 ///< See datasheet +#define SSD1306_COLUMNADDR 0x21 ///< See datasheet +#define SSD1306_PAGEADDR 0x22 ///< See datasheet +#define SSD1306_SETCONTRAST 0x81 ///< See datasheet +#define SSD1306_CHARGEPUMP 0x8D ///< See datasheet +#define SSD1306_SEGREMAP 0xA0 ///< See datasheet +#define SSD1306_DISPLAYALLON_RESUME 0xA4 ///< See datasheet +#define SSD1306_DISPLAYALLON 0xA5 ///< Not currently used +#define SSD1306_NORMALDISPLAY 0xA6 ///< See datasheet +#define SSD1306_INVERTDISPLAY 0xA7 ///< See datasheet +#define SSD1306_SETMULTIPLEX 0xA8 ///< See datasheet +#define SSD1306_DISPLAYOFF 0xAE ///< See datasheet +#define SSD1306_DISPLAYON 0xAF ///< See datasheet +#define SSD1306_COMSCANINC 0xC0 ///< Not currently used +#define SSD1306_COMSCANDEC 0xC8 ///< See datasheet +#define SSD1306_SETDISPLAYOFFSET 0xD3 ///< See datasheet +#define SSD1306_SETDISPLAYCLOCKDIV 0xD5 ///< See datasheet +#define SSD1306_SETPRECHARGE 0xD9 ///< See datasheet +#define SSD1306_SETCOMPINS 0xDA ///< See datasheet +#define SSD1306_SETVCOMDETECT 0xDB ///< See datasheet + +#define SSD1306_SETLOWCOLUMN 0x00 ///< Not currently used +#define SSD1306_SETHIGHCOLUMN 0x10 ///< Not currently used +#define SSD1306_SETSTARTLINE 0x40 ///< See datasheet + +#define SSD1306_EXTERNALVCC 0x01 ///< External display voltage source +#define SSD1306_SWITCHCAPVCC 0x02 ///< Gen. display voltage from 3.3V + +#define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26 ///< Init rt scroll +#define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27 ///< Init left scroll +#define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29 ///< Init diag scroll +#define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A ///< Init diag scroll +#define SSD1306_DEACTIVATE_SCROLL 0x2E ///< Stop scroll +#define SSD1306_ACTIVATE_SCROLL 0x2F ///< Start scroll +#define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3 ///< Set scroll range + +#define SSD1306_LCDWIDTH SCREEN_WIDTH ///< DEPRECATED: width w/SSD1306_128_64 defined +#define SSD1306_LCDHEIGHT SCREEN_HEIGHT ///< DEPRECATED: height w/SSD1306_128_64 defined + +class SSD1306 : public Adafruit_GFX { +public: + // Constructor + //CUI(Adafruit_SSD1306 &display); + SSD1306(); + void dim(bool dim); + void setContrast(uint8_t contrast); + +protected: + bool bOK; + uint8_t buffer[SCREEN_WIDTH * SCREEN_HEIGHT / 8]; + int8_t i2caddr; ///< I2C address initialized when begin method is called. + uint8_t contrast; ///< normal contrast setting for this device + + bool begin(uint8_t switchvcc = SSD1306_SWITCHCAPVCC, uint8_t i2caddr = DISPLAY_I2C_ADDRESS); + inline uint8_t *getBuffer(void) { return buffer; }; + inline void clearDisplayBuffer(void) { memset(buffer, 0, WIDTH * ((HEIGHT + 7) / 8)); }; + void updateScreen(); + void updateRegion(uint8_t pageStart, uint8_t pageEnd, uint8_t colStart , uint8_t colEnd); + void ssd1306_hscroll(uint8_t dir, uint8_t pageStart, uint8_t pageEnd, uint8_t offset, uint8_t interval); + +private: + int8_t vccstate; ///< VCC selection, set by begin method. + uint32_t wireClk; ///< Wire speed for SSD1306 transfers + uint32_t restoreClk; ///< Wire speed following SSD1306 transfers + + virtual void drawPixel(int16_t x, int16_t y, uint16_t color); + void ssd1306_commandList(const uint8_t *c, uint8_t n); + void ssd1306_command1(uint8_t c); +}; +#endif diff --git a/Setup.cpp b/Setup.cpp new file mode 100644 index 0000000..9d32cef --- /dev/null +++ b/Setup.cpp @@ -0,0 +1,302 @@ +#include "HermitCrab.h" +#include "Config.h" +#include "AHT2x.h" +#include "NTC_10K.h" +#include "ZCD.h" +#include "History.h" +#include "ConnectWiFi.h" +#include "WiFiHost.h" +#include "TimeManager.h" +#include "OTA.h" +#include "UI.h" +#include "LED0.h" +#include "BLEScan.h" +#include +#include "esp_coexist.h" + +#define TAG_SETUP "TAG_SETUP" +// Task handle +TaskHandle_t TaskHandle_0; + +bool g_bWiFiSetupExecuted = false; +bool g_bWiFiHasBeenConnected = false; + +extern STATUS_TYPE status; +extern CHistory history; + +void setupConfig(); +void setupStatus(); +void restoreStatus(); +void setupWiFi(); +void setupPostWiFi(bool bBoot); +void setupPins(); +void setupSensor(); +void setupZCD(); +void setup_BLE(); +void scanI2C(); + +void setup() { + // put your setup code here, to run once: + #ifdef DEBUG + Serial.begin(115200); + #endif + //esp_log_level_set("*", ESP_LOG_INFO); // Global log level + //esp_log_level_set("BLE_POLL", ESP_LOG_INFO); // Module-specific level + //esp_coex_preference_set(ESP_COEX_PREFER_BT); + + DPRINTLN(" **********************"); + DPRINTF(" SETUP - Start - ver. %s type: %d\n", HC__VERSION, THIS_DEVICE_TYPE); + DPRINTLN(" **********************"); + g_bWiFiHasBeenConnected = false; + g_bWiFiHasBeenConnected = false; + g_nYear = 2024; + g_nMonth = 10; + g_nDay = 15; + g_nHour = 0; + g_nMinute = 0; + g_nSecond = 0; + bShowSensor = false; + + led0.setup(PIN_LED_WIFI, PWM_AP_FREQ, PWM_AP_CHANNEL); + led0.setDuty(20); + + setupConfig(); + setupStatus(); + + setupPins(); + scanI2C(); + setupSensor(); + + ui.setup(); + ui.message(0, "WiFi..."); + setupWiFi(); + + if (aht25.sensor() || aht10_0x39.sensor()) { + ui.message(4, "Sensor... OK!"); + } else { + ui.message(4, "Sensor... None!"); + } + + ui.message(5, "ZCD..."); + setupZCD(); + ui.message(5, "ZCD... OK!"); + + ui.message(6, "Setup OK!"); + //if (!isWiFiConnected) delay(3000); + ble.setupConnect(config.nBLESensorAddr, config.nBLESensorAddr2); + + // Restore Status + restoreStatus(); + + // Create a task pinned to core 0 + xTaskCreatePinnedToCore( + core0Task, // Function to run as a task + "Task0", // Task name + 10240, // Stack size in words + NULL, // Task input parameter + 0, // Priority of the task + &TaskHandle_0, // Task handle + 0 // Core 0 + ); + DPRINTLN("Setup Completed\n========================\n"); +} + + +// ====================================================================== +// +// Setup +// +// ====================================================================== +void setupConfig() { + config.load(); + history.loadPID(); + config.m_nDeviceType = THIS_DEVICE_TYPE; + if (config.m_nPublicPort == 3939) + config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF); +} + +void setupWiFi() { + // Set WiFiEvent + WiFi.onEvent(WiFiEvent); + strncpy(BLE_SSID, config.ssid, sizeof(BLE_SSID)); + strncpy(BLE_PW, config.pw, sizeof(BLE_PW)); + // Connect WiFi for OTA + if (config.ssid[0] && config.pw[0]) { +#if defined(ESP32) + esp_wifi_set_max_tx_power(84); +#elif defined(ESP8266) + WiFi.setOutputPower(20.5f); + pinMode(16,OUTPUT); + digitalWrite(16, LOW); + int c = 0; +#endif + + DPRINTF("BOOT: Connecting to WiFi: SSID: '%s', PW: '%s'\n", config.ssid, config.pw); + WiFi.mode(WIFI_STA); + WiFi.begin(config.ssid, config.pw); + + unsigned long beginTime = millis(); + while (WiFi.status() != WL_CONNECTED) { + delay(250); + DPRINT('.'); + if (millis() - beginTime > 30000) + break; + } + DPRINTLN(); + + if (WiFi.status() == WL_CONNECTED) { + ledcWrite(PIN_LED_WIFI, PWM_FULL); // LED_OFF + ui.message(0, "WiFi...OK!"); + DPRINTLN("WiFi - Connected at SETUP"); + DPRINTF("WiFi - SSID(%s) PW(%s) IP(%s)\n", config.ssid, config.pw, WiFi.localIP().toString().c_str()); + g_bWiFiHasBeenConnected = true; + + setupPostWiFi(true); + } else { + DPRINTLN("WiFi - ** NOT ** Connected at SETUP."); + //WiFi.disconnect(false, true, 500); + } + } +} + +void setupPostWiFi(bool bBoot = false) { + if (WiFi.status() == WL_CONNECTED) { + // Time + if (bBoot) ui.message(1, "Time..."); + timeManager.begin(); + vTaskDelay((bBoot ? 500 : 250)/portTICK_PERIOD_MS); + timeManager.checkNTPResponse(); + + if (bBoot) { + if (timeManager.hasNTPUpdate()) { + ui.message(1, "Time...OK!"); + + // OTA + DPRINTLN("Setup - TimeManager.begin()"); + DPRINTLN("\n===============================\n"); + DPRINTLN(" Trying OTA"); + ui.message(2, "Update check..."); + checkOTA(true); + ui.message(2, "Update check...OK!"); + DPRINTLN(" OTA Process completed!"); + DPRINTLN("===============================\n"); + } else { + ui.message(2, "Update check SKIPPED!"); + timeManager.sendNTPRequest(); + } + } + + // Host + if (bBoot) ui.message(3, "Server..."); + host.Setup(); + DPRINTLN("Setup - host.Setup()"); + if (bBoot) ui.message(3, "Server...OK!"); + g_bWiFiSetupExecuted = true; + } +} + +void setupPins() { + pinMode(PIN_HEATER1, OUTPUT); + pinMode(PIN_HEATER2, OUTPUT); + digitalWrite(PIN_HEATER1, HEATER_OFF); + digitalWrite(PIN_HEATER2, HEATER_OFF); + + //ledcAttachChannel(PIN_LED_WIFI, PWM_AP_FREQ, PWM_RESOLUTION, PWM_AP_CHANNEL); + //ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink + + ledcAttachChannel(PIN_MIST, PWM_MIST_FREQ, PWM_RESOLUTION, PWM_MIST_CHANNEL); + ledcWrite(PIN_MIST, PWM_OFF); + + ledcAttachChannel(PIN_LED_HEATER1, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER1_CHANNEL); + ledcAttachChannel(PIN_LED_HEATER2, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER2_CHANNEL); + ledcAttachChannel(PIN_LIGHT, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_LIGHT_CHANNEL); + + ledcAttachChannel(PIN_MOTOR, PWM_25KHZ_FREQ, PWM_RESOLUTION, PWM_MOTOR_CHANNEL); + ledcAttachChannel(PIN_FAN, PWM_25KHZ_FREQ, PWM_RESOLUTION, PWM_FAN_CHANNEL); + + ledcWrite(PIN_LED_HEATER1, PWM_FULL - PWM_OFF); + ledcWrite(PIN_LED_HEATER2, PWM_FULL - PWM_OFF); + ledcWrite(PIN_LIGHT, PWM_OFF); + ledcWrite(PIN_MOTOR, PWM_OFF); + ledcWrite(PIN_FAN, PWM_OFF); +} + +void setupStatus() { + if (config.bStatusSaved) { + status = config.statusSave; + config.bStatusSaved = false; + } + + + // init sensor and counter + status.nTemp1 = 0; + status.nTemp2 = 0; + status.nTemp3 = 0; + status.nHumid1 = 0; + status.nHumid2 = 0; + status.zcdAC = 0; + status.zcdLoad = 0; + status.nFlags = 0x00; +} + +void restoreStatus() { + if (isWiFiConnected()) { + if (timeManager.hasNTPUpdate()) { + time_t now; + time(&now); + uint32_t gap = (uint32_t)now - config.statusSave.now; + DPRINTF("Reboot in %.1f seconds\n", gap / 1000.0f); + if (gap < 60000 && config.bStatusSaved) { + status = config.statusSave; + status.nFlags |= (uint16_t)(config.statusSave.nFlags & 0xFF); + status.nLightDuty = 0; + config.bStatusSaved = false; + DPRINTLN(" Status Restored!"); + } + } + } +} + +void scanI2C() { + Wire.begin(); + + DPRINTLN("I2C - Scanning..."); + for (byte addr = 1; addr < 127; addr++) { + Wire.beginTransmission(addr); + if (Wire.endTransmission() == 0) { + DPRINTF("I2C - Found device at address: 0x%02X\n", addr); + } + } + DPRINTLN(" Scanning Done."); +} + +void setupSensor() { + // AHTx0 + if (aht25.setup()) { + delay(82); + aht25.readSensor(millis()); + DPRINTF("AHTx0 initialized successfully at 0x38. Temp: %.2f, Humid: %.2f%%\n", + aht25.getTemperature() / 100.0f, aht25.getHumidity() / 100.0f); + } + + delay(10); + if (aht10_0x39.setup(0x39)) { //begin(PIN_SCL, PIN_SDA, AHT10_ADDRESS_0X39)) { + delay(82); + if (aht10_0x39.readSensor(millis())) { + //aht10_0x39.initBuffer(); + } + DPRINTF("AHTx0 initialized successfully at 0x39. Temp: %.2f, Humid: %.2f%%\n", + aht10_0x39.getTemperature() / 100.0f , aht10_0x39.getHumidity() / 100.0f ); + } + delay(10); + + history.init(status.nTemp1, status.nHumid1); + + if (!aht25.sensor() && !aht10_0x39.sensor()) { + DPRINTF("AHTx0 initialization failed. SCL:%d SDA:%d\n", PIN_SCL, PIN_SDA ); + } + + // Temp3 - NTC + ntc.setup(config.bNTCNegativePolarity); + status.nTemp3 = ntc.getTemp(); +} \ No newline at end of file diff --git a/Task0.ino b/Task0.ino new file mode 100644 index 0000000..a9f9d03 --- /dev/null +++ b/Task0.ino @@ -0,0 +1,128 @@ +#include "HermitCrab.h" +#include "Config.h" +#include "ConnectWiFi.h" +#include "UI.h" +#include +#include + +#include "TimeManager.h" +#include "WiFiHost.h" +#include "NTC_10K.h" +#include "AHT2x.h" + +#define TAG_TASK0 "Task0" + +extern bool g_bWiFiSetupExecuted; + +void setup_BLE(); + +// ================================================================================== +// +// Core0 Loop - Connection and communication +// +// ================================================================================== +MY_IRAM_ATTR void core0Task(void *pvParameters) { + ESP_LOGI(TAG_TASK0,"Core 0 Task Started"); + wl_status_t lastWiFiStatus = WL_DISCONNECTED; + unsigned long tickMillis = millis(); + unsigned long tickSecond; + unsigned long lastTick = tickMillis / 1000; + unsigned long lastTickMillis = tickMillis; + unsigned long lastSensorUpdate1 = tickMillis; + unsigned long lastSensorUpdate2 = tickMillis + 2500; + uint16_t tick1000, lastTick1000 = tickMillis % 1000;; + uint16_t tick250, lastTick250 = tickMillis % 250; + uint8_t slot; + uint8_t lastSlot = tickMillis / 50; + + esp_task_wdt_add(NULL); // NULL for the current task + ui.start(); + + ble.setupScan(); + + while (true) { + esp_task_wdt_reset(); + tickMillis = millis(); + tick250 = tickMillis % 250; + tick1000 = tickMillis % 1000; + tickSecond = tickMillis / 1000; + slot = tick1000 / 50; + + //=============================================================================== + // Loop top + // Once in a second loop + if (tick1000 != lastTick1000) { + if (slot != lastSlot) { + switch (slot) { + case 1: + case 6: + case 11: + case 16: // UI Display + ui.updateDisplayTop(tickSecond); + break; + case 2: + case 7: + case 12: + case 17: // NTC Temp Sensor + ntc.readSensor(); + break; + case 3: // NTP - Time + if (isWiFiConnected()) { + if (timeManager.getTime(tickMillis)) { + ESP_LOGI(TAG_TASK0,"NTP time loaded: %s", printTime()); + } + } + break; + case 4: + case 9: + case 14: + case 19: + ntc.readSensor(); + break; + case 5: // Heartbeat + if (isWiFiConnected()) { + host.SendHeartBeat(tickMillis); + } + break; + + case 8: // BLE + ble.loop(tickMillis); + break; + + case 13: // ATH2x - 0x38 + aht25.readSensor(tickMillis); + break; + + case 15: // ATH0x - 0x39 + aht10_0x39.readSensor(tickMillis); + break; + + case 18: // UI Bottom + ui.updateDisplayBottom(tickSecond); + break; + default: // 0 10 + break; + } + lastSlot = slot; + } + + lastTick1000 = tick1000; + lastTick250 = tick250; + } // end of - if (tick1000 != lastTick1000) + // ===================================================== + + // Unconditional Loop + host.MonitorUDP(); + + // Loop end + //========================================================================== + lastTickMillis = tickMillis; + esp_task_wdt_reset(); + } // end of - While(True) + + ESP_LOGI(TAG_TASK0,"Core 0 Task Exit"); + vTaskDelete(NULL); +} +// ================================================================================== +// End of Core0 Loop +// ================================================================================== diff --git a/TimeManager.cpp b/TimeManager.cpp new file mode 100644 index 0000000..3c187cd --- /dev/null +++ b/TimeManager.cpp @@ -0,0 +1,92 @@ +#include "HermitCrab.h" +#include "TimeManager.h" +#include "ConnectWiFi.h" +#include +#include "Config.h" +#define TAG_TIME "Time" +TimeManager timeManager; + +void TimeManager::begin() { + setenv("TZ", "UTC", 1); // Set time zone to Asia/Seoul (UTC+9) + tzset(); // Apply the timezone setting + + // Initialize UDP only if WiFi is connected + if (isWiFiConnected() && !udpInitialized) { + udpInitialized = udp.begin(localPort); + } + // Send an initial NTP request on startup + sendNTPRequest(); + lastNTPRequestMillis = millis(); +} + +void TimeManager::Stop() { + if (udpInitialized) { + udp.stop(); + } +} + +bool TimeManager::getTime(unsigned long tickMillis) { + // Check WiFi Connection + if (!isWiFiConnected()) { + //ESP_LOGI(TAG_TIME,"TimeManager - getTime called while not connected"); + if (udpInitialized) udpInitialized = false; + return false; + } + + // Check if 30 minutes have passed since last NTP request + if (udpInitialized && + ((tickMillis - lastNTPRequestMillis >= NTP_REQUEST_INTERVAL) || + !bHasNTPTime && (tickMillis - lastNTPRequestMillis >= NTP_FAILED_INTERNAL))) { + sendNTPRequest(); + lastNTPRequestMillis = tickMillis; + } + + // Check if there is an NTP response, and if so, update the system clock + return checkNTPResponse(); +} + +MY_IRAM_ATTR void TimeManager::sendNTPRequest() { + byte ntpPacket[48] = {0}; + ntpPacket[0] = 0b11100011; // LI, Version, Mode + ntpPacket[1] = 0; // Stratum, or type of clock + ntpPacket[2] = 6; // Polling Interval + ntpPacket[3] = 0xEC; // Peer Clock Precision + + udp.beginPacket(ntpServer, 123); // NTP requests are sent to port 123 + udp.write(ntpPacket, 48); + udp.endPacket(); +} + +MY_IRAM_ATTR bool TimeManager::checkNTPResponse() { + if (udp.parsePacket() == 0) { + // No response yet + return false; + } + + byte ntpBuffer[48]; + udp.read(ntpBuffer, 48); + + // Extract and set the time if response is valid + unsigned long epochTime = (unsigned long)ntpBuffer[40] << 24 | + (unsigned long)ntpBuffer[41] << 16 | + (unsigned long)ntpBuffer[42] << 8 | + (unsigned long)ntpBuffer[43]; + epochTime -= 2208988800UL; // Convert from NTP to Unix time + // epochTime += (9 * 3600); // GMT+9 + if (!bHasNTPTime) { + bHasNTPTime = true; + firstNTPTime = epochTime; + config.m_nEpochTime = epochTime; + } + setSystemClock(epochTime); + // ESP_LOGI(TAG_TIME,printTime()); + return true; +} + + +void TimeManager::setSystemClock(unsigned long epochTime) { + struct timeval now; + now.tv_sec = epochTime; + now.tv_usec = 0; + settimeofday(&now, NULL); +} diff --git a/TimeManager.h b/TimeManager.h new file mode 100644 index 0000000..8420002 --- /dev/null +++ b/TimeManager.h @@ -0,0 +1,33 @@ +#ifndef TIMEMANAGER_H +#define TIMEMANAGER_H + +#include + +class TimeManager { +public: + void begin(); + void Stop(); + bool getTime(unsigned long tick); // Called every second to handle requests and updates + inline bool hasNTPUpdate() { return bHasNTPTime; }; + inline unsigned long getFirstNTPTime() { return firstNTPTime; } + bool checkNTPResponse(); + void sendNTPRequest(); + +private: + WiFiUDP udp; + const char* ntpServer = "pool.ntp.org"; + const unsigned int localPort = 2390; + bool udpInitialized = false; + bool bHasNTPTime = false; + unsigned long firstNTPTime = 0; + + + unsigned long lastNTPRequestMillis = 0; + const unsigned long NTP_REQUEST_INTERVAL = 30 * 60 * 1000; // 30 minutes + const unsigned long NTP_FAILED_INTERNAL = 1 * 60 * 1000; // 1 minute + + void setSystemClock(unsigned long epochTime); +}; + +extern TimeManager timeManager; +#endif // TIMEMANAGER_H \ No newline at end of file diff --git a/UI.cpp b/UI.cpp new file mode 100644 index 0000000..9b790d3 --- /dev/null +++ b/UI.cpp @@ -0,0 +1,1152 @@ + +#include "HermitCrab.h" +#include "Config.h" +#include "ConnectWiFi.h" +#include "TimeManager.h" +#include "WiFiHost.h" + +#include "UI.h" +#include // 35 +#include // 25 +#include +#include + +#define TAG_UI "UI" +// Buttons +#define DEBOUNCE_DELAY 100 +#define LONG_PRESS_DURATION 1500 + +// Display +#define SPACING 2 +#define FONT_DESCENT 3 +#define POS_X_UNIT 112 +#define WIDTH_UNIT 16 +#define HEIGHT_UNIT 16 + +#define POS_Y_BOTTOM (SCREEN_HEIGHT - 1) +#define WIDTH_D3 20 +#define POS_X_D3 (POS_X_UNIT - WIDTH_D3 - SPACING) +#define HEIGHT_D3 26 + +#define WIDTH_DOT 7 +#define POS_X_DOT (POS_X_D3 - WIDTH_DOT - SPACING) +#define HEIGHT_DOT 7 + +#define WIDTH_D2 26 +#define POS_X_D2 (POS_X_DOT - WIDTH_D2 - SPACING) +#define HEIGHT_D2 36 + +#define WIDTH_D1 WIDTH_D2 +#define POS_X_D1 (POS_X_D2 - WIDTH_D1 - SPACING) +#define HEIGHT_D1 HEIGHT_D2 + +#define WIDTH_D0 WIDTH_D1 +#define POS_X_D0 (POS_X_D1 - WIDTH_D1 - SPACING) +#define HEIGHT_D0 HEIGHT_D1 + +enum MAIN_MODE { + MODE_TEMP, + MODE_HUMID, + MODE_CLOCK +}; + +//Adafruit_SSD1306 ssd1306(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); +CUI ui; + +const uint8_t logo[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3F, 0x3F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x26, 0x17, 0xC0, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x88, 0x20, 0x30, 0x00, 0x00, 0x80, 0x04, 0x02, 0x02, 0xC1, 0xFC, 0x00, 0x00, 0x80, + 0x04, 0x06, 0x5E, 0x03, 0xC3, 0x00, 0x00, 0x80, 0x04, 0x04, 0xF8, 0x03, 0x00, 0xC0, 0x00, 0x80, + 0x04, 0x00, 0xF0, 0x07, 0x00, 0x30, 0x00, 0x80, 0x08, 0x11, 0x18, 0x0D, 0x00, 0xC8, 0x03, 0x00, + 0x08, 0x12, 0x20, 0x0C, 0x81, 0xFC, 0x05, 0x00, 0x10, 0x26, 0x50, 0x0F, 0x81, 0xFE, 0x0A, 0x00, + 0x08, 0x64, 0xF0, 0x1D, 0x86, 0x67, 0x14, 0x00, 0x08, 0x41, 0x80, 0x18, 0xC7, 0x31, 0xF8, 0x00, + 0x08, 0xCB, 0x80, 0x3B, 0xC6, 0x1B, 0x20, 0x00, 0x08, 0xCB, 0x80, 0x7C, 0x66, 0x1F, 0xE0, 0x00, + 0x09, 0xDF, 0xB0, 0x10, 0x60, 0x39, 0xE0, 0x00, 0x09, 0x87, 0xE0, 0x38, 0xF8, 0x79, 0x90, 0x00, + 0x0B, 0xAE, 0x00, 0x7C, 0x7C, 0xF1, 0xFF, 0x00, 0x13, 0x6E, 0x00, 0x3C, 0x60, 0x98, 0xFD, 0x80, + 0x13, 0x08, 0x40, 0x7C, 0x01, 0x9E, 0xFD, 0xC0, 0x17, 0x1C, 0x00, 0xFC, 0x03, 0x8D, 0xFB, 0xC0, + 0x16, 0x5C, 0x02, 0x3C, 0x0E, 0xFF, 0xFB, 0xE0, 0x0E, 0x5C, 0x04, 0x78, 0x1F, 0x9F, 0x99, 0xF0, + 0x0C, 0x9C, 0x60, 0x78, 0x77, 0xFF, 0xFF, 0xF8, 0x04, 0x9F, 0xC0, 0xB3, 0xFF, 0xF6, 0x9F, 0x7C, + 0x06, 0x9F, 0x01, 0x1F, 0x7E, 0xFF, 0xFF, 0xBC, 0x06, 0x1C, 0x00, 0x7F, 0xFB, 0x16, 0xEF, 0xFC, + 0x02, 0x48, 0x01, 0xF7, 0xFF, 0xC7, 0xCF, 0xFE, 0x01, 0x08, 0x03, 0xBE, 0xF7, 0xF4, 0x0F, 0xFA, + 0x00, 0x90, 0x0F, 0xFA, 0xFF, 0xB4, 0x07, 0x7E, 0x00, 0x40, 0x1D, 0xFF, 0x77, 0x42, 0x07, 0xBA, + 0x00, 0x40, 0x2F, 0xF7, 0x7B, 0x41, 0x06, 0x5B, 0x00, 0x41, 0xFF, 0x6E, 0xDC, 0xC3, 0x07, 0x7F, + 0x00, 0x27, 0x7D, 0x3C, 0xDF, 0xCB, 0x87, 0x5B, 0x00, 0x3F, 0xFF, 0xF6, 0xCE, 0x59, 0xC6, 0xF9, + 0x00, 0x1E, 0xF7, 0x7A, 0xC7, 0xFB, 0xC7, 0x6D, 0x00, 0x0B, 0xFE, 0xDF, 0xE8, 0x9B, 0x86, 0x7D, + 0x00, 0x0B, 0xC5, 0x8F, 0xF0, 0xC3, 0x80, 0x3F, 0x00, 0x17, 0x85, 0x85, 0xE8, 0xFB, 0xC0, 0x37, + 0x00, 0x3E, 0x0B, 0x05, 0x68, 0xBE, 0xE0, 0x37, 0x00, 0x7C, 0x13, 0x05, 0xF8, 0xBF, 0xF0, 0x16, + 0x00, 0xB8, 0x26, 0x09, 0x70, 0xFD, 0xF0, 0x1E, 0x01, 0x70, 0x24, 0x0A, 0x70, 0xFF, 0xF8, 0x1C, + 0x03, 0xC0, 0x58, 0x0A, 0x50, 0xDF, 0xB8, 0x1C, 0x07, 0x80, 0x98, 0x0E, 0x50, 0xFB, 0xF8, 0x1C, + 0x1F, 0x00, 0xB0, 0x16, 0x70, 0x7D, 0xF0, 0x18, 0x3C, 0x01, 0x60, 0x14, 0x60, 0x3D, 0xF0, 0x00, + 0x78, 0x01, 0xE0, 0x14, 0x60, 0x1E, 0xF0, 0x00, 0xF0, 0x03, 0x80, 0x18, 0xE0, 0x07, 0xE0, 0x00, + 0xC0, 0x07, 0x00, 0x38, 0xE0, 0x03, 0x80, 0x00, 0x00, 0x0E, 0x00, 0x31, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x1E, 0x00, 0x71, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +const uint8_t degreeC[] = { + 0x60, 0x00, + 0x90, 0x00, + 0x90, 0x00, + 0x63, 0xE4, + 0x0E, 0x1C, + 0x1C, 0x04, + 0x18, 0x00, + 0x38, 0x00, + 0x38, 0x00, + 0x38, 0x00, + 0x38, 0x00, + 0x38, 0x00, + 0x1C, 0x04, + 0x0E, 0x08, + 0x07, 0xF0, + 0x00, 0x00 +}; + +const uint8_t percentSign[] = { + 0x00, 0x00, + 0x38, 0x18, + 0x7C, 0x30, + 0xC6, 0x30, + 0xC6, 0x60, + 0xC6, 0xc0, + 0x7C, 0xC0, + 0x39, 0x80, + 0x01, 0x9C, + 0x03, 0x3E, + 0x03, 0x63, + 0x06, 0x63, + 0x0C, 0x63, + 0x0C, 0x3E, + 0x18, 0x1C, + 0x00, 0x00 +}; + + +enum UI_ITEM { + ITEM_CLOCK, + + // Sensor + ITEM_TEMP1, + ITEM_TEMP2, + ITEM_TEMP3, + ITEM_HUMID, + ITEM_HUMID2, + + // SV + ITEM_TEMP_TARGET, + ITEM_HUMID_TARGET, + + // CV + ITEM_HEAT1, + ITEM_HEAT2, + ITEM_MIST, + ITEM_FAN, + ITEM_MOTOR, + ITEM_LIGHT, + + ITEM_HEAT1_MANUAL, + ITEM_HEAT2_MANUAL, + ITEM_MIST_MANUAL, + ITEM_FAN_MANUAL, + ITEM_MOTOR_MANUAL, + ITEM_LIGHT_MANUAL, + ITEM_CHECK_AC, + + ITEM_COUNT +}; + +char *title[] = { + "Time:", + + "Temp1", "Temp2", "Temp3", + + "RH1", "RH2", + + "TT", "RHT", + + "Heat1", "Heat2", "Mist", "Fan", "Pump", "Lum", + + "Ht1 M", "Ht2 M", "Mst M", "Fan M", "Pmp M", "Lum M", + + "Chk AC" }; + +uint8_t *unit[] = { (uint8_t *) degreeC, (uint8_t *) percentSign, nullptr }; +const uint16_t main_unit_idx[] = { 0, 1, 2 }; +const uint16_t item_unit_idx[] = { + 2, // Time + 0, 0, 0, // Temp + 1, 1, // Humid + 0, 1, // SV + 1, 1, 1, 1, 1, 1, // CV + 2, 2, 2, 2, 2, 2, // Manual + 2 }; + +const bool set_idx[] = { + false, + false, false, false, + false, false, + + true, true, + + true, true, true, true, true, true, + true, true, true, true, true, true, + + true }; + +const bool fineControl[] = { + false, + false, false, false, + false, false, + + true, true, + + false, false, false, false, false, false, + true, true, true, true, true, true, + + true }; + +// +// Buttons +// +void buttonSetISR(); +void buttonUpISR(); +void buttonDownISR(); + + +// Constructor for CUI class +//CUI::CUI(Adafruit_SSD1306 &display) +CUI::CUI() + : SSD1306() +{} + +void CUI::setup() { + // Initialize the display + ESP_LOGI(TAG_UI," UI - setup()"); + bOK = false; + bDot = false; + bButtonChanged = false; + m_nMessageMode = 0; + m_nMainMode = 0; + m_nItemMode = 0; + m_nItem = 0; + m_nValue = 0; + m_pUnit = (uint8_t *) unit[item_unit_idx[m_nItem]]; + m_nD0 = m_nD1 = m_nD2 = m_nD3 = 0; + m_pDUnit = nullptr; + + lastMessageMode = 1; + lastMainMode = 1; + lastItemMode = 1; + lastItem = 999; + lastValue = 999; + lastUnit = nullptr; + + lastD0 = 999; + lastD1 = 999; + lastD2 = 999; + lastD3 = 999; + lastpDUnit = nullptr; + + // Check if device exists + for (int i = 0; i < 5; i++) { + Wire.beginTransmission(i2caddr); + if (Wire.endTransmission() == 0) { + bOK = true; + ESP_LOGI(TAG_UI," UI - device Found at 0x%02X\n", i2caddr); + break; + } + delay(50); + } + + if (!bOK) { + ESP_LOGI(TAG_UI," UI - device NOT found at 0x%02X\n", i2caddr); + return; + } + + // Init Display hardware + begin(SSD1306_SWITCHCAPVCC, i2caddr); + //setContrast(contrast / 2); + setTextColor(SSD1306_WHITE); + updateScreen(); + + // Display Logo + int x = (width() - 64); + int y = (height() - 64) / 2; + drawBitmap(x, y, logo, 64, 64, 1); + updateScreen(); + ssd1306_hscroll(true, 0, 7, x, 32); + + // Box + boxMode = { 0, HEIGHT_UNIT, 24, 8 }; + boxTitle = { 0, 0, 64, 16 }; + boxValue = { 0, 0, 0, 16 }; + boxUnit = { POS_X_UNIT, 0, WIDTH_UNIT, HEIGHT_UNIT }; + + boxDUnit = {POS_X_UNIT, POS_Y_BOTTOM - HEIGHT_UNIT, WIDTH_UNIT, HEIGHT_UNIT}; // 16 + boxD3 = {POS_X_D3, POS_Y_BOTTOM - HEIGHT_D3, WIDTH_D3, HEIGHT_D3}; // 17 + 2 + boxDot = {POS_X_DOT, POS_Y_BOTTOM - HEIGHT_DOT, WIDTH_DOT, HEIGHT_DOT}; // 7 + 2 + boxD2 = {POS_X_D2, POS_Y_BOTTOM - HEIGHT_D2, WIDTH_D2, HEIGHT_D2}; // 17 + 2 + boxD1 = {POS_X_D1, POS_Y_BOTTOM - HEIGHT_D1, WIDTH_D1, HEIGHT_D1}; // 17 + 2 + boxD0 = {POS_X_D0, POS_Y_BOTTOM - HEIGHT_D0, WIDTH_D0, HEIGHT_D0}; // 17 + 2 + + + // + // Buttons + // + pinMode(PIN_SW_SET, INPUT_PULLUP); + pinMode(PIN_SW_UP, INPUT_PULLUP); + pinMode(PIN_SW_DOWN, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(PIN_SW_SET), buttonSetISR, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_SW_UP), buttonUpISR, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_SW_DOWN), buttonDownISR, CHANGE); + initButtonState(); +} + +void CUI::start() { + vTaskDelay(1500/portTICK_PERIOD_MS); + clearDisplayBuffer(); + updateScreen(); + initButtonState(); + //getBoundaries(); +} + +void CUI::getBoundaries() { + // Display Boundary + { + int16_t x, y; + uint16_t w, h; + char sz[2]; + sz[1] = 0; + int nMaxW24 = 0; + int nMaxH24 = 0; + int nMaxW18 = 0; + int nMaxH18 = 0; + int nMaxW9 = 0; + int nMaxH9 = 0; + int nMaxW24A = 0; + int nMaxH24A = 0; + int nMaxW18A = 0; + int nMaxH18A = 0; + int nMaxW9A = 0; + int nMaxH9A = 0; + + for (int i = 0; i < 10; i++) { + sz[0] = 0x30 + i; + setFont(&FreeSansBold24pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound24 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW24) nMaxW24 = w; + if (h > nMaxH24) nMaxH24 = h; + + setFont(&FreeSans18pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW18) nMaxW18 = w; + if (h > nMaxH18) nMaxH18 = h; + + setFont(&FreeSans9pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW9) nMaxW9 = w; + if (h > nMaxH9) nMaxH9 = h; + } + + for (int i = 0; i < 26; i++) { + sz[0] = 'A' + i; + setFont(&FreeSansBold24pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound24 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW24A) nMaxW24A = w; + if (h > nMaxH24A) nMaxH24A = h; + + setFont(&FreeSans18pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW18A) nMaxW18A = w; + if (h > nMaxH18A) nMaxH18A = h; + + setFont(&FreeSans9pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW9A) nMaxW9A = w; + if (h > nMaxH9A) nMaxH9A = h; + } + + { + sz[0] = ':'; + setFont(&FreeSansBold24pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound24 - '%c' w(%d) h(%d)\n", sz[0], w, h); + + setFont(&FreeSans18pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + + setFont(&FreeSans9pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + } + + { + sz[0] = '.'; + setFont(&FreeSansBold24pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound24 - '%c' w(%d) h(%d)\n", sz[0], w, h); + + setFont(&FreeSans18pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + + setFont(&FreeSans9pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + } + + ESP_LOGI(TAG_UI,"Font24 Max w(%d) h(%d)\n", nMaxW24, nMaxH24); + ESP_LOGI(TAG_UI,"Font24A Max w(%d) h(%d)\n", nMaxW24A, nMaxH24A); + + ESP_LOGI(TAG_UI,"Font18 Max w(%d) h(%d)\n", nMaxW18, nMaxH18); + ESP_LOGI(TAG_UI,"Font18A Max w(%d) h(%d)\n", nMaxW18A, nMaxH18A); + + ESP_LOGI(TAG_UI,"Font_9 Max w(%d) h(%d)\n", nMaxW9, nMaxH9); + ESP_LOGI(TAG_UI,"Font_9A Max w(%d) h(%d)\n", nMaxW9A, nMaxH9A); + } +} + +MY_IRAM_ATTR void CUI::message(uint8_t lineNo, char *sz) { + int16_t x, y; + uint16_t w, h; + + if (!bOK) return; + + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + if (w > 128) w = 128; + fillRect(0,lineNo * 8,w,8, SSD1306_BLACK); + setFont(NULL); + setTextSize(1); + setCursor(0, lineNo * 8); + print(sz); + updateRegion(lineNo,lineNo,0,w); +} + +// Main function to monitor and update display +MY_IRAM_ATTR void CUI::updateDisplay(unsigned long tickSecond) { + if (!bOK) return; + static unsigned long lastUpdate = 0; + static unsigned long lastMainModeChange = 0; + + bool bUpdateTop = false; + bool bUpdateBottom = false; + + bUpdateTop = displayTop(); + if (tickSecond != lastUpdate) { + bUpdateBottom = displayBottom(); + lastUpdate = tickSecond; + } + + if (bUpdateTop && bUpdateBottom) { + updateScreen(); + } else if (bUpdateTop) { + updateRegion(0, 1, 0, SCREEN_WIDTH - 1); + } else if (bUpdateBottom) { + updateRegion(3, 7, 0, SCREEN_WIDTH - 1); + } + + if (tickSecond - lastMainModeChange >= 20) { + lastMainMode = m_nMainMode; + m_nMainMode = (m_nMainMode + 1) % 2; + lastMainModeChange = tickSecond; + } +} + +MY_IRAM_ATTR void CUI::updateDisplayTop(unsigned long tick) { + if (!bOK) return; + + if (displayTop()) { + updateRegion(0, 1, 0, SCREEN_WIDTH - 1); + } +} + +MY_IRAM_ATTR void CUI::updateDisplayBottom(unsigned long tickSecond) { + if (!bOK) return; + + static unsigned long lastMainModeChange = 0; + + if (displayBottom()) { + updateRegion(3, 7, 0, SCREEN_WIDTH - 1); + } + + if (tickSecond - lastMainModeChange >= 20) { + lastMainMode = m_nMainMode; + m_nMainMode = (m_nMainMode + 1) % 2; + lastMainModeChange = tickSecond; + } +} + +MY_IRAM_ATTR bool CUI::displayTop() { + static uint8_t lastHour = 255, lastMin = 255, lastSec = 255; + bool bUpdateTop = false; + char sz[32]; + int16_t x, y; + uint16_t w, h; + int16_t firstC = SCREEN_WIDTH - 1; + int16_t lastC = 0; + int16_t endX = SCREEN_WIDTH - WIDTH_UNIT - SPACING * 2; + + // Message Mode + if (!bButtonChanged) { + if (config.bCheckAC && !isWiFiConnected()) m_nMessageMode = MESSAGE_MODE::MODE_NO_WIFI; + else if (config.bCheckAC && (status.nFlags & FLAG_ZCD_AC)) m_nMessageMode = MESSAGE_MODE::MODE_AC; + else if (config.bCheckAC && (status.nFlags & FLAG_ZCD_LOAD)) m_nMessageMode = MESSAGE_MODE::MODE_LOAD; + else m_nMessageMode = MESSAGE_MODE::MODE_NONE; + + if (m_nMessageMode != lastMessageMode) { + char sz[32]; + switch(m_nMessageMode) { + case MESSAGE_MODE::MODE_NO_WIFI: + strcpy(sz, "No WiFi! "); + break; + case MESSAGE_MODE::MODE_AC: + strcpy(sz, "Check AC! "); + break; + case MESSAGE_MODE::MODE_LOAD: + strcpy(sz, "Check Load! "); + break; + } + + if (m_nMessageMode != MESSAGE_MODE::MODE_NONE) { + fillRect(0, 0, SCREEN_WIDTH, HEIGHT_UNIT, SSD1306_BLACK); + setFont(&FreeSans9pt7b); + setCursor(boxTitle.x, boxTitle.y + 15 - FONT_DESCENT); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + print(sz); + updateRegion(0, 1, 0, w - 1); + lastItemMode = 999; + lastMessageMode = m_nMessageMode; + return false; + } + } else { + if (m_nMessageMode != MESSAGE_MODE::MODE_NONE) { + //ESP_LOGI(TAG_UI,"UI - Message NOT CHANGED and MODE is NOT NORMAL"); + return false; + } + } + } + lastMessageMode = MESSAGE_MODE::MODE_NONE; + + // ItemMode + if (m_nItemMode != lastItemMode) { + setFont(NULL); + setTextSize(1); + boxMode.w = 24; + fillRect(boxMode.x, boxMode.y, boxMode.w, boxMode.h, SSD1306_BLACK); + switch(m_nItemMode) { + case ITEM_MODE::MODE_SET: + setCursor(boxMode.x, boxMode.y); + print("SET"); + //ESP_LOGI(TAG_UI,"UI - Item Mode Change - SET"); + break; + case ITEM_MODE::MODE_CONFIG: + setCursor(boxMode.x, boxMode.y); + print("CFG"); + //ESP_LOGI(TAG_UI,"UI - Item Mode Change - CONFIG"); + break; + case ITEM_MODE::MODE_NORMAL: + //ESP_LOGI(TAG_UI,"UI - Item Mode Change - NORMAL"); + break; + } + updateRegion(2,2,0,boxMode.w); + lastItemMode = m_nItemMode; + lastItem = 999; + lastValue = m_nValue + 1111; + } + + // Top + setFont(&FreeSans9pt7b); + + if (m_nItem != lastItem) { + + // Title + fillRect(0, 0, SCREEN_WIDTH, HEIGHT_UNIT, SSD1306_BLACK); + setCursor(boxTitle.x, boxTitle.y + 15 - FONT_DESCENT); + getTextBounds((const char *)title[m_nItem], 0, 0, &x, &y, &w, &h); + boxTitle.w = w + 2; + print(title[m_nItem]); + + // Value + if (m_nItem == UI_ITEM::ITEM_CLOCK) { + sprintf(sz, "%02d:%02d.%02d", g_nHour, g_nMinute, g_nSecond); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + boxValue.x = boxTitle.w + 8; + boxValue.w = SCREEN_WIDTH - boxValue.x; + lastHour = g_nHour; + lastMin = g_nMinute; + lastSec = g_nSecond; + } else { + if (m_nItem < ITEM_HEAT1_MANUAL) { + sprintf(sz, "%.1f", m_nValue / 10.0f); + } + else { + if (m_nItem < ITEM_CHECK_AC) + sprintf(sz, "%s", m_nValue ? "Manual" : "Auto"); + else + sprintf(sz, "%s", m_nValue ? "Yes" : "No"); + endX = SCREEN_WIDTH - 1; + } + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + int16_t newX = endX - w - SPACING; + uint16_t newW = endX - newX; + boxValue.x = newX; + boxValue.w = newW; + lastValue = m_nValue; + } + setCursor(boxValue.x, boxValue.y + 15 - FONT_DESCENT); + print(sz); + + // Item Unit + if (m_nItem != UI_ITEM::ITEM_CLOCK && m_nItem < UI_ITEM::ITEM_HEAT1_MANUAL) { + m_pUnit = (uint8_t *) unit[item_unit_idx[m_nItem]]; + drawBitmap(boxUnit.x, boxUnit.y, m_pUnit, boxUnit.w, boxUnit.h, SSD1306_WHITE); + lastUnit = m_pUnit; + } else { + m_pUnit = nullptr; + } + + lastItem = m_nItem; + bUpdateTop = true; + firstC = 0; + lastC = 127; + return true; + } + + // value + if (m_nItem == UI_ITEM::ITEM_CLOCK) { + if (lastHour != g_nHour || lastMin != g_nMinute || lastSec != g_nSecond) { + lastHour = g_nHour; + lastMin = g_nMinute; + lastSec = g_nSecond; + fillRect(boxValue.x, boxValue.y, boxValue.w, boxValue.h, SSD1306_BLACK); + sprintf(sz, "%02d:%02d.%02d", g_nHour, g_nMinute, g_nSecond); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + boxValue.x = boxTitle.w + 8; + boxValue.w = SCREEN_WIDTH - boxValue.x; + + setCursor(boxValue.x, boxValue.y + 15 - FONT_DESCENT); + print(sz); + + bUpdateTop = true; + firstC = boxValue.x; + lastC = 127; + } + } else if (m_nValue != lastValue) { + fillRect(boxValue.x, boxValue.y, boxValue.w + SPACING, boxValue.h, SSD1306_BLACK); + if (m_nItem < ITEM_HEAT1_MANUAL) { + sprintf(sz, "%.1f", m_nValue / 10.0f); + } + else { + if (m_nItem < ITEM_CHECK_AC) + sprintf(sz, "%s", m_nValue ? "Manual" : "Auto"); + else + sprintf(sz, "%s", m_nValue ? "Yes" : "No"); + endX = SCREEN_WIDTH - SPACING - 1; + } + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + int16_t newX = endX - w - SPACING; + uint16_t newW = endX - newX; + + setCursor(newX, boxValue.y + 15 - FONT_DESCENT); + print(sz); + + bUpdateTop = true; + firstC = boxValue.x < newX ? boxValue.x : newX; + lastC = endX + SPACING; + boxValue.x = newX; + boxValue.w = newW; + lastValue = m_nValue; + } + + + + if (bUpdateTop && lastC - firstC > 0) { + //unsigned long m = millis(); + updateRegion(0, 1, firstC, lastC); + //m = millis() - m; + //ESP_LOGI(TAG_UI,"UI - Update Top(%d~%d): %d msec\n", firstC, lastC, m); + } + return false; +} + +MY_IRAM_ATTR bool CUI::displayBottom() { + bool bUnitChange = false; + bool bD3Change = false; + bool bD2Change = false; + bool bD1Change = false; + bool bD0Change = false; + int firstC = SCREEN_WIDTH - 1; + int lastC = 0; + char sz[32]; + static uint8_t bLastNTP = 0xFF; + static uint8_t bLastBLE = ~FLAG_BLE_NODATA; + static uint8_t bLastConn = 0xFF; + static uint8_t bLastWiFi = 0xFF; + + // Bottom + // Update temperature and humidity if they have changed + int16_t value; + switch (m_nMainMode) { + case MAIN_MODE::MODE_TEMP: + value = status.nTemp1; + if (value < 0) value = 0; + m_pDUnit = (uint8_t *) °reeC[0]; + break; + case MAIN_MODE::MODE_HUMID: + value = status.nHumid1; + if (value < 0) value = 0; + m_pDUnit = (uint8_t *) &percentSign[0]; + break; + default: + value = 0; + break; + } + + // Unit + if (m_pDUnit != lastpDUnit) { + fillRect(boxDUnit.x, boxDUnit.y, boxDUnit.w, boxDUnit.h, SSD1306_BLACK); + drawBitmap( boxDUnit.x, boxDUnit.y, + m_pDUnit, + boxDUnit.w, boxDUnit.h, SSD1306_WHITE); + lastpDUnit = m_pDUnit; + bUnitChange = true; + firstC = boxDUnit.x; + lastC = boxDUnit.x + boxDUnit.w - 1; + } + + // Value + m_nD3 = value % 10; + value /= 10; + m_nD2 = value % 10; + value /= 10; + m_nD1 = value % 10; + m_nD0 = value / 10; + + // Update Tenths digit + if (m_nD3 != lastD3) { + fillRect(boxD3.x, boxD3.y, boxD3.w, boxD3.h, SSD1306_BLACK); + setFont(&FreeSans18pt7b); // 25 pixels high + sprintf(sz, "%d", m_nD3); + setCursor(boxD3.x, SCREEN_HEIGHT - FONT_DESCENT); + print(sz); + lastD3 = m_nD3; + bD3Change = true; + firstC = boxD3.x; + int end = boxD3.x + boxD3.w - 1; + if (lastC < end) + lastC = end; + } + + setFont(&FreeSansBold24pt7b); // 35 pixels high + // Dot + if (!bDot) + { + setCursor(boxDot.x, SCREEN_HEIGHT - FONT_DESCENT); + print("."); + bDot = true; + firstC = boxDot.x; + int end = boxDot.x + boxDot.w - 1; + if (lastC < end) + lastC = end; + } + + // Update One's digit + if (m_nD2 != lastD2) { + fillRect(boxD2.x, boxD2.y, boxD2.w, boxD2.h, SSD1306_BLACK); + sprintf(sz, "%d", m_nD2); + setCursor(boxD2.x, SCREEN_HEIGHT - FONT_DESCENT); + print(sz); + lastD2 = m_nD2; + bD2Change = true; + firstC = boxD2.x; + int end = boxD2.x + boxD2.w - 1; + if (lastC < end) + lastC = end; + } + + // Update Ten's digit + if (m_nD1 != lastD1) { + fillRect(boxD1.x, boxD1.y, boxD1.w, boxD1.h, SSD1306_BLACK); + if (m_nD0 > 0 || m_nD1 > 0) { + sprintf(sz, "%d", m_nD1); + setCursor(boxD1.x, SCREEN_HEIGHT - FONT_DESCENT); + print(sz); + } + lastD1 = m_nD1; + bD1Change = true; + firstC = boxD1.x; + int end = boxD1.x + boxD1.w - 1; + if (lastC < end) + lastC = end; + } + + // Update Hundred's digit + if (m_nD0 != lastD0) { + fillRect(boxD0.x, boxD0.y, boxD0.w, boxD0.h, SSD1306_BLACK); + if (m_nD0 > 0) { + sprintf(sz, "%d", m_nD0); + setCursor(boxD0.x, SCREEN_HEIGHT - FONT_DESCENT); + print(sz); + } + lastD0 = m_nD0; + bD0Change = true; + firstC = boxD0.x; + int end = boxD0.x + boxD0.w - 1; + if (lastC < end) + lastC = end; + } + + uint8_t sp = (bD0Change || bD1Change || bD2Change) ? 3 : 4; + int updateWidth = lastC - firstC; + if (updateWidth > 96) { + return true; + } + else + if (updateWidth > 0) { + //unsigned long m = millis(); + updateRegion(sp, 7, firstC, lastC); + //m = millis() - m; + //ESP_LOGI(TAG_UI,"UI - Update Bottom(%d~%d): %d msec\n", firstC, lastC, m); + } + + // Bottom Left + { + bool bUpdate = false; + setFont(NULL); + setTextSize(1); + + // WiFi Status + uint8_t bWiFi = isWiFiConnected(); + if (bWiFi != bLastWiFi) { + if (!isWiFiConnected()) { + setCursor(0, 7 * 8); + print('W'); + } else { + fillRect(0, 7 * 8, 8, 8, SSD1306_BLACK); + } + bLastWiFi = bWiFi; + bUpdate = true; + } + + // Time Manager + uint8_t bNTP = timeManager.hasNTPUpdate() ? 1 : 0; + if (bNTP != bLastNTP) { + if (!timeManager.hasNTPUpdate()) { + setCursor(0, 6 * 8); + print('T'); + } + else { + fillRect(0, 6 * 8, 8, 8, SSD1306_BLACK); + } + bLastNTP = bNTP; + bUpdate = true; + } + + // Client Connection + uint8_t bConn = host.isConnected(); + if (bConn != bLastConn) { + if (bConn) { + setCursor(0, 7 * 8); + print('C'); + } + else { + fillRect(0, 7 * 8, 8, 8, SSD1306_BLACK); + } + bLastConn = bConn; + bUpdate = true; + } + + if (bUpdate) { + updateRegion(6,7,0,7); + lastMainMode = m_nMainMode; + } + } + return false; +} + +// +// Buttons +// +MY_IRAM_ATTR void CUI::loopButton(unsigned long tickMillis) { + //if (host.isConnected()) { + // initButtonState(); + // return; + //} + static unsigned long lastButtonAction = 0; + + checkButtonStates(tickMillis); + if (bButtonSetDown || bButtonSetUp || bButtonUpDown || bButtonUpUp || bButtonDownDown || bButtonDownUp) { + bButtonChanged = true; + lastButtonAction = tickMillis; + } else + if (tickMillis - lastButtonAction > 20000) { + bButtonChanged = false; + } + + if (bButtonSetUp) { + switch(m_nItemMode) { + case ITEM_MODE::MODE_NORMAL: + if (set_idx[m_nItem]) + m_nItemMode = buttonSetDownDuration > 1000 ? MODE_CONFIG : MODE_SET; + break; + case ITEM_MODE::MODE_SET: + if (set_idx[m_nItem]) { + switch(m_nItem) { + case ITEM_TEMP_TARGET: config.nTempTarget = m_nValue; break; + case ITEM_HUMID_TARGET: config.nHumidTarget = m_nValue; break; + + case ITEM_HEAT1: status.nHeater1Duty = m_nValue * 10; break; + case ITEM_HEAT2: status.nHeater2Duty = m_nValue * 10; break; + case ITEM_MIST: + status.nMistDuty = m_nValue * 10; + break; + case ITEM_FAN: + status.nFanDuty = m_nValue; + break; + case ITEM_MOTOR: status.nMotorDuty = m_nValue; + break; + case ITEM_LIGHT: + status.nLightTargetDuty = m_nValue; + break; + + case ITEM_HEAT1_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_HEATER1 : status.nFlags & ~FLAG_MANUAL_HEATER1; + break; + case ITEM_HEAT2_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_HEATER2 : status.nFlags & ~FLAG_MANUAL_HEATER2; + break; + case ITEM_MIST_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_MIST : status.nFlags & ~FLAG_MANUAL_MIST; + break; + case ITEM_FAN_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_FAN : status.nFlags & ~FLAG_MANUAL_FAN; + break; + case ITEM_MOTOR_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_MOTOR : status.nFlags & ~FLAG_MANUAL_MOTOR; + break; + case ITEM_LIGHT_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_LIGHT : status.nFlags & ~FLAG_MANUAL_LIGHT; + break; + case ITEM_CHECK_AC: + config.bCheckAC = m_nValue ? true : false; + } + m_nItemMode = MODE_NORMAL; + } + break; + case ITEM_MODE::MODE_CONFIG: + m_nItemMode = MODE_NORMAL; + break; + } + } + else { + switch (m_nItemMode) { + case MODE_NORMAL: + if (bButtonUpUp) { + m_nItem = m_nItem > 0 ? --m_nItem : UI_ITEM::ITEM_COUNT - 1; + lastValue = m_nValue + 1; + } + if (bButtonDownUp) { + m_nItem = m_nItem < ITEM_COUNT - 1 ? ++m_nItem : 0; + lastValue = m_nValue + 1; + } + break; + case MODE_SET: + if (bButtonUpUp) { + if (fineControl[m_nItem]) { + if (m_nItem >= ITEM_HEAT1_MANUAL) { + m_nValue = m_nValue ? 0 : 1; + } else if (m_nValue < 1000) { + m_nValue++; + } + } + else { + m_nValue = (m_nValue / 10 + 1) * 10; + if (m_nValue > 1000) m_nValue = 1000; + } + } + if (bButtonDownUp) { + if (fineControl[m_nItem]) { + if (m_nItem >= ITEM_HEAT1_MANUAL) { + m_nValue = m_nValue ? 0 : 1; + } else if (m_nValue > 0) { + m_nValue--; + } + } + else { + m_nValue = (m_nValue / 10 - 1) * 10; + if (m_nValue < 0) m_nValue = 0; + } + } + break; + } + } + + if (m_nItemMode == ITEM_MODE::MODE_NORMAL || !set_idx[m_nItem]) { + switch(m_nItem) { + case ITEM_TEMP1: m_nValue = status.nTemp1; break; + case ITEM_TEMP2: m_nValue = status.nTemp2; break; + case ITEM_TEMP3: m_nValue = status.nTemp3; break; + case ITEM_HUMID: m_nValue = status.nHumid1; break; + case ITEM_HUMID2:m_nValue = status.nHumid2; break; + + case ITEM_TEMP_TARGET: m_nValue = config.nTempTarget; break; + case ITEM_HUMID_TARGET: m_nValue = config.nHumidTarget; break; + + case ITEM_HEAT1: m_nValue = status.nHeater1Duty / 10; break; + case ITEM_HEAT2: m_nValue = status.nHeater2Duty / 10; break; + case ITEM_MIST: m_nValue = status.nMistDuty / 10; break; + case ITEM_FAN: m_nValue = status.nFanDuty; break; + case ITEM_MOTOR: m_nValue = status.nMotorDuty; break; + case ITEM_LIGHT: m_nValue = status.nLightTargetDuty; break; + + case ITEM_HEAT1_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_HEATER1 ? 1 : 0; break; + case ITEM_HEAT2_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_HEATER2 ? 1 : 0; break; + case ITEM_MIST_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_MIST ? 1 : 0; break; + case ITEM_FAN_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_FAN ? 1 : 0; break; + case ITEM_MOTOR_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_MOTOR ? 1 : 0; break; + case ITEM_LIGHT_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_LIGHT ? 1 : 0; break; + + case ITEM_CHECK_AC: m_nValue = config.bCheckAC ? 1 : 0; break; + } + } + + bButtonSetUp = false; + bButtonUpUp = false; + bButtonDownUp = false; +} + +void CUI::initButtonState() { + buttonSetChangeTime = 0; // Time when button was pressed + buttonUpChangeTime = 0; // Time when button was pressed + buttonDownChangeTime = 0; // Time when button was pressed + + buttonSetDownTime = 0; // Time when button was pressed + buttonUpDownTime = 0; // Time when button was pressed + buttonDownDownTime = 0; // Time when button was pressed + + buttonSetDownDuration = 0; // Time when button was pressed + buttonUpDownDuration = 0; // Time when button was pressed + buttonDownDownDuration = 0; // Time when button was pressed + + bButtonSetChanged = false; + bButtonSetUp = false; + bButtonSetDown = false; + + bButtonUpChanged = false; + bButtonUpUp = false; + bButtonUpDown = false; + + bButtonDownChanged = false; + bButtonDownUp = false; + bButtonDownDown = false; + + m_nMainMode = 0; + m_nItemMode = 0; + m_nItem = 0; + m_nValue = 0; + m_pUnit = (uint8_t *) unit[item_unit_idx[m_nItem]]; + m_nD0 = m_nD1 = m_nD2 = m_nD3 = 0; + + lastMainMode = 1; + lastItemMode = 1; + lastItem = UI_ITEM::ITEM_COUNT; + lastValue = 1; + lastUnit = nullptr; + lastD0 = lastD1 = lastD2 = lastD3 = 1; +} + +void CUI::checkButtonStates(unsigned long currentMillis) { + if (bButtonSetChanged) { + // Compare with the last interrupt time to ensure debounce delay + if (currentMillis - buttonSetChangeTime > DEBOUNCE_DELAY) { + if (digitalRead(PIN_SW_SET) == LOW) { + // Button pressed + buttonSetDownTime = buttonSetChangeTime; + bButtonSetDown = true; + } else { + // Button released + if (bButtonSetDown) { + bButtonSetUp = true; + bButtonSetDown = false; + buttonSetDownDuration = currentMillis - buttonSetDownTime; + //ESP_LOGI(TAG_UI,"UI Button - SET button RELEASED. Down for %dms\n", buttonSetDownDuration); + } + } + //lastProcessedTime = currentMillis; // Update processed time + bButtonSetChanged = false; // Reset the flag + } + } + + if (bButtonUpChanged) { + // Compare with the last interrupt time to ensure debounce delay + if (currentMillis - buttonUpChangeTime > DEBOUNCE_DELAY) { + if (digitalRead(PIN_SW_UP) == LOW) { + // Button pressed + buttonUpDownTime = buttonUpChangeTime; + bButtonUpDown = true; + } else { + // Button released + if (bButtonUpDown) { + bButtonUpUp = true; + bButtonUpDown = false; + buttonUpDownDuration = currentMillis - buttonUpDownTime; + //ESP_LOGI(TAG_UI,"UI Button - UP button RELEASED. Down for %dms\n", buttonUpDownDuration); + } + } + //lastProcessedTime = currentMillis; // Update processed time + bButtonUpChanged = false; // Reset the flag + } + } + + if (bButtonDownChanged) { + // Compare with the last interrupt time to ensure debounce delay + if (currentMillis - buttonDownChangeTime > DEBOUNCE_DELAY) { + if (digitalRead(PIN_SW_DOWN) == LOW) { + // Button pressed + buttonDownDownTime = buttonDownChangeTime; + bButtonDownDown = true; + } else { + // Button released + if (bButtonDownDown) { + bButtonDownUp = true; + bButtonDownDown = false; + buttonDownDownDuration = currentMillis - buttonDownDownTime; + //ESP_LOGI(TAG_UI,"UI Button - DOWN button RELEASED. Down for %dms\n", buttonDownDownDuration); + } + } + //lastProcessedTime = currentMillis; // Update processed time + bButtonDownChanged = false; // Reset the flag + } + } +} + +// ISR for the Set button handling +IRAM_ATTR void buttonSetISR() { + // Record the time of the button interrupt and set a flag + ui.buttonSetChangeTime = millis(); + ui.bButtonSetChanged = true; // Flag for main loop to process +} + +// ISR for the Up button handling +IRAM_ATTR void buttonUpISR() { + ui.buttonUpChangeTime = millis(); + ui.bButtonUpChanged = true; // Flag for main loop to process +} + +// ISR for the Down button handling +IRAM_ATTR void buttonDownISR() { + ui.buttonDownChangeTime = millis(); + ui.bButtonDownChanged = true; // Flag for main loop to process +} \ No newline at end of file diff --git a/UI.h b/UI.h new file mode 100644 index 0000000..82f2883 --- /dev/null +++ b/UI.h @@ -0,0 +1,103 @@ +#ifndef CUI_H +#define CUI_H + +#include "SSD1306.h" + +#define WIDTH SCREEN_WIDTH +#define HEIGHT SCREEN_HEIGHT +#define DISPLAY_PAGE_COUNT (SCREEN_HEIGHT / 8) + +enum ITEM_MODE { + MODE_NORMAL, + MODE_SET, + MODE_CONFIG, +}; + +enum MESSAGE_MODE { + MODE_NONE, + MODE_NO_WIFI, + MODE_AC, + MODE_LOAD +}; + +typedef struct box_struct { + int16_t x, y; + uint16_t w, h; +} BOX_TYPE; + +class CUI : public SSD1306 { +public: + // Constructor + //CUI(Adafruit_SSD1306 &display); + CUI(); + void setup(); + void start(); + void updateDisplay(unsigned long tick); + void updateDisplayTop(unsigned long tick); + void updateDisplayBottom(unsigned long tick); + void message(uint8_t lineNo, char *szMessage); + void progress(uint8_t lineNo, uint8_t progress); + void loopButton(unsigned long tickMillis); + inline void setItemMode(uint16_t mode) { m_nItemMode = mode; }; + + // Shared variables with ISR + volatile bool bButtonSetChanged; + volatile bool bButtonUpChanged; + volatile bool bButtonDownChanged; + volatile unsigned long buttonSetChangeTime; // Time when button was pressed + volatile unsigned long buttonUpChangeTime; // Time when button was pressed + volatile unsigned long buttonDownChangeTime; // Time when button was pressed + +private: + uint16_t m_nMessageMode, lastMessageMode; + uint16_t m_nMainMode, lastMainMode; // Temp, Humid, Clock + uint16_t m_nItemMode, lastItemMode; // Normal, Set, Config + uint16_t m_nItem, lastItem; // Temp, Hum, Heat ... + int16_t m_nValue, lastValue; + uint8_t *m_pUnit, *lastUnit; + uint16_t m_nD0, lastD0; + uint16_t m_nD1, lastD1; + uint16_t m_nD2, lastD2; + uint16_t m_nD3, lastD3; + uint8_t *m_pDUnit, *lastpDUnit; + BOX_TYPE boxMode, boxTitle, boxValue, boxUnit, boxD0, boxD1, boxD2, boxDot, boxD3, boxDUnit; + + bool bDot; + bool bButtonChanged; + + + + // Helper function to update digits + bool displayTop(); + bool displayBottom(); + //bool displayTemperature(); + //bool displayHumidity(); + //void displayClock() {}; + void getBoundaries(); + + // Buttons + unsigned long buttonSetDownTime; + unsigned long buttonUpDownTime; + unsigned long buttonDownDownTime; + + unsigned int buttonSetDownDuration; + unsigned int buttonUpDownDuration; + unsigned int buttonDownDownDuration; + + bool bButtonSetUp; // Flag for menu button + bool bButtonSetDown; // Flag for menu button + + bool bButtonUpUp; // Flag for up button + bool bButtonUpDown; // Flag for down button + + + bool bButtonDownUp; // Flag for up button + bool bButtonDownDown; // Flag for down button + + void initButtonState(); + void checkButtonStates(unsigned long tickMillis); + +}; +#endif + +extern CUI ui; diff --git a/UPnPClient.cpp b/UPnPClient.cpp new file mode 100644 index 0000000..ef39e39 --- /dev/null +++ b/UPnPClient.cpp @@ -0,0 +1,491 @@ +#include +#include "HermitCrab.h" +#include "UPnpClient.h" +#include "Config.h" +#include "WiFiHost.h" +#define TAG_UPNP "UPnP" + + +bool CUpnpClient::registerUPnP(uint32_t *pip, uint16_t *pport) { + routerIP = *pip; // Gateway IP address + publicPort = *pport; // host.m_nPublicPort = config.m_nPublicPort + publicIP = 0UL; + bool bSuccess = false; + ESP_LOGI(TAG_UPNP,"UPnP %s(%D)\n", routerIP.toString().c_str(), publicPort); + + // Step 1 - Discover UPnP service. + if (discoverUPnP()) { + // Step 2 - Check if mapping already exists + if (!(bSuccess = requestPortMappingEntry())) { + // Step 3 - Request new mapping + bSuccess = requestPortForwarding(); + } + } else { + ESP_LOGI(TAG_UPNP," UPnP discovery failed."); + } + + if (bSuccess) { + // Extract external IP + requestExternalIP(); + ESP_LOGI(TAG_UPNP," Public IP(Port): %s(%d)\n", publicIP.toString().c_str(), publicPort); + + + // Extract external port assigned by UPnP + // requestExternalPort(); + // ESP_LOGI(TAG_UPNP,"Assigned External Port: %d\n", publicPort); + + *pport = publicPort; // External server port + *pip = publicIP; // External IP address + return true; + } else { + ESP_LOGI(TAG_UPNP," UPnP PortForwarding failed."); + } + return bSuccess; +} + +bool CUpnpClient::discoverUPnP() { + //ESP_LOGI(TAG_UPNP,"\n\n** Sending SSDP discovery request..."); + WiFiUDP udp; + bool ret = false; + + // SSDP M-SEARCH Request + const char *ssdpRequest = + "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "MX: 3\r\n" + "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" + "\r\n"; + + udp.beginMulticast(SSDP_MULTICAST_IP, SSDP_PORT); + udp.beginPacket(SSDP_MULTICAST_IP, SSDP_PORT); + udp.write((const uint8_t *)ssdpRequest, strlen(ssdpRequest)); // Fix length + udp.endPacket(); + + unsigned long startTime = millis(); + while (millis() - startTime < SEARCH_TIMEOUT) { + int packetSize = udp.parsePacket(); + if (packetSize > 0) { + udp.read(buffer, sizeof(buffer) - 1); + buffer[packetSize] = '\0'; + + //ESP_LOGI(TAG_UPNP,"SSDP response received:"); + //ESP_LOGI(TAG_UPNP,buffer); + + // Check if the response contains "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1" + char *stField = strstr(buffer, "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1"); + if (stField) { + // Extract router's UPnP service URL + char *location = strstr(buffer, "LOCATION: "); + if (location) { + location += 9; // Move past "LOCATION: " + while (*location == ' ') location++; // skip blanks + char *end = strchr(location, '\r'); + if (end) { + *end = '\0'; + //ESP_LOGI(TAG_UPNP,"Router UPnP URL: %s\n", location); + routerLocation = String(location); + + // Extract IP and Port from the Location URL + int firstColonPos = routerLocation.indexOf(':'); // Find the first colon (for the protocol) + int secondColonPos = routerLocation.indexOf(':', firstColonPos + 1); // Find the second colon (IP:PORT) + + if (secondColonPos != -1) { + routerIPString = routerLocation.substring(routerLocation.indexOf("://") + 3, secondColonPos); // Extract IP + routerPort = routerLocation.substring(secondColonPos + 1, routerLocation.indexOf('/', secondColonPos)).toInt(); // Extract Port + } + + routerIP.fromString(routerIPString); + //ESP_LOGI(TAG_UPNP,"Router UPnP IP(Port): %s(%d)\n", routerIP.toString().c_str(), routerPort); + + String xml = fetchUPnPDescription(routerLocation); + if (!xml.isEmpty()) { + // ESP_LOGI(TAG_UPNP,xml); + // ESP_LOGI(TAG_UPNP,"\n--- End Of XML ----\n"); + + // Parse the XML and get the controlURL + // String parseXML(const String &xml); + controlURL = parseXML(xml); + if (!controlURL.isEmpty()) { + ret = true; + break; + } + } + } + } + } + } + } + udp.stop(); + + if (!ret) { + DPRINTLN("No SSDP response from UPnP device."); + } + return ret; +} + +String CUpnpClient::fetchUPnPDescription(const String &location) { + HTTPClient http; + WiFiClient client; + + //ESP_LOGI(TAG_UPNP,"Fetching UPnP XML from: \"%s\"\n", location.c_str()); + + http.begin(client, location); + int httpCode = http.GET(); + + if (httpCode > 0) { + //ESP_LOGI(TAG_UPNP,"HTTP Response Code: %d\n", httpCode); + if (httpCode == HTTP_CODE_OK) { + String xmlContent = http.getString(); + http.end(); + return xmlContent; + } + } + else { + //ESP_LOGI(TAG_UPNP,"HTTP Request failed, error: %s\n", http.errorToString(httpCode).c_str()); + } + + http.end(); + return ""; +} + +String CUpnpClient::parseXML(const String &xml) { + // Locate the WANIPConnection service + int servicePos = xml.indexOf("urn:schemas-upnp-org:service:WANIPConnection:1"); + if (servicePos == -1) { + ESP_LOGI(TAG_UPNP,"WANIPConnection service not found in XML."); + return ""; + } + + // Find the start of the controlURL tag within the block + int controlStart = xml.indexOf("", servicePos); + if (controlStart == -1) { + ESP_LOGI(TAG_UPNP,"controlURL not found in service block."); + return ""; + } + controlStart += 12; // Move past "" + + // Find the closing tag + int controlEnd = xml.indexOf("", controlStart); + if (controlEnd == -1) { + ESP_LOGI(TAG_UPNP,"Malformed XML: Missing ."); + return ""; + } + + // Extract and clean the controlURL + String controlURL = xml.substring(controlStart, controlEnd); + controlURL.trim(); // Remove any spaces or newlines + + //ESP_LOGI(TAG_UPNP,"Extracted controlURL: %s\n", controlURL.c_str()); + return controlURL; +} + +int CUpnpClient::sendSoapRequest(const char *request, char *response, size_t responseSize) { + WiFiClient client; + if (!client.connect(routerIP, routerPort)) { // UPnP uses port 1900 for SOAP requests + ESP_LOGI(TAG_UPNP,"Failed to connect to router: %s\n", routerIP.toString().c_str()); + return -1; + } + + client.print("POST /control?WANIPConn1 HTTP/1.1\r\n"); + client.print("Host: "); + client.print(routerIP); + client.print("\r\n"); + client.print("Content-Type: text/xml; charset=\"utf-8\"\r\n"); + client.print("SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetSpecificPortMappingEntry\"\r\n"); + client.print("Content-Length: "); + client.print(strlen(request)); + client.print("\r\n\r\n"); + client.print(request); + + unsigned long startMillis = millis(); + while (!client.available() && millis() - startMillis < 5000) { + delay(10); // Wait for response + } + + int index = 0; + while (client.available() && index < responseSize - 1) { + response[index++] = client.read(); + } + response[index] = '\0'; + + client.stop(); + return index; +} + +bool CUpnpClient::requestPortMappingEntry() { + ESP_LOGI(TAG_UPNP,"** Requesting UPnP Port Mapping Entry..."); + char soapRequest[512]; + char mappingName[32]; + sprintf(mappingName, "HC_%04X", publicPort); + + snprintf(soapRequest, sizeof(soapRequest), + "" + "" + "" + "" + "" + "%d" + "TCP" + "%s" + "%s" + "" + "" + "", + publicPort, + WiFi.localIP().toString().c_str(), + mappingName); + + char response[1024]; + int responseLen = sendSoapRequest(soapRequest, response, sizeof(response)); + + if (responseLen > 0) { + if (strstr(response, mappingName)) { + //ESP_LOGI(TAG_UPNP," Port mapping already exists."); + return true; + } + } + ESP_LOGI(TAG_UPNP," Port mapping not found."); + return false; +} + +bool CUpnpClient::requestPortForwarding() { + // Sending port-forwarding request to the router's external IP or gateway + ESP_LOGI(TAG_UPNP,"** Requesting UPnP Port Forwarding..."); + + if (controlURL.isEmpty()) { + ESP_LOGI(TAG_UPNP,"Error: controlURL is empty."); + return false; + } + + // Ensure controlURL is correctly formatted (handle absolute/relative cases) + String postURL = controlURL; + if (postURL.startsWith("http://") || postURL.startsWith("https://")) { + int pathStart = postURL.indexOf('/', 8); // Skip "http://" + if (pathStart != -1) { + postURL = postURL.substring(pathStart); // Extract path only + ESP_LOGI(TAG_UPNP,"PostURL: \"%s\"\n", postURL.c_str()); + } else { + ESP_LOGI(TAG_UPNP,"Invalid controlURL format."); + return false; + } + } + + // XML body + char xmlBody[800]; + snprintf(xmlBody, sizeof(xmlBody), + "" + "" + "" + "" + "%d" + "TCP" + "%d" + "%s" + "1" + "HC_%04X" + "0" + "", + publicPort, publicPort, + WiFi.localIP().toString().c_str(), + publicPort); + //ESP_LOGI(TAG_UPNP,"XML Body: "); + //ESP_LOGI(TAG_UPNP,xmlBody); + + + // Calculate the correct content length + int contentLength = strlen(xmlBody); + + snprintf(buffer, sizeof(buffer), + "POST %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Content-Type: text/xml; charset=\"utf-8\"\r\n" + "SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"\r\n" + "Content-Length: %d\r\n" + "Connection: Close\r\n" + "\r\n" + "%s", // Append XML body after headers + controlURL.c_str(), + routerIP.toString().c_str(), routerPort, + contentLength, + xmlBody); + + //ESP_LOGI(TAG_UPNP,"==== HTTP Request ===\n\n%s\n\n--- End Of HTTP ---\n", buffer); + + // Connect and send + WiFiClient client; + if (!client.connect(routerIP, routerPort)) { + ESP_LOGI(TAG_UPNP,"Failed to connect to the router. %s:%d\n", routerIP.toString().c_str(), routerPort); + return false; + } + + client.print(buffer); + + // Read response + String response; + uint32_t timeout = millis() + 5000; + while (client.available() == 0) { + if (millis() > timeout) { + ESP_LOGI(TAG_UPNP,"Router did not respond to port mapping request."); + client.stop(); + return false; + } + } + + while (client.available()) { + response += client.readString(); + } + client.stop(); + return true; +}; + +bool CUpnpClient::requestExternalIP() { + //ESP_LOGI(TAG_UPNP,"\n\nRequesting External IP via UPnP..."); + + // XML Request Body + char xmlBody[300]; + snprintf(xmlBody, sizeof(xmlBody), + "" + "" + "" + "" + "" + ""); + + int contentLength = strlen(xmlBody); + + // HTTP Request + snprintf(buffer, sizeof(buffer), + "POST %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Content-Type: text/xml; charset=\"utf-8\"\r\n" + "SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress\"\r\n" + "Content-Length: %d\r\n" + "Connection: Close\r\n" + "\r\n" + "%s", + controlURL.c_str(), + routerIP.toString().c_str(), routerPort, + contentLength, + xmlBody); + + // Connect and Send + WiFiClient client; + if (!client.connect(routerIP, routerPort)) { + DPRINTLN("Failed to connect to router for External IP request."); + return false; + } + + client.print(buffer); + + // Read Response + String response; + uint32_t timeout = millis() + 5000; + while (client.available() == 0) { + if (millis() > timeout) { + DPRINTLN("Router did not respond to External IP request."); + client.stop(); + return false; + } + } + + while (client.available()) { + response += client.readString(); + } + client.stop(); + + //ESP_LOGI(TAG_UPNP,"External IP Response:"); + //ESP_LOGI(TAG_UPNP,response); + + // Extract External IP from XML + char *sz = (char *)(response.c_str()); + char *extIP = strstr(sz, ""); + if (extIP) { + extIP += 22; + char *end = strchr(extIP, '<'); + if (end) *end = '\0'; + + publicIP = IPAddress(extIP); + DPRINTF("UPnP - External IP: %s\n", extIP); + return true; + } + + DPRINTLN("Failed to extract External IP."); + return false; +} + +bool CUpnpClient::requestExternalPort() { + //ESP_LOGI(TAG_UPNP,"\n\nRequesting External Port via UPnP..."); + + // XML Request Body for getting External Port + char xmlBody[300]; + snprintf(xmlBody, sizeof(xmlBody), + "" + "" + "" + "" + "" + ""); + + int contentLength = strlen(xmlBody); + + // HTTP Request to get External Port + snprintf(buffer, sizeof(buffer), + "POST %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Content-Type: text/xml; charset=\"utf-8\"\r\n" + "SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalPort\"\r\n" + "Content-Length: %d\r\n" + "Connection: Close\r\n" + "\r\n" + "%s", + controlURL.c_str(), + routerIP.toString().c_str(), routerPort, + contentLength, + xmlBody); + + // Connect and Send + WiFiClient client; + if (!client.connect(routerIP, routerPort)) { + ESP_LOGI(TAG_UPNP,"Failed to connect to router for External Port request."); + return false; + } + + client.print(buffer); + + // Read Response + String response; + uint32_t timeout = millis() + 5000; + while (client.available() == 0) { + if (millis() > timeout) { + ESP_LOGI(TAG_UPNP,"Router did not respond to External Port request."); + client.stop(); + return false; + } + } + + while (client.available()) { + response += client.readString(); + } + client.stop(); + + //ESP_LOGI(TAG_UPNP,"External Port Response:"); + //ESP_LOGI(TAG_UPNP,response); + + // Extract External Port from XML + char *sz = (char *)(response.c_str()); + char *extPort = strstr(sz, ""); + if (extPort) { + extPort += 17; + char *end = strchr(extPort, '<'); + if (end) *end = '\0'; + + publicPort = atoi(extPort); + ESP_LOGI(TAG_UPNP,"External Port: %d\n", publicPort); + return true; + } + + ESP_LOGI(TAG_UPNP,"Failed to extract External Port."); + return false; +}; diff --git a/UPnPClient.h b/UPnPClient.h new file mode 100644 index 0000000..9296cee --- /dev/null +++ b/UPnPClient.h @@ -0,0 +1,37 @@ +#include +#include +#define SSDP_MULTICAST_IP IPAddress("239.255.255.250") +#define SSDP_PORT 1900 +#define UPNP_HTTP_PORT 5000 // Most routers use 5000, but it can vary +#define SEARCH_TIMEOUT 5000 // 2 seconds timeout + +class CUpnpClient { +private: + IPAddress routerIP; + String routerIPString; + uint16_t routerPort; + + IPAddress publicIP; + uint16_t publicPort; + + uint32_t lastDiscoveryTime; + String routerLocation; + String controlURL; + char buffer[1024]; // Stack-based buffer for responses + +public: + CUpnpClient() : publicPort(0), lastDiscoveryTime(0) {} + + bool registerUPnP(uint32_t *pip, uint16_t *pport); + +private: + String fetchUPnPDescription(const String &location); + String parseXML(const String &xml); + int sendSoapRequest(const char *request, char *response, size_t responseSize); + + bool discoverUPnP(); + bool requestPortMappingEntry(); + bool requestPortForwarding(); + bool requestExternalIP(); + bool requestExternalPort(); +}; diff --git a/WiFiHost.cpp b/WiFiHost.cpp new file mode 100644 index 0000000..1abeb52 --- /dev/null +++ b/WiFiHost.cpp @@ -0,0 +1,885 @@ +#include +#undef INADDR_NONE +#include +#include +#include +#include +#include +#include // For struct in_addr +#include +#include +#include "OTA.h" + +#include "HermitCrab.h" +#include "Config.h" +#include "ConnectWiFi.h" +#include "TimeManager.h" +#include "History.h" +#include "zcd.h" +#include "AHT2x.h" +#include "WiFiHost.h" +#include "UPnPClient.h" + + + +#define TAG_WIFI_HOST "WiFi Host" +#define TCP_PACKET_SIZE_MAX 1460 + +//WiFiServer wifiServer(SERVER_PORT); +//WiFiClient wifiClient; +CWiFiHost host; +CONFIG_TYPE configCopy; + +void CWiFiHost::Setup() +{ + // UDP Packet + packetUDP = { + .sig1 = SIGNATURE1, + .m_nSize = sizeof(UDP_PACKET), + .m_nMessage = UDP_MESSAGE::MESSAGE_HEARTBEAT, + .m_nDeviceType = config.m_nDeviceType, + .m_nResetReason = RESET_REASON_CHIP_POWER_ON, + .m_nChipID = config.m_nChipId, + .m_nDeviceID = 0, + //.m_nVersion = (uint32_t)atoll(&HC__VERSION[2]), + .dwIPAddress = 0, + .m_nPort = UDP_PORT, + //.m_MACAddress = "", + //.m_sDeviceName = "", + .status = {0}, + .sig2 = SIGNATURE2 }; + packetUDP.m_nVersion = (uint32_t)atoll(&HC__VERSION[2]); + strncpy((char *)packetUDP.m_MACAddress, WiFi.macAddress().c_str(), 17); + packetUDP.m_MACAddress[17] = 0; + strcpy(packetUDP.m_sDeviceName, config.m_sDeviceName); + + // TCP Packet + hostPacket = { + .sig1 = SIGNATURE1, + .len = sizeof(TCP_PACKET), + .cmd = ENUM_COMMAND::CMD_HELLO, + .op = OPERATION_MODE::MODE_WAITING, + .time = 0, + .status = {0}, + .sig2 = SIGNATURE2 }; + clientPacket = hostPacket; + + //if (m_nPublicPort == 0) + { + if (config.m_nPublicPort == 0) + config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF); + m_nPublicPort = config.m_nPublicPort; + } + + wifiServer = WiFiServer(SERVER_PORT, 1); + wifiExternal = WiFiServer(m_nPublicPort, 1); + + //wifiStatus = WIFI_NOT_CONNECTED; + m_nDataSend_sent = 0; + m_nDataReceive_size = 0; + m_pDataSend_data = nullptr; + m_nDataReceive_received = 0; + m_nDataReceive_size = 0; + m_pDataReceive_data = nullptr; + + externalServerIP = IPAddress((uint32_t) 0UL); + //sockfd = -1; + //connectStartTime = 0; + //isConnecting = false; + + // Operation + m_nMode = MODE_WAITING; + m_bHelloSent = false; + m_bSendHistoryPending = false; + + m_nLastReceivedTime = + m_nLastHeartBeatSentTime = + m_nLastUDPBroadcastTime = millis(); + m_bClientConnected = false; + m_dwPublicIP = 0; + wifiClient.stop(); + + if (isWiFiConnected()) + { + // UPnP Client + { + uint32_t ip = WiFi.gatewayIP(); + uint16_t port = m_nPublicPort; + CUpnpClient upnp; + + if (upnp.registerUPnP(&ip, &port)) { + status.nFlags |= FLAG_UPNP; + } else { + status.nFlags &= ~FLAG_UPNP; + } + + if (ip != 0) + m_dwPublicIP = ip; + if (port != m_nPublicPort) { + config.m_nPublicPort = port; + config.save(); + m_nPublicPort = port; + } + + } + + // Server + wifiServer.begin(SERVER_PORT, 1); + wifiExternal.begin(m_nPublicPort, 1); + m_nLastReceivedTime = millis(); + m_bClientConnected = false; + + // UDP + packetUDP.dwIPAddress = WiFi.localIP(); + udpLocal.begin(UDP_PORT); + udpExternal.begin(UDP_EXTERNAL_PORT); + + if (m_cExternalServerIPAddress == IPAddress((uint32_t) 0l)) { + if (WiFi.hostByName("visionsoft.kr", m_cExternalServerIPAddress)) { + DPRINTF("WiFi - ExternalServer IP resolved as \"%s\"\n", m_cExternalServerIPAddress.toString().c_str()); + } else { + m_cExternalServerIPAddress = IPAddress((uint32_t) 0); + DPRINTF("WiFi - ExternalServer IP NOT resolved\n"); + } + } + } +} + +MY_IRAM_ATTR void CWiFiHost::Stop() { + CloseConnection(); + + // Stop server + wifiServer.stop(); + wifiExternal.stop(); + m_bClientConnected = false; + + // Stop Client + if (wifiClient) + wifiClient.stop(); + + // Stop UDP + udpLocal.stop(); // or udpLocal.end(); depending on your preference + udpExternal.stop(); // or udpExternal.end(); depending on your preference +} + +MY_IRAM_ATTR void CWiFiHost::CloseConnection() +{ + if (wifiClient && wifiClient.connected()) wifiClient.stop(); + m_bClientConnected = false; + m_nMode = MODE_WAITING; +} + +IRAM_ATTR void CWiFiHost::Loop(unsigned long clock) +{ + static unsigned long lastReceivedTime = 0; + if (!isWiFiConnected()) return; + + switch (m_nMode) { + case MODE_WAITING: // Expecting connection from clients + if (m_bClientConnected) + { + ESP_LOGI(TAG_WIFI_HOST,"Host: dropping connection for not connected"); + if (wifiClient && wifiClient.connected()) { + ESP_LOGI(TAG_WIFI_HOST,"Host: stopping wifi client"); + wifiClient.stop(); + } + m_bClientConnected = false; + } + + // Accept from internal XOR external port + wifiClient = wifiServer.accept(); + if (!wifiClient || !wifiClient.connected()) { + wifiClient = wifiExternal.accept(); + } + + if (wifiClient && wifiClient.connected()) + { + ESP_LOGI(TAG_WIFI_HOST,"Host: Connection Accepted"); + wifiClient.setNoDelay(true); + m_nLastReceivedTime = clock; + m_bClientConnected = true; + m_bHelloSent = false; + m_nMode = MODE_PACKET; + + // LED + ledcWrite(PIN_LED_WIFI, PWM_FULL - 10); // Amost ON + } + break; + case MODE_PACKET: + // Client Connected + if (m_bClientConnected && wifiClient.connected()) + { + CheckClient(clock); + + if (m_bClientConnected) { + if (clock - m_nLastReceivedTime > 60000) + { + ESP_LOGI(TAG_WIFI_HOST,"Host: dropping connection for no HB in 60 seconds"); + wifiClient.stop(); + m_bClientConnected = false; + } + + // Send HeartBeat + if (clock - m_nLastHeartBeatSentTime >= 1000) { + SendHeartBeat(); + m_nLastHeartBeatSentTime = clock; + } + } + } + if (!m_bClientConnected || !wifiClient.connected()) { + m_nMode = MODE_WAITING; + // LED + ledcWrite(PIN_LED_WIFI, PWM_FULL - 2); // Almost Off + } + break; + case MODE_SEND: + if (SendData(clock)) { + if (m_bSendHistoryPending) { + // Mark pending to send the second part: from the start to head-1 + //SendData(history.getRingData2() /* &ring[0] */, sizeof(STATUS_TYPE) * head); + //m_bSendHistoryPending = true; + //m_nPendingHistoryCount = history.getHead(); + m_bSendHistoryPending = false; + if (m_nPendingHistoryCount > 0) { + ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - 2nd part of History (%d)\n", m_nPendingHistoryCount); + SendData(history.getRingData2(), sizeof(STATUS_TYPE) * m_nPendingHistoryCount); + m_nPendingHistoryCount = 0; + } + } else { + m_nMode = MODE_PACKET; + } + } + break; + case MODE_RECV: + if (ReceiveData(clock)) m_nMode = MODE_PACKET; + break; + } +} + +MY_IRAM_ATTR void CWiFiHost::SendHeartBeat(unsigned long clock) { + if (!isWiFiConnected()) { + //ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendHeartBeat() called while not connected!"); + return; + } + + // Send Heartbeats to external server and to local devices + if (clock - m_nLastUDPBroadcastTime > 1000) { + static int count = 55; + + // UDP Heartbeat + if (++count >= 60) { + // External Heartbeat + UDP_CONFIG_TYPE pktConfig; + pktConfig.udp = packetUDP; + pktConfig.udp.m_nPort = m_nPublicPort; + pktConfig.udp.dwIPAddress = m_dwPublicIP; + pktConfig.udp.status = status; + + if (((uint32_t)m_cExternalServerIPAddress) != (uint32_t)0l) + udpExternal.beginPacket(m_cExternalServerIPAddress, (uint16_t)UDP_EXTERNAL_PORT); + else + udpExternal.beginPacket("visionsoft.kr", (uint16_t)UDP_EXTERNAL_PORT); + + if (config.bConfigSaved) { + // Config is save locally. Save it on cloud + config.bConfigSaved = false; + pktConfig.udp.m_nMessage = MESSAGE_CONFIG_SAVE; + pktConfig.con = config; + udpExternal.write((uint8_t*)&pktConfig, sizeof(pktConfig)); + ESP_LOGI(TAG_WIFI_HOST,"HeartBeat Packet *Config Save* sent out to external Server"); + } else { + // Send only UDP packet + udpExternal.write((uint8_t*)&(pktConfig.udp), sizeof(UDP_PACKET)); + } + udpExternal.endPacket(); + count = 0; + } else if (!m_bClientConnected) { + // Local AP broadcast + packetUDP.m_nMessage = UDP_MESSAGE::MESSAGE_HEARTBEAT; + packetUDP.dwIPAddress = WiFi.localIP(); + packetUDP.m_nPort = SERVER_PORT; + strcpy(packetUDP.m_sCompanyName, COMPANY_NAME); + strcpy(packetUDP.m_sService, SERVICE_NAME); + udpLocal.beginPacket((IPAddress)0xFFFFFFFF, (uint16_t)UDP_PORT); + udpLocal.write((uint8_t*)&packetUDP, sizeof(UDP_PACKET)); + udpLocal.endPacket(); + } + m_nLastUDPBroadcastTime = clock; + } +} + +MY_IRAM_ATTR void CWiFiHost::MonitorUDP() { + // Listen for UDP External port + IPAddress ip; + int size = udpExternal.parsePacket(); + if (size >= sizeof(packetUDP)) { + if ((udpExternal.read((char *) &packetUDP, sizeof(packetUDP)) == sizeof(packetUDP)) && + packetUDP.sig1 == SIGNATURE1 && + packetUDP.sig2 == SIGNATURE2 && + packetUDP.m_nChipID == config.m_nChipId) { + + switch (packetUDP.m_nMessage) { + case MESSAGE_IP: // Extenal IP and Port set + // if (packetUDP.m_nChipID == config.m_nChipId) { + // m_dwPublicIP = packetUDP.dwIPAddress; + // m_nPublicPort = packetUDP.m_nPort; + // ip = IPAddress(m_dwPublicIP); + // ESP_LOGI(TAG_WIFI_HOST,"External IP(%s) and Port(%d)\n", ip.toString().c_str(), m_nPublicPort); + // } else { + // ESP_LOGI(TAG_WIFI_HOST,"External Server Response for other device Received\n"); + // } + break; + case MESSAGE_CONFIG_SEND: + if (size == sizeof(UDP_PACKET) + sizeof(CONFIG_STRUCT)) { + if (udpExternal.read((char*) &configCopy, sizeof(configCopy))) { + ESP_LOGI(TAG_WIFI_HOST,"CONFIG received from the external DB server\n"); + } + } else { + char *buffer[256]; + while((size = udpExternal.parsePacket()) > 0) { + // Discard any leftover data + udpExternal.read((char*) &buffer, sizeof(buffer)); + } + } + break; + case MESSAGE_QUERY_IP: + // if (m_nMode == MODE_WAITING) { + // // Get sender's IP and port + // externalServerIP = udpExternal.remoteIP(); + // uint16_t senderPort = udpExternal.remotePort(); + // ESP_LOGI(TAG_WIFI_HOST,"External Server - MESSAGE_QUERY_IP from %s:%d\n", externalServerIP.toString().c_str(), senderPort); + // m_nMode = MODE_EXTERNAL_SERVER; + // } + break; + case MESSAGE_RESET: + Restart(); + break; + default: + break; + } + } + } +} + +IRAM_ATTR void CWiFiHost::CheckClient(unsigned long clock) +{ + bool bLED = false; + static TCP_PACKET cpkt; + int available = wifiClient.available(); + if (available >= sizeof(TCP_PACKET)) { + uint8_t* pC = (uint8_t*)&cpkt; + int count = wifiClient.readBytes(pC, sizeof(TCP_PACKET)); + if (count == sizeof(TCP_PACKET)) { + // Now we have a full packet size data + if (cpkt.sig1 == SIGNATURE1 && + cpkt.sig2 == SIGNATURE2 && + cpkt.len == sizeof(TCP_PACKET)) { + // Process the completed PACKET + ProcessPacket(cpkt); + m_nLastReceivedTime = clock; + return; + } + + // Invalid Packet - remove the first byte off from the buff + ESP_LOGI(TAG_WIFI_HOST,"Invalid Packet %s%s size: %d/%d\n", + cpkt.sig1 == SIGNATURE1 ? "SIG1 OK" : "", + cpkt.sig2 == SIGNATURE2 ? "SIG2 OK" : "", + cpkt.len, sizeof(TCP_PACKET)); + // Shift the buffer data off by 1 byte for the next cycle + //buffIndexC = sizeof(TCP_PACKET) - 1; + //memcpy(pC, &pC[1], buffIndexC); + while (m_bClientConnected && + wifiClient.connected() && + wifiClient.readBytes((uint8_t *) &cpkt, sizeof(cpkt)) > 0) { + }; + } + } +} + +IRAM_ATTR void CWiFiHost::ProcessPacket(TCP_PACKET& pkt) +{ + switch (pkt.cmd) + { + // System + case CMD_HEARTBEAT: + m_nLastReceivedTime = millis(); + //ESP_LOGI(TAG_WIFI_HOST,"H"); + break; + case CMD_HELLO: + case CMD_HELLO_DEBUG: + { + // Send the Config Data to PC + // Send Packet Back + ESP_LOGI(TAG_WIFI_HOST,"WiFi - HELLO received"); + pkt.u16[0] = SIGNATURE1; + pkt.n16[1] = sizeof(CONFIG_TYPE); + pkt.u16[2] = SIGNATURE2; + config.m_nChipId; + SendPacket(pkt); + + // Send Data Packets + ESP_LOGI(TAG_WIFI_HOST,"WiFi - Config Send pending..."); + int sent = SendData((uint8_t*)&config, sizeof(CONFIG_TYPE)); + m_bHelloSent = true; + } + break; + case CMD_DROP_CONNECTION: + wifiClient.stop(); + m_bClientConnected = false; + m_nMode = MODE_WAITING; + ESP_LOGI(TAG_WIFI_HOST,"WiFI - Client requets to DROP CONNECTION"); + break; + case CMD_RESET_REASON: + pkt.n16[0] = esp_reset_reason(); + SendPacket(pkt); + break; + case CMD_SAVE_RESTART: + if (pkt.u16[0] == SIGNATURE1 && + pkt.u16[1] == SIGNATURE2) { + yield(); + Restart(); + } + break; + case CMD_RESET_RESTART: + if (pkt.u16[0] == SIGNATURE1 && + pkt.u16[1] == SIGNATURE2) { + pkt.cmd = CMD_DROP_CONNECTION; + SendPacket(pkt); + yield(); + Restart(); + } + break; + case CMD_SEND_HISTORY: + if (pkt.u16[0] == SIGNATURE1 && pkt.u16[1] == SIGNATURE2) { + int16_t count = history.getRingCount(); + if (count > 0 ) { + // inform client the count of history + pkt.op = count; + SendPacket(pkt); + int16_t size = history.getRingSize(); + if (count < size) { + ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - Whold part of History (%d)\n", count); + SendData(history.getRingData1()/* &ring[tail] */, sizeof(STATUS_TYPE) * count); + } else { + int count1st = size - history.getRingTail(); + ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - 1st part of History Total(%d), 1st(%d) H(%d) T(%d)\n", + count, count1st, history.getRingHead(), history.getRingTail()); + SendData(history.getRingData1(), sizeof(STATUS_TYPE) * count1st), + // Mark pending to send the second part: from the start to head-1 + //SendData(history.getRingData2() /* &ring[0] */, sizeof(STATUS_TYPE) * head); + m_bSendHistoryPending = true; + m_nPendingHistoryCount = history.getRingHead(); + } + } + } + break; + case CMD_PAUSE: + break; + case CMD_RESUME: + break; + case CMD_RESET_SENSOR: + aht25.setScanFlag(true); + aht10_0x39.setScanFlag(true); + break; + + // Config + case CMD_INIT_CONFIG: + if (pkt.u16[0] == SIGNATURE1 && + pkt.u16[1] == sizeof(CONFIG_STRUCT) && + pkt.u16[2] == SIGNATURE2) { + config.init(); + config.save(); + } + break; + case CMD_LOAD_CONFIG: + if (pkt.u16[0] == SIGNATURE1 && + pkt.u16[1] == sizeof(CONFIG_STRUCT) && + pkt.u16[2] == SIGNATURE2) { + config.load(); + history.loadPID(); + } + break; + case CMD_SAVE_CONFIG: + if (pkt.u16[0] == SIGNATURE1 && + pkt.u16[1] == sizeof(CONFIG_STRUCT) && + pkt.u16[2] == SIGNATURE2) { + config.save(); + } + break; + case CMD_RECV_CONFIG: + // Receive Confif Data from PC + if (pkt.u16[0] == SIGNATURE1 && + pkt.u16[1] == sizeof(CONFIG_STRUCT) && + pkt.u16[2] == SIGNATURE2) { + ReceiveData((uint8_t *)&configCopy, sizeof(CONFIG_TYPE)); + m_bReceiveConfigPending = true; + ESP_LOGI(TAG_WIFI_HOST,"WiFi - Receive Config initiated..."); + } + break; + case CMD_SEND_CONFIG: + if (pkt.u16[0] == SIGNATURE1 && + pkt.u16[1] == sizeof(CONFIG_STRUCT) && + pkt.u16[2] == SIGNATURE2) { + SendPacket(pkt); + SendData((const uint8_t *)&config, (unsigned int) sizeof(config)); + ESP_LOGI(TAG_WIFI_HOST,"WiFi - Send Config initiated..."); + } + break; + case CMD_SEND_CONFIG_SERVER: + + case CMD_LOAD_CONFIG_SERVER: + break; + + // PID + case CMD_INIT_PID_PARAM: + history.savePID(); + break; + case CMD_LOAD_PID_PARAM: + history.loadPID(); + break; + case CMD_SAVE_PID_PARAM: + history.savePID(); + break; + case CMD_SET_PID: + config.Kp_Temp1 = pkt.f[0]; + config.Kd_Temp1 = pkt.f[1]; + config.LR_Temp1 = pkt.f[2]; + config.Kp_Humidity = pkt.f[3]; + config.Kd_Humidity = pkt.f[4]; + config.LR_Humidity = pkt.f[5]; + history.loadPID(); + break; + case CMD_GET_PID: + pkt.f[0] = config.Kp_Temp1; + pkt.f[1] = config.Kd_Temp1; + pkt.f[2] = config.LR_Temp1; + pkt.f[3] = config.Kp_Humidity; + pkt.f[4] = config.Kd_Humidity; + pkt.f[5] = config.LR_Humidity; + SendPacket(pkt); + break; + + // Control + case CMD_SET_CONTROL: + config.bSmartControl = pkt.by[0] ? true : false; + config.bNightControl = pkt.by[1] ? true : false; + config.bControlTemperature = pkt.by[2] ? true : false; + config.bControlHumidity = pkt.by[3] ? true : false; + break; + case CMD_GET_CONTROL: + pkt.by[0] = config.bSmartControl ? 0xFF : 0; + pkt.by[1] = config.bNightControl ? 0xFF : 0; + pkt.by[2] = config.bControlTemperature ? 0xFF : 0; + pkt.by[3] = config.bControlHumidity ? 0xFF : 0; + SendPacket(pkt); + break; + + // Operation + case CMD_SET_TEMP_TARGET: + config.nTempTarget = pkt.u16[0]; + ESP_LOGI(TAG_WIFI_HOST,"WiFi - TempTarget changed to: %d\n", pkt.u16[0]); + break; + case CMD_SET_TEMP_TARGET_NIGHT: + config.nTempTargetNight = pkt.u16[0]; + ESP_LOGI(TAG_WIFI_HOST,"WiFi - TempTargetNight changed to: %d\n", pkt.u16[0]); + break; + case CMD_SET_HUMID_TARGET: + config.nHumidTarget = pkt.u16[0]; + ESP_LOGI(TAG_WIFI_HOST,"WiFi - HumidTarget changed to: %d\n", pkt.u16[0]); + break; + case CMD_SET_SENSOR_OFFSET: + config.nTemp1Offset = pkt.n16[0]; + config.nHumid1Offset = pkt.n16[1]; + if (pkt.n16[2] > 1) { + config.nTemp2Offset = pkt.n16[2]; + config.nHumid2Offset = pkt.n16[3]; + if (pkt.n16[2] > 2) + config.nTemp3Offset = pkt.n16[4]; + } + ESP_LOGI(TAG_WIFI_HOST,"WiFi - SensorOffset changed to: %d, %d\n", pkt.n16[0], pkt.n16[1]); + break; + + case CMD_SET_AC1_PARAM: + config.ac1 = pkt.device; + break; + case CMD_SET_AC2_PARAM: + config.ac2 = pkt.device; + break; + case CMD_SET_MIST_PARAM: + config.mist = pkt.device; + break; + case CMD_SET_FAN_PARAM: + config.fan = pkt.device; + break; + case CMD_SET_MOTOR_PARAM: + config.motor = pkt.device; + break; + case CMD_SET_LIGHT_PARAM: + config.light = pkt.device; + break; + + // Status + case CMD_SET_HEATER1_DUTY: + status.nHeater1Duty = pkt.u16[0]; + if (pkt.u16[1]) + status.nFlags |= FLAG_MANUAL_HEATER1; + else + status.nFlags &= ~FLAG_MANUAL_HEATER1; + if (status.nHeater1Duty == 0) { + setHeater1Duty(0); + } + break; + case CMD_SET_HEATER2_DUTY: + status.nHeater2Duty = pkt.u16[0]; + if (pkt.u16[1]) + status.nFlags |= FLAG_MANUAL_HEATER2; + else + status.nFlags &= ~FLAG_MANUAL_HEATER2; + if (status.nHeater2Duty == 0) { + setHeater2Duty(0); + } + break; + case CMD_SET_MIST_DUTY: + status.nMistDuty = pkt.u16[0]; + if (pkt.u16[1]) + status.nFlags |= FLAG_MANUAL_MIST; + else + status.nFlags &= ~FLAG_MANUAL_MIST; + break; + case CMD_SET_FAN_DUTY: + status.nFanDuty = pkt.u16[0]; + if (pkt.u16[1]) + status.nFlags |= FLAG_MANUAL_FAN; + else + status.nFlags &= ~FLAG_MANUAL_FAN; + + break; + case CMD_SET_MOTOR_DUTY: + status.nMotorDuty = pkt.u16[0]; + if (pkt.u16[1]) + status.nFlags |= FLAG_MANUAL_MOTOR; + else + status.nFlags &= ~FLAG_MANUAL_MOTOR; + break; + case CMD_SET_LIGHT_DUTY: + status.nLightTargetDuty = pkt.u16[0]; + if (pkt.u16[1]) + status.nFlags |= FLAG_MANUAL_LIGHT; + else + status.nFlags &= ~FLAG_MANUAL_LIGHT; + break; + + // Manual Operation + case CMD_SET_MANUAL_HEATER1: + if (pkt.u16[0]) + status.nFlags |= FLAG_MANUAL_HEATER1; + else + status.nFlags &= ~FLAG_MANUAL_HEATER1; + break; + case CMD_SET_MANUAL_HEATER2: + if (pkt.u16[0]) + status.nFlags |= FLAG_MANUAL_HEATER2; + else + status.nFlags &= ~FLAG_MANUAL_HEATER2; + break; + case CMD_SET_MANUAL_MIST: + if (pkt.u16[0]) + status.nFlags |= FLAG_MANUAL_MIST; + else + status.nFlags &= ~FLAG_MANUAL_MIST; + break; + case CMD_SET_MANUAL_FAN: + if (pkt.u16[0]) + status.nFlags |= FLAG_MANUAL_FAN; + else + status.nFlags &= ~FLAG_MANUAL_FAN; + break; + case CMD_SET_MANUAL_LIGHT: + if (pkt.u16[0]) + status.nFlags |= FLAG_MANUAL_LIGHT; + else + status.nFlags &= ~FLAG_MANUAL_LIGHT; + break; + + // Time + case CMD_SET_TIME_NIGHT: + config.nNightStartHour = pkt.u16[0]; + config.nNightStartMin = pkt.u16[1]; + config.nNightEndHour = pkt.u16[2]; + config.nNightEndMin = pkt.u16[3]; + break; + case CMD_SET_WIFI_CLIENT_DISPLAY: + config.m_nDisplayTempHigh = pkt.n16[0]; + config.m_nDisplayTempLow = pkt.n16[1]; + config.m_nDisplayTime = pkt.n16[2]; + config.m_fShowRealTime = pkt.n16[3]; + config.m_fShowHistory = pkt.n16[4]; + ESP_LOGI(TAG_WIFI_HOST,"WiFi - Client Display Settings changed."); + break; + + default: + ESP_LOGI(TAG_WIFI_HOST,"WiFi - Packet Received Type: %d\n", pkt.cmd); + break; + } +} + +IRAM_ATTR int CWiFiHost::SendPacket(TCP_PACKET& pkt) +{ + size_t sent = 0; + if (m_bClientConnected && wifiClient && wifiClient.connected()) + { + sent = wifiClient.write((char*)&pkt, sizeof(TCP_PACKET)); + } + return sent; +} + +IRAM_ATTR size_t CWiFiHost::SendData(const uint8_t* data, size_t size) { + if (data != nullptr) { + m_nMode = MODE_SEND; + m_pDataSend_data = (char *) data; + m_nDataSend_size = size; + m_nDataSend_sent = 0; + ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData(size: %d)\n", size); + return size; + } + return 0; +} + +IRAM_ATTR bool CWiFiHost::SendData(unsigned long clock) +{ + if (m_nDataSend_sent < m_nDataSend_size) + { + bool connected = wifiClient.connected(); + if (m_bClientConnected && wifiClient && connected) { + size_t count = m_nDataSend_size - m_nDataSend_sent; + if (count > TCP_PACKET_SIZE_MAX) count = TCP_PACKET_SIZE_MAX; + //size_t avail = wifiClient.availableForWrite(); + //if (avail < count) { + // ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() avail(%d) is less than count(%d)\n", avail, count); + // //count = avail; + //} + + if (count > 0) { + int16_t sentCount = wifiClient.write(&m_pDataSend_data[m_nDataSend_sent], count); + if (sentCount != count) { + ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() sent(%d) is not count(%d)\n", sentCount, count); + if (sentCount <= 0) + yield(); + } + m_nDataSend_sent += sentCount; + ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() size(%d) sent(%d) total_sent(%d)\n", m_nDataSend_size, count, m_nDataSend_sent); + } + //else { + // ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() avail is 0"); + // yield(); + //} + } else { + m_nMode = MODE_WAITING; + m_pDataSend_data = nullptr; + m_nDataSend_size = 0; + m_nDataSend_sent = 0; + ESP_LOGI(TAG_WIFI_HOST," SendData: Connection lost - reset sendData()!"); + return true; + } + } + + if (m_nDataSend_sent == m_nDataSend_size) { + ESP_LOGI(TAG_WIFI_HOST, " SendDdata: DataSend completed!"); + return true; + } + return false; +} + +IRAM_ATTR size_t CWiFiHost::ReceiveData(uint8_t* data, size_t size) +{ + m_nMode = MODE_RECV; + m_pDataReceive_data = (char *) data; + m_nDataReceive_size = size; + m_nDataReceive_received = 0; +#ifdef ESP8266 + digitalWrite(PIN_EXTRA, LED_ON); +#endif + return size; +} + +IRAM_ATTR bool CWiFiHost::ReceiveData(unsigned long clock) +{ + // Receive Data + size_t nSize = 0; + if (m_nDataReceive_received < m_nDataReceive_size) + { + if (m_bClientConnected && wifiClient && wifiClient.connected()) { + int16_t count = m_nDataReceive_size - m_nDataReceive_received; + if (count > TCP_PACKET_SIZE_MAX) count = TCP_PACKET_SIZE_MAX; + nSize = wifiClient.readBytes(&m_pDataReceive_data[m_nDataReceive_received], count); + if (nSize > 0) { + m_nLastReceivedTime = clock; + m_nDataReceive_received += nSize; + } + } else { + m_nMode = MODE_WAITING; + m_pDataReceive_data = nullptr; + m_nDataReceive_size = 0; + m_nDataReceive_received = 0; + ESP_LOGI(TAG_WIFI_HOST," SendData: Connection lost - reset receiveData()!"); + return true; + } + } + + // Check for any pending action + if (m_nDataReceive_received == m_nDataReceive_size) { + if (m_bReceiveConfigPending && m_pDataReceive_data == (char *)&configCopy) { + config = configCopy; + history.loadPID(); + ESP_LOGI(TAG_WIFI_HOST,"WiFi - Config Received"); + m_bReceiveConfigPending = false; + } + ESP_LOGI(TAG_WIFI_HOST," ReceiveData: Data Receive Completed!"); + return true; + } + + if (nSize <= 0 && clock - m_nLastReceivedTime > 5000) { + ESP_LOGI(TAG_WIFI_HOST," ReceiveData: TimeOut Abort!"); + return true; + } + return false; +} + +IRAM_ATTR void CWiFiHost::SendHeartBeat() { + if (m_bHelloSent) { + hostPacket.cmd = CMD_HEARTBEAT; + time_t now = time(NULL); // Get current time in seconds + status.now = (uint32_t) time(NULL); + //status.uptime = now - timeManager.getFirstNTPTime(); + hostPacket.status = status; + SendPacket(hostPacket); + } +} + +MY_IRAM_ATTR void CWiFiHost::Restart() { + if (isWiFiConnected()) { + if (m_bClientConnected && m_bClientConnected && wifiClient && wifiClient.connected()) { + hostPacket.cmd = CMD_DROP_CONNECTION; + SendPacket(hostPacket); + } + } + vTaskDelay(500/portTICK_PERIOD_MS); + + // stop all network sockets + Stop(); // Stop Sockets + + vTaskDelay(50/portTICK_PERIOD_MS); + + // Turn Off + setHeater1Duty(0); + setHeater2Duty(0); + ledcWrite(PIN_MIST, 0); + ledcWrite(PIN_FAN, 0); + ledcWrite(PIN_MOTOR, 0); + ledcWrite(PIN_LIGHT, 0); + vTaskDelay(50/portTICK_PERIOD_MS); + config.statusSave = status; + config.save(); + vTaskDelay(50/portTICK_PERIOD_MS); + ESP.restart(); +} diff --git a/WiFiHost.h b/WiFiHost.h new file mode 100644 index 0000000..43e2d16 --- /dev/null +++ b/WiFiHost.h @@ -0,0 +1,284 @@ +#ifndef __WIFI_HOST_H +#define __WIFI_HOST_H +#include "HermitCrab.h" +#ifdef ESP8266 +#include +#else +#include +#endif +#include + +#define UDP_PORT 4949 +#define SERVER_PORT 3939 +#define UDP_EXTERNAL_PORT (UDP_PORT + 1) +#define TCP_EXTERNAL_PORT (SERVER_PORT + 1) +#define CONNECT_TIMEOUT_MS 5000 + +enum UDP_MESSAGE { + MESSAGE_HEARTBEAT, + MESSAGE_IP, + MESSAGE_NOTFOUND, + MESSAGE_CONFIG_SAVE, + MESSAGE_CONFIG_SEND, + MESSAGE_AP, + MESSAGE_QUERY_IP, + MESSAGE_UPDATE_STATUS, + MESSAGE_UPDATE_CONFIG, + + + + MESSAGE_COUNT, + MESSAGE_RESET = UDP_EXTERNAL_PORT, +}; + +enum OPCODE_TYPE { + OP_RECV_ONLY, + OP_RECV_SAVE, +}; + +enum OPERATION_MODE { + MODE_WAITING, + MODE_PACKET, + MODE_SEND, + MODE_RECV, + MODE_EXTERNAL_SERVER +}; + +enum ENUM_COMMAND +{ + // Command + CMD_HEARTBEAT = 1, // 1 HeartBeat - prevents disconnect from the host + + // Connection + CMD_HELLO = 101, // 0 Hello + CMD_DROP_CONNECTION, + CMD_RESET_REASON, + + // Device Command + CMD_SAVE_RESTART = 201, + CMD_RESET_RESTART, + CMD_SEND_HISTORY, + CMD_PAUSE, + CMD_RESUME, + CMD_RESET_SENSOR, + CMD_RESET_DISPLAY, + + // OTA Update + CMD_UPDATE_CHECK = 301, + CMD_UPDATE_AVAILABLE, + CMD_UPDATE_FORCED, + + // Config + CMD_INIT_CONFIG = 1201, + CMD_LOAD_CONFIG, + CMD_SAVE_CONFIG, + CMD_SEND_CONFIG, + CMD_RECV_CONFIG, + CMD_SEND_CONFIG_SERVER, + CMD_LOAD_CONFIG_SERVER, + + // PID + CMD_INIT_PID_PARAM = 1301, + CMD_LOAD_PID_PARAM, + CMD_SAVE_PID_PARAM, + CMD_SET_PID, + CMD_GET_PID, + + // Control + CMD_SET_CONTROL = 1401, + CMD_GET_CONTROL, + + // Operation + CMD_SET_TEMP_TARGET = 1501, + CMD_SET_TEMP_TARGET_NIGHT, + CMD_SET_HUMID_TARGET, + CMD_SET_SENSOR_OFFSET, + CMD_SET_AC1_PARAM, + CMD_SET_AC2_PARAM, + CMD_SET_MIST_PARAM, + CMD_SET_FAN_PARAM, + CMD_SET_MOTOR_PARAM, + CMD_SET_LIGHT_PARAM, + + // Status + CMD_SET_HEATER1_DUTY = 2101, + CMD_SET_HEATER2_DUTY, + CMD_SET_MIST_DUTY, + CMD_SET_FAN_DUTY, + CMD_SET_FAN_DUTY_AUTO, + CMD_SET_MOTOR_DUTY, + CMD_SET_MOTOR_DUTY_AUTO, + CMD_SET_LIGHT_DUTY, + + // Manual Operations + CMD_SET_MANUAL_HEATER1 = 2201, + CMD_SET_MANUAL_HEATER2, + CMD_SET_MANUAL_MIST, + CMD_SET_MANUAL_FAN, + CMD_SET_MANUAL_MOTOR, + CMD_SET_MANUAL_LIGHT, + + // Time + CMD_SET_TIME_HEATER1 = 2301, + CMD_SET_TIME_HEATER2, + CMD_SET_TIME_MIST, + CMD_SET_TIME_FAN, + CMD_SET_TIME_MOTOR, + CMD_SET_TIME_LIGHT, + CMD_SET_TIME_NIGHT, + + // UI + CMD_SET_WIFI_CLIENT_DISPLAY = 2401, // Graph TempHigh, TempLow, TimeScale etc. + CMD_SET_OLED_CONTRAST, + CMD_SET_OLED_DISPLAY_CHECKAC, + + // Debugging + CMD_HELLO_DEBUG = 9101, + CMD_SHOW_CODE, + COMPUTE_TIME_MAX, + CMD_INVALID = 9999 +}; + +#pragma pack(push) /* push current alignment to stack */ +#pragma pack(1) /* set alignment to 1 byte boundary */ +// TCP Packet +typedef struct TCP_PACKET_STRUCT +{ + uint16_t sig1; + uint16_t len; + uint16_t cmd; + uint16_t op; // 8 + uint32_t time; // 4 : 12 + union { + char ch[40]; + uint8_t by[40]; + int16_t n16[20]; + uint16_t u16[20]; + int32_t n32[10]; + uint32_t u32[10]; + float f[10]; + STATUS_TYPE status; + DEVICE_PARAM_TYPE device; + }; //40 : 52 + uint16_t sig2; // 2 : 54 +} TCP_PACKET; + +// UDP Packet +typedef struct UDP_PACKET_STRUCT +{ + uint16_t sig1; + uint16_t m_nSize; + uint16_t m_nMessage; + uint16_t m_nDeviceType; + uint16_t m_nResetReason; + // 10 + uint32_t m_nChipID; + uint32_t m_nDeviceID; + // 18 + union { + uint32_t m_nVersion; + uint32_t clock; + }; + // 22 + union { + uint32_t dwIPAddress; + uint8_t byIPAddress[4]; + }; + // 26 + uint16_t m_nPort; + // 28 + uint8_t m_MACAddress[20]; + // 48 + char m_sDeviceName[32]; + // 80 + union { + char m_sService[32]; + STATUS_TYPE status; // + }; + // 112 + char m_sCompanyName[32]; + // 144 + uint16_t sig2; + // Size: 146 +} UDP_PACKET; + +typedef struct { + UDP_PACKET udp; + CONFIG_TYPE con; +} UDP_CONFIG_TYPE; + +#pragma pack(pop) /* restore original alignment from stack */ + +class CWiFiHost +{ +public: + void Setup(); + void Stop(); + void Loop(unsigned long clock); + inline bool isConnected() { return m_bClientConnected; } + void CloseConnection(); + void SendHeartBeat(unsigned long clock); + void MonitorUDP(); + inline void setPublicIPPort(uint32_t ip, uint16_t port) { m_dwPublicIP = ip; m_nPublicPort = port; } + +private: + //void setupUDP(); + //void loopUDP(unsigned long clock); + //bool checkOTA(bool bForceUpdate); + + void CheckClient(unsigned long clock); + void ProcessPacket(TCP_PACKET& pkt); + void SendHeartBeat(); + int SendConfig(); + //void FillPacket(); + +private: + int SendPacket(TCP_PACKET& pkt); + size_t SendData(const uint8_t* data, size_t size); + bool SendData(unsigned long clockMills); + size_t ReceiveData(uint8_t* data, size_t size); + bool ReceiveData(unsigned long clockMills); + void Restart(); + + // Data Send/Receive Mode + char *m_pDataSend_data; + size_t m_nDataSend_size; + size_t m_nDataSend_sent; + + char *m_pDataReceive_data; + size_t m_nDataReceive_size; + size_t m_nDataReceive_received; + + bool m_bReceiveConfigPending; + bool m_bSendHistoryPending; + int16_t m_nPendingHistoryCount; + + // SocketConnection + IPAddress externalServerIP; + //int sockfd = -1; + //unsigned long connectStartTime = 0; + //bool isConnecting = false; + + //bool initiateConnection(unsigned long clock); + //bool checkConnection(unsigned long clock); + + // Operation + uint8_t m_nMode; + bool m_bHelloSent; + unsigned long m_nLastReceivedTime; + unsigned long m_nLastHeartBeatSentTime; + unsigned long m_nLastUDPBroadcastTime; + volatile bool m_bClientConnected; + uint32_t m_dwPublicIP; + uint16_t m_nPublicPort; + IPAddress m_cExternalServerIPAddress; + WiFiUDP udpLocal, udpExternal; + WiFiServer wifiServer, wifiExternal; + WiFiClient wifiClient; + UDP_PACKET packetUDP; + TCP_PACKET hostPacket; + TCP_PACKET clientPacket; +}; + +extern CWiFiHost host; +#endif \ No newline at end of file diff --git a/c_cpp_properties.json b/c_cpp_properties.json new file mode 100644 index 0000000..06b099d --- /dev/null +++ b/c_cpp_properties.json @@ -0,0 +1,39 @@ +{ + "configurations": [ + { + "name": "Arduino", + "includePath": [ + "${workspaceFolder}/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/cores/esp32/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/portable/xtensa/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/arch/xtensa/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/qio_qspi/include/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/variants/esp32/**", + + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/cores/esp8266/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/ESP8266WiFi/src/**", + "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/tools/SDK/include/**", + + "D:/Projects/libraries/ESP-TuyaBLE/src/**", + "D:/Projects/libraries/NimBLE-Arduino/src/**" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE", + "ESP32" + ], + "windowsSdkVersion": "10.0.19041.0", + "compilerPath": "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2302/bin/xtensa-esp32-elf-gcc.exe", + "cStandard": "c11", + "cppStandard": "c++17", + "intelliSenseMode": "gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/zcd.cpp b/zcd.cpp new file mode 100644 index 0000000..e33bc89 --- /dev/null +++ b/zcd.cpp @@ -0,0 +1,185 @@ +#include "HermitCrab.h" +#include "Config.h" +#include "zcd.h" +#include +#include +#include +#include + +#define TAG_ZCD "ZCD" +// Constants +#define EFFECTIVE_POWER 0.86 +#define LEADING_TIME_RATIO 0.06 + +// ESP32 Clock Constants +const uint32_t AC_CYCLE_TIME_CLOCKS = 8333; // Half cycle of 60Hz AC in clock cycles +const uint32_t EFFECTIVE_HALF_CYCLE = EFFECTIVE_POWER * AC_CYCLE_TIME_CLOCKS; // Effective half cycle in clock cycles +const uint32_t LEADING_PULSE_COUNT = EFFECTIVE_HALF_CYCLE * LEADING_TIME_RATIO; // Leading pulse count +const uint32_t MAX_PULSE_COUNT = LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE; // Maximum valid pulse count + + +volatile uint32_t dutyHeater1; // Calculated timerHeater1 count for TRIAC firing +volatile uint32_t dutyHeater2; // Calculated timerZCD count for TRIAC firing +volatile uint8_t zcdACCount; +volatile uint8_t zcdLoadCount; + +hw_timer_t *timerHeater1; +hw_timer_t *timerHeater2; + + +short getHeater1Duty() { + if (dutyHeater1 == 0) return 0; + if (dutyHeater1 >= LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE) return 10000; + return round(10000.0f * (dutyHeater1 - LEADING_PULSE_COUNT) / EFFECTIVE_HALF_CYCLE); +} + +short getHeater2Duty() { + if (dutyHeater2 == 0) return 0; + if (dutyHeater2 >= LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE) return 10000; + return round(10000.0f * (dutyHeater1 - LEADING_PULSE_COUNT) / EFFECTIVE_HALF_CYCLE); +} + +// Function to set the duty based on percentage (0 to 10000) +IRAM_ATTR void setHeater1Duty(short duty) { + if (duty <= 0) { + dutyHeater1 = 0; // If 0% duty, no pulse (turn off TRIAC) + } else if (duty >= 10000) { + // 100% duty corresponds to the leading pulse + full effective half cycle + dutyHeater1 = LEADING_PULSE_COUNT; + } else { + // Map duty to power ratio (0 to 1) + float powerRatio = (float) duty / 10000.0f; + + // Calculate the angle in radians using the inverse cosine directly + float angleRadians = acosf(1.0f - 2.0f * powerRatio); + + // Convert angle to time delay (in clock cycles) + // Normalized angle (0 to PI) maps to half-cycle (0 to EFFECTIVE_HALF_CYCLE) + uint32_t pulseCount = (angleRadians / M_PI) * EFFECTIVE_HALF_CYCLE; + + dutyHeater1 = LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE - pulseCount; + } + + uint32_t nDuty = duty * PWM_FULL / 10000; + ledcWrite(PIN_LED_HEATER1, PWM_FULL - nDuty); + ESP_LOGD(TAG_ZCD,"Set Duty: %.2f%%, Timer Count: %u clock cycles", duty, dutyHeater1); +} + +// Function to set the duty based on percentage (0 to 10000) +IRAM_ATTR void setHeater2Duty(short duty) { + if (config.bAC2_OnOff) { + if (duty >= 10000) { + digitalWrite(PIN_HEATER2, HEATER_ON); + duty = 10000; + } else { + digitalWrite(PIN_HEATER2, HEATER_OFF); + duty = 0; + } + dutyHeater2 = 0; + } else { + if (duty <= 0) { + dutyHeater2 = 0; // If 0% duty, no pulse (turn off TRIAC) + } else if (duty >= 10000) { + // 100% duty corresponds to the leading pulse + full effective half cycle + dutyHeater2 = LEADING_PULSE_COUNT; + } else { + // Map duty to power ratio (0 to 1) + float powerRatio = (float) duty / 10000.0f; + + // Calculate the angle in radians using the inverse cosine directly + float angleRadians = acosf(1.0f - 2.0f * powerRatio); + + // Convert angle to time delay (in clock cycles) + // Normalized angle (0 to PI) maps to half-cycle (0 to EFFECTIVE_HALF_CYCLE) + uint32_t pulseCount = (angleRadians / M_PI) * EFFECTIVE_HALF_CYCLE; + + dutyHeater2 = LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE - pulseCount; + } + } + + uint32_t nDuty = duty * PWM_FULL / 10000; + ledcWrite(PIN_LED_HEATER2, PWM_FULL - nDuty); + ESP_LOGD(TAG_ZCD,"Set Duty: %.2f%%, Timer Count: %u clock cycles\n", duty, dutyHeater1); +} + + +void ARDUINO_ISR_ATTR onTimer1() { + digitalWrite(PIN_HEATER1, HIGH); // Fire TRIAC + delayMicroseconds(10); // Short pulse to trigger TRIAC + digitalWrite(PIN_HEATER1, LOW); // Turn off TRIAC trigger +} + +void ARDUINO_ISR_ATTR onTimer2() { + digitalWrite(PIN_HEATER2, HIGH); // Fire TRIAC + delayMicroseconds(10); // Short pulse to trigger TRIAC + digitalWrite(PIN_HEATER2, LOW); // Turn off TRIAC trigger +} + +// Zero-Cross Detection Interrupt Service Routine +void ARDUINO_ISR_ATTR zcdACISR() { + uint32_t clock = micros(); + static uint32_t lastClock = 0l; + + if (clock - lastClock < 8000) + return; + lastClock = clock; + + zcdACCount++; + + // Heater 1 + if (dutyHeater1 == MAX_PULSE_COUNT) { + onTimer1(); + } + else if (dutyHeater1 >= LEADING_PULSE_COUNT && dutyHeater1 < MAX_PULSE_COUNT) { + // Stop the timer, configure new alarm, then explicitly start + timerStop(timerHeater1); // Stop any existing timer action + timerWrite(timerHeater1, 0); // Reset counter to 0 + timerAlarm(timerHeater1, dutyHeater1, false, 0); // Set alarm with updated duty + timerStart(timerHeater1); // Start the timer explicitly + } + // Heater 2 + if (dutyHeater2 == MAX_PULSE_COUNT) { + onTimer2(); + } + else if (dutyHeater2 >= LEADING_PULSE_COUNT && dutyHeater2 < MAX_PULSE_COUNT) { + // Stop the timer, configure new alarm, then explicitly start + timerStop(timerHeater2); // Stop any existing timer action + timerWrite(timerHeater2, 0); // Reset counter to 0 + timerAlarm(timerHeater2, dutyHeater2, false, 0); // Set alarm with updated duty + timerStart(timerHeater2); // Start the timer explicitly + } +} + +void ARDUINO_ISR_ATTR zcdLoadISR() { + zcdLoadCount++; +} + +void setupZCD() { + pinMode(PIN_ZCD_AC, INPUT); + pinMode(PIN_ZCD_LOAD, INPUT); + pinMode(PIN_HEATER1, OUTPUT); + pinMode(PIN_HEATER2, OUTPUT); + + dutyHeater1 = 0; // Calculated timerHeater1 count for TRIAC firing + dutyHeater2 = 0; // Calculated timerZCD count for TRIAC firing + zcdACCount = 0; + zcdLoadCount = 0; + timerHeater1 = NULL; + + attachInterrupt(PIN_ZCD_AC, zcdACISR, CHANGE); // Attach zero-cross detection ISR + attachInterrupt(PIN_ZCD_LOAD, zcdLoadISR, CHANGE); // Attach zero-cross detection ISR + + // Initialize and configure the timer + if ((timerHeater1 = timerBegin(1000000)) != NULL) { + timerAttachInterrupt(timerHeater1, &onTimer1); // Attach TRIAC firing routine + timerStop(timerHeater1); // Ensure timer is stopped initially + timerStart(timerHeater1); // Explicitly start the timer after setup + + } + + if ((timerHeater2 = timerBegin(1000000)) != NULL) { + timerAttachInterrupt(timerHeater2, &onTimer2); // Attach TRIAC firing routine + timerStop(timerHeater2); // Ensure timer is stopped initially + timerStart(timerHeater2); // Explicitly start the timer after setup + } +} \ No newline at end of file diff --git a/zcd.h b/zcd.h new file mode 100644 index 0000000..be8d297 --- /dev/null +++ b/zcd.h @@ -0,0 +1,13 @@ +#ifndef __ZCD_H +#define __ZCD_H +extern volatile uint8_t zcdACCount, zcdLoadCount; + +// ZCD +void setupZCD(); +void setHeater1Duty(short duty); +void setHeater2Duty(short duty); +short getHeater1Duty(); +#if defined(ESP8266) +void setMistDuty(short duty); +#endif +#endif \ No newline at end of file