From b2077c0199a697f027c29602ceca4aef3074d063 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sat, 14 Mar 2026 02:41:08 +1300 Subject: [PATCH] Improve ESP-NOW messaging and tab defaults - Use shared ESPNOW payload limit and message splitting - Expand default tab names and add flash/build artifacts. Made-with: Cursor --- build_static/app.js.gz | Bin 0 -> 24693 bytes build_static/styles.css | 37 ++++++ build_static/styles.css.gz | Bin 0 -> 270 bytes db/tab.json | 2 +- flash.sh | 244 +++++++++++++++++++++++++++++++++++++ src/controllers/preset.py | 4 +- src/main.py | 13 +- src/util/espnow_message.py | 81 ++++++++++++ 8 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 build_static/app.js.gz create mode 100644 build_static/styles.css create mode 100644 build_static/styles.css.gz create mode 100755 flash.sh diff --git a/build_static/app.js.gz b/build_static/app.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..ba8b24239257d586046cbfadbc03a410f4ed9e68 GIT binary patch literal 24693 zcmV)AK*YZviwFqDY_Vwq|6y=&E^2cC?Y-M}8^@9;_>QkA!|n}W41lC;d3#7vS`sDg zW*zrQ|XUn`Sl8bD4paHyu z8V6LNKS(}z68Pt=EUKk!RRQ=(d3J@P7>iHiYCg@E{oY=0ut)XJ%SF7p)ZHL@~h~vj8gVB1*s<#%wP>O^Y&jSv=jn8soTg_VlVvGZ90xwOaFh@5Pk&0j5;Gg;#o6>~ zl2!e8faApluQnLC7;H~cLYQX7#qu(FoNOmg#^&;E9gA0@MUHU(k>H`SrSBwOI|c?E0S*O)NWNb#Xu zK#idVEpi6vobp2e__M^nVeNr9=F{|an%Nt|fjSE<&-H&YXoMG7&qAd z!gk5+3kLrV5Lly4l2&>m0J^ebw&YLF!1OcYy}V>K;*@uT)6bN zoMvMx==Wa2l4TRb=c%HAyS-r|n(cX1qgbYS0TXAp0T7QDQ@Wm>-5yT*Jp?gABI;6a z-P%KURRE_1{`Vh`UcW+wd^E{FC1=yLs`k4yp6=toXdi7swZ}c5&`7h_ z1(-pH$uEcIHgvF5ASnFrPXW|)F33$51sCKiwj$z65@n`>>K|#D`dgtG-h}r`!M$7DG*?s(`6sw|7jkl6~Oe-@oB*T}>+0+HD=h#(1h$CPG9$5l44WB2#GeOj@ zdQY{FZS;X}0mMO~4IB6=JHSCGYDtS@(WWO!Pt@xH#q9O=0&t+^8)7hss1vUN%YK3^ z*f*aE?CQjBN2~DdesW@c#eW{1UM-h^Rc2{M^2;}$*?3po#A~WaG#v5~Hq0mcT{VDi zwE8k%PP6^)!88}wQNvFV#<{z_Uz9`RuY*d5TVSkO4zu1W% z(O6oa`|y0b-93Hwpo@jz$lo7J?g*hiT`bbuabD4X`}&yiGhLwBT?9E^p_VIM@}PyKYH5TsPdaWbot%!Q+i#e}L8M_um8GcT58b zvh+}zeC4qJz;gC*6uOMKZTE|~Dfum-=3^F64ylg5!schQ`SRA1h87txJaX^*-FcT? zdsKN$L_m@jPJ-Qf_))SVSW^+`^YW^gjJtaQLZ8m(fXN4!`E=4J7=sw~?yy}L16_WR zf@sob+wCs9lxfQAnz_O;<+Fv#_FUD;8$n5zG4cX7Tsqmb26{D53l5&@JBx()LpUfY z_CVg0od3kVm0Y(%bl6(Ohx1eJv4?>kPyYVD{sTy!SLEIeqC&O21zzhMs{EQEA;wiX zEvJ)iyz*H&Es?Txzkd1h`JcD9yJ5y#Tz&KT^!Ay!SmFyQXfR(&*3RCs(`Avs|BtE+ zI+&y&<bY%KC~U7}#XNJ@9a2Zm`6r*(NK@T00B;0x$4j#rW!m*0lu6 zfv{jr_5CMFLH&AEKyluZL|kpKEMDtPR>ue8VPP>zWR2Q(cfrU4MMnwFL(9OzolQZ+ z+hEC9yG<60H+Pol0E;43H(41m&(pNZJEc-F0&LEuxd8~0{`hs%p4s7L)iszcy;7wDRS&vs27cZrnXPd&a(M( zjaIAh=hJd>yPN#eyPw{`b&T7YpWpUB3yjiRy&Cw9SAo5u{no18^5Y;fdwJ2Ij^lZa zR(MQ0oxV7EFeqvOGd5Zg+9x~$T^LVbK!gsL$kY{ZR9=t@NOvn#LU=-h5Ch;=5v-cD zodlgdA1+EcFhKiVBeb~Gkr6>ezY!56)OMUt1~nMT#vsqOY7|~tDICQCisi{YO*ev# zz*NJ{EfsLxsKX=ANnKpi$*R-ttN%spvw9;D+(hIwsBy){?{a&(egOIE7TX?DzY%xi z`qHr8R9v0n6^70VUN^4N)FJDELj{L7`Bt~Iv%sVr7`bDjeJ_dH*SYgYc!&|8Ul8^06XWM_5*!fAGTW6Ft2Dy}vovf(JP zcsbKw)YLL!cugfw{H&?VM6sG$oVpI06ok-NDLn_i*@IG=$4nn2=KN6<;ZlVNg9GNK zjXG9VZBWLlTF$lhO7=tMIpcBHeVVWkO4L<2w}z!im#&GfdpoR6?^n*lYaM-eDQ!3K z#Mel+nfwgJ#^1!P*SRX)z7S9k2pnhW!#75|blN2GjWX4tS$_0j2zs$){N9bzN zqGc)UJM?P&>s7Y66|l?2)9JL|8=Lpm-ax;0AZu%}m++xcDXh+tRBbISKHKWKfAT*3 zWP5zVNFG2KM27(UvDd#Lh|++BnT_B&#%x5mLXr`BWiQhSn7^ndYPW1_Gq@n6A(6Lr ze!uIR$+;>gfI5=hd;GHElY6Le>ILcEI@AgGrG`X03Eg*V7=gA%&wk zcO~3a(9X^nRcH&l{#@{`u+y|ngU(&-XOsrxQ+`HTe&h^169$4(4TQm?_%Z%#nHT-u zus3iFc_K?>&7H58oXtqirW-N15ue34M>GN~WaC~1%wp_*g7p?t_O9yTVFa{2iGp z;c%zzXIvN@fLde{JV@E#Y`ZX23wQa|Xrx{blj|I9elwy;Kt(UJMV6f2s!bHO`mKiN zd^%w=DV(=obAWp_Pv+SIhntwiOT^*{`dv(ti)ne9PI-k-d78X<_x244htBabmEH*; z`|Oz5+)MB4;GXfvaa7AQi1+}S&$9Ar>8T@#({cKCNt#>kYX^ITTyTfJ(kRjzJFrIW~otAS=}$itxO1U6BN zr;6s(Z=hS`M43AnoQBbMMB8*3}LmVu{{4q=PY!9p$MRRyhq)RMeNJC5tQC5yKR zCI0c~=xqXj%z-Ibj(y1_V51QfO*m6ZLIWuqm)QbttSq^{%wZwbxKgClFgBn4`FS(- ze?Y<0a}_+jS_Kd5J0QRaHEL*|)yYz~S#QvmSJZR1UIX|z6r;=6_{Gg~0c?=$FFAZ( z31=l7OTW6B&&vfY@WmpZ43qO|x*R4cjBc7;q-VFtvdq$DHL&g@0ZI4{y>~3eJ$8~_ z5ae7xAbii_kAn*}2s(w-_TzOS`FvhSJ?vV>w7dd3`VdhNHVHwR4$J!zMhNH$qd-f0 zSY5AD&HNm?XW98i23tFEd~J@5^TBi4<8?+i#?|k~Rs5Y0jK29nnmui3{0vy{EG;;8 z(mB%g5~>F6@A2xl+TDk(}$8RT(lag6w(I6$H&T-QdZERYu)fr5MGI)}yn>lL0h zAb+r_dkLJhOE?%HR3vAYSH(xvSUV?kp5D?a-v^>1hW8<#0Esaxvhv(3XuT)dd3rTn zeh?MC^C7^hT5O-}c5#vs^^)Xty;raC8FpHl7WKGEQ7=SR{a4bSpusI_J5Q`Z(;>NF zgJJDEe)|_y>&di9xi6s4DtsW(wlYlPgQvn}vh-E37r=Y~)gHCAxhSEDvCz>3RTLpv zE3RgzAZvh}?^j?rD^I1XM3C=)mD7#-9Ph`j0UQ2=&05U(d49uP+@$s=EH%z4@h3*I)vCT?@o6cjeCQ({1tptPk`hcD;0QBaY3AybvlE5 z)>~0q@mlq4mfHo49|Sg_YeZHnmzIfIKhqAoL_;W7wj{*f{qt6)_tvbHGrhIhD%s9r zt=KTauWM>8_j&WN`s#RHBkKe!jiPo?c(-qMH4k#u@m>%$7+x*NC|Pi#VBYg;$DvhH zJTEm67RB^_67$??oRxU~3$iQ0>52MLj>(5y3;4ZP|2PP`7RGTiYqZI-DI#^9E{cB7 zKj^#2{}?x=UdXhG;o~q?IINMngA=Id&<-2|BN;UssB_byT(J(vMn|Vyrqg%gX4~HL zi(Z=>Y1=z=)vv(OX;B|5=o%4+S)8wTUTVzHq!_lL724V)v(dgxj;G$dY?&84UPqIS zaYXd=h+`XJ@s2{{`oiO#g~%^0Os*|2etEfZQz3C3OXf9&#~`vd$&j0%yh}B{-A{J* zA_rqF$!~oLaC1qp=|pcP8McuS*Kk7FL}FZDa@@oT#g`=O=jlk|u%JnI z-jcl$k5BeGb#DX2iTHx(M#oBD9ywM zC^HnM*b}_l3z5gABX^z`M=A>3J-p;FX#dF`a^fTZt)6J&BWV2$qjXZ#Dy+GqqTZ$m zF9Tpqr$&-Cy}=OxTCWsm`m4Y?p!$9QLIC6kAT-eZj#bpGqY{baz679TgnWWr)mLgE zP58fj!uhPwx$N1fZ+BFyt6A%m+m~<(pp&OeIgF3o7Bv@?e{CwjG#ZLYLZya1tMI#p zd?emyoBUqEICd49ftUbTQ=1{GhM^X(n=rmhiXIT+#cafrcsJA_S zxX5O~aA*zTd9a_tU6@e79Nd9mvH)^_`@*6CaaQ|_4H2-JR{W+6;J zZD$P?^hMUxfAYG(H1xUgde5>lmth!mSC!wM#k#3NR_loXxYKhcKpQd;J6P`!)Z$zU zoKk*M((+KcFS;_<71h#?JHtc-=7!1D`S29dU>_q{Ji-4}A-jjY0fiJ4{7*iSVQ_HL z?_t)*dz-md#LYo|xmRnDwaq*HPDG>_TE$>lB?>EtRtDdnzl1lFaqA9ybhq#quMe(4 z-t{GHsUTxP&!;BB)GAh+VLSmxtx)fw=TfQLc_9K4eDir?MH66Z5^*AMb$Y9laChe^ zz`qB3Thmo51r8gT7w3_WN!=<3#7reAx%URnzF@W}%*r3((91yiznSURe;xLFIqXuW zhx$H+XR$C%UdW?{h(;3cOe`Z0!29jvSS*XiAVRAzO-aQ0`b{W_>8ceaVIylrNe+vA znS)&VH*##9qdPicL`M^JU+8)M(`$JGUyC~KJt`9gH8C`HG z1>~9$W%xZXEQ)ZAWzsv2)Cs|EHk*haa)-Uo;XTCS&;z;%rydy-pXX!{S9)@qE>yUB zC`(POgBp@U-bFV{D7`Y;me6|eU=Dmu#&mE&lCmR6IIk?&4hTK|cZ{?j)r`wf2&lML zRCGFD(0`u`dx>DY6uR}ij!Z@03T#uC^%mU3nx5_IpLUUkYwaB49!#iTMkJxflqfdV zMyYCahYMY)8;xpZg%okFJaZ?9NcBIGzXv+-hP8-@G#KQkUO=79>a@U%R^T7 zoy!MkzWv35A>0dgdXZQMA|7xkv~ox;2?X$UCmWoP^P*xmKGe#b&@`~=j-Ob@xdNTb z)Z*6cq8h60Zgcoj@Mdhfa_8=M1w1qAAggwF1m;d4ee(-?eF#zlXUzQ;mq7sSwbm2e z`xxeDf?~XcudURA*qp!Zv-VaZ3iCOzNY&NpEMKzABd|zW4c)%0I@Ld?o2ga3!@H4s z)jPKS)ug8R#L>KlvXa@vN6+nvtBRH6Bbuj2&>}slCypkTHQ(vgOvHq8L^2j2Mus5O zC`tY6j$@cG8(Q;V2<;i23!%}!#-x5kDo}}AUM+P8>v}7|Ew#ZF9uLCR~jmP`(Pm3L8Bt znmh%rZ&pbBAh$$v(MBh1;$W`Ox}-=-;LR6FMQKN?j(k}p;eZm8B2Qw=^gIExL6y4)Q$^~%PgK@~_pMoNYm%A#4NtXovnNGv<<4g8E_ zxrjPQZQ=i)E`h;;rZ6ei($6mp^mbZ*@~u1uLiVR7_N6R8j%g+O!EiV=>#uN(uRq5y zo9RK!s$qBPbU*cnF!7Pi;)Y?wUU-WHLoR#SNYgV60?D%TKbS*XR4 z2mm+W@MqH=7IrHRUk0Y zgFYXAP*h0`0-N^q?Ym{Vyt>nNZC&T*`Bpl2w~f+ZT(sM`ibhd?oYMJ7RI~T%E7jK3 zM#_}XFov;UKrAUpHj2jbe02H!US0ald=E8csv^|ZHov^9^kKe$Yx&uIw6nJ6MCPp_6` z9hk$n$Hll6xlU`=^=<|ZAyys?c2o@{<}Q#CI0Wo^xST<3m2jD;JF zdVK{oU3CV4wHH1s+wTeV7us+jj*IMHukr z%r&doqRzIWi=9Esu_>179vpncR>b(&td>qlTEPzrTn69$uax``&h zZE4d3(L{|*{JM8KfL%&=Q?>$d@9(ze8VnN7foNWri-xwUmfv2o1G{Un6f$>>g0j-V zfp87g^v0@Fy!O10%&4-q{892_Bbfq*ME5nr7?f}%(q%HuQuzBvoFc|^1p?8EaD}F# z8L!XWXV5C+!M)uw3h!+nf3NwYWCz#P`~EoD*=a!_BvY^FFJ0>ZCZ3QavvOK4KFrf8K+o>{c&0)S5wsTOrP%pMR(Exh@^hm~(gxz`CC}RF zBW)Z*D-R|w762QHb(jes zuo3~NZO0HbjK^$Rb79e)L(rvPBpq5YR#ivHcEqo>D|AuoywDLsao zL|8o8n&`{ff}ZB@q@wkjE`XM6AZmN#H;TRMe6qaUO@8=(ezV7(g3%P){d!7)M@Nfv zlA{LjN2@G7`-pjk3Q0P4Nf`-$tlajy$%FY#QkBzul6?JrivKd+;A&A}V|u^h;Zt#$ zN7W^;7$2?BR;ML?PABCxd?v^+1Yr4qO5qYb_Rd*sNwwnLRz}Y~dU};Lp?FBaA&BEad+=Se{ zU{eRZex|Z;Re?3o1SN4OB)3w95y9s3* zVq?2Jsxv<;c9h$(^HSyW1`Np9&kO5@&E~aP*lepwh)qy+pXjJ{q)W0*U|Cpb#$oY= z^Glu69OxW|=S`d5hRP!A1?BFqLiJUQbIbN5%q*#+&sd>;v&2tEnAtgINfK>7q1)k%8`{u~S#v}&$#EsU{(EBR(R*FwrgnhdjK3#>AWRfqkGkm;6A6S7}@#Es) z9!Ez@xePCwS9%LV6fr~qxEEIZu9av0chkvzmU7%~VwOMp zo+9RK&B&u0HJ7+_QliG&6bVX@Sh@z)cd1rH{fA}hV{;lH*a)XXPpmv?Y zmHGi?oiW1o@^LdB>bHC|b5by=D{j|YH~Xfj*?&Iob#@=y@g?@SxO&1WV-jhrMg>Wr zZ;<`JkDK;4eBE64Alj2$XDjH1u;v(Q=F6k=W^8-+dfo65`PYpfkC;^-_ac`J8jdIp za6sYK!bpY5R(((KB0cXK@I&?!0-mMltPzLxg!IeknIDDVtd+Hj8N7a;l?wo zT#fU`y&y}0_sR9Itp)$VEC|Jl+7qjC1`Feu) zREo}fJs2gVMZ-e-#y!$@!ZB}ZLXEgo^Y?jue&@Q5S9H9jqv z4CklIqU*mXG#Zr~0Q7hh0@2Q>+yK(=Hz8m$Wl}Q~$`Q^0AmNnkYlE>-7vF36n+OLZ zm>~(Nc~UY4i-IO6)v?o)Rs4m;T6g}1ClYd=R~pDI2kFq;t{{e8PH$!PbqH>x2T5_Q zPFDV_jBG9Pwurxni7#?^-BBO>v8LOV(|{uhZ5Mf_0+M&a(a04fMwCoDeI_wJnyM-X z@?QT0Ze^t@i%ROAIwy>2KGI0<;RX@7d7o4^SOqaAm z^)1Y7`{veR`(|F-IEKkTkJo*5>rV1d-RP@Zm%qAoowtyN2-01&95gsw(E+1-6kDu zU5+GIgzFj!h(@axzB>r3MtKNE`(E|Js-jFvVdhb8qs8x2zw}p^Let3?u}Rc7V}Cc)*g^FJEDBa!7u99_ zv9wZ8NbAu#t$@9{xX3E;LVEJeX9bTFCnqIB30FwJ`v%oyvO_Y%hJk8Wj)Foo-516% zu48aR>X$`6o6+DT`K1|o0j5Oe8d2BoK%&rFHdMgW0y&n`Ox~;ARyLRSL|tglH>ECJ zu3+khsLZf61&XSlgjKy-`PJZykS$trm?%Vn^vC*CDqVFnn|DBcP$DByLk{003+uuvzKD^xD+Dm5X%_zOd_P4uxYh15> zVh$DPM&Oz9*zJbb&}q|;P9xrrl$Y1PB#|&8{v>h`vfypk{=Z1K*FgxU0mUV+o9cJh`m4&Nr#txC4{6a7c^6&nM=ti$` zHzbR7pPYEln+jH2j^#esgE(XG{KcymM=$A2e!m8chL>Hq$>bjhagX2ep1JG+DB0gCUvuVy6N(56mCja^$0y zE|Wf{bu$`5!O0HD-wcPOYEf2IUokoe88}ud;EDbXq9uOV3)x+Ac9UB9y|}h?wj#bx z3%}0uGT|4~l|~3h~6qxoe}Bk$zan^KT&s0h*g z1QDM5RARQ2*TMN+5!gFiEc@;OGsgsoNASK5o

Y)xdi_2%cAq98%1^ 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 UsWxKkSRZHRH|9V;(;Wf;0F(!R#Q*>R literal 0 HcmV?d00001 diff --git a/db/tab.json b/db/tab.json index aa03731..ffdc6a2 100644 --- a/db/tab.json +++ b/db/tab.json @@ -2,7 +2,7 @@ "1": { "name": "default", "names": [ - "1","2","3","4","5","6","7","8" + "a","b","c","d","e","f","g","h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" ], "presets": [ [ diff --git a/flash.sh b/flash.sh new file mode 100755 index 0000000..4025284 --- /dev/null +++ b/flash.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env sh +set -eu + +# Environment variables: +# PORT - serial port (default: /dev/ttyUSB0) +# BAUD - baud rate (default: 460800) +# FIRMWARE - local path to firmware .bin +# FW_URL - URL to download firmware if FIRMWARE not provided or missing + +PORT=${PORT:-} +BAUD=${BAUD:-460800} +CHIP=${CHIP:-esp32} # esp32 | esp32c3 + +# Map chip-specific settings +ESPT_CHIP="$CHIP" +FLASH_OFFSET=0x1000 +DEFAULT_DOWNLOAD_PAGE="https://micropython.org/download/ESP32/" +BOARD_ID="ESP32_GENERIC" +BOARD_PAGE="https://micropython.org/download/${BOARD_ID}/" +case "$CHIP" in + esp32c3) + ESPT_CHIP="esp32c3" + FLASH_OFFSET=0x0 + DEFAULT_DOWNLOAD_PAGE="https://micropython.org/download/ESP32C3/" + BOARD_ID="ESP32_GENERIC_C3" + BOARD_PAGE="https://micropython.org/download/${BOARD_ID}/" + ;; + esp32) + ESPT_CHIP="esp32" + FLASH_OFFSET=0x1000 + DEFAULT_DOWNLOAD_PAGE="https://micropython.org/download/ESP32/" + BOARD_ID="ESP32_GENERIC" + BOARD_PAGE="https://micropython.org/download/${BOARD_ID}/" + ;; + *) + echo "Unsupported CHIP: $CHIP (supported: esp32, esp32c3)" >&2 + exit 1 + ;; +esac + +# Download-only mode: fetch the appropriate firmware and exit +if [ -n "${DOWNLOAD_ONLY:-}" ]; then + # Prefer resolving latest if nothing provided + if [ -z "${FIRMWARE:-}" ] && [ -z "${FW_URL:-}" ]; then + LATEST=1 + fi + if ! resolve_firmware; then + echo "Failed to resolve firmware for CHIP=$CHIP" >&2 + exit 1 + fi + echo "$FIRMWARE" + exit 0 +fi + +# Helper: resolve the latest firmware URL for a given board pattern with multiple fallbacks +resolve_latest_url() { + board_pattern="$1" # e.g., ESP32_GENERIC_C3-.*\.bin + # Candidate pages to try in order + pages="${BOARD_PAGE} ${DOWNLOAD_PAGE:-$DEFAULT_DOWNLOAD_PAGE} https://micropython.org/download/ https://micropython.org/resources/firmware/" + for page in $pages; do + echo "Trying to resolve latest from $page" >&2 + html=$(curl -fsSL -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' -e 'https://micropython.org/download/' "$page" || true) + [ -z "$html" ] && continue + # Prefer matching the board pattern + url=$(printf "%s" "$html" \ + | sed -n 's/.*href=\"\([^\"]*\.bin\)\".*/\1/p' \ + | grep -E "$board_pattern" \ + | head -n1) + if [ -n "$url" ]; then + case "$url" in + http*) echo "$url"; return 0 ;; + /*) echo "https://micropython.org$url"; return 0 ;; + *) echo "$page$url"; return 0 ;; + esac + fi + done + return 1 +} + +# If LATEST is set and neither FIRMWARE nor FW_URL are provided, auto-detect latest URL +if [ -n "${LATEST:-}" ] && [ -z "${FIRMWARE:-}" ] && [ -z "${FW_URL:-}" ]; then + # Default board identifiers for each chip + case "$CHIP" in + esp32c3) BOARD_ID="ESP32_GENERIC_C3" ;; + esp32) BOARD_ID="ESP32_GENERIC" ;; + *) BOARD_ID="ESP32_GENERIC" ;; + esac + pattern="${BOARD_ID}-.*\\.bin" + echo "Resolving latest firmware for $BOARD_ID" + if FW_URL=$(resolve_latest_url "$pattern"); then + export FW_URL + echo "Latest firmware resolved to: $FW_URL" + else + echo "Failed to resolve latest firmware for pattern $pattern" >&2 + exit 1 + fi +fi + +# Resolve firmware path, downloading if needed +resolve_firmware() { + if [ -z "${FIRMWARE:-}" ]; then + if [ -n "${FW_URL:-}" ] || [ -n "${LATEST:-}" ]; then + # If FW_URL still unset, resolve latest using board-specific pattern + if [ -z "${FW_URL:-}" ]; then + case "$CHIP" in + esp32c3) BOARD_ID="ESP32_GENERIC_C3" ;; + esp32) BOARD_ID="ESP32_GENERIC" ;; + *) BOARD_ID="ESP32_GENERIC" ;; + esac + pattern="${BOARD_ID}-.*\\.bin" + echo "Resolving latest firmware for $BOARD_ID" + if ! FW_URL=$(resolve_latest_url "$pattern"); then + echo "Failed to resolve latest firmware for pattern $pattern" >&2 + exit 1 + fi + fi + mkdir -p .cache + FIRMWARE=".cache/$(basename "$FW_URL")" + if [ ! -f "$FIRMWARE" ]; then + echo "Downloading firmware from $FW_URL to $FIRMWARE" + curl -L --fail -o "$FIRMWARE" "$FW_URL" + else + echo "Firmware already downloaded at $FIRMWARE" + fi + else + # Default fallback: fetch latest using board-specific pattern + case "$CHIP" in + esp32c3) BOARD_ID="ESP32_GENERIC_C3" ;; + esp32) BOARD_ID="ESP32_GENERIC" ;; + *) BOARD_ID="ESP32_GENERIC" ;; + esac + pattern="${BOARD_ID}-.*\\.bin" + echo "No FIRMWARE or FW_URL specified. Auto-fetching latest for $BOARD_ID" + if ! FW_URL=$(resolve_latest_url "$pattern"); then + echo "Failed to resolve latest firmware for pattern $pattern" >&2 + exit 1 + fi + mkdir -p .cache + FIRMWARE=".cache/$(basename "$FW_URL")" + if [ ! -f "$FIRMWARE" ]; then + echo "Downloading firmware from $FW_URL to $FIRMWARE" + curl -L --fail -o "$FIRMWARE" "$FW_URL" + else + echo "Firmware already downloaded at $FIRMWARE" + fi + fi + else + if [ ! -f "$FIRMWARE" ]; then + if [ -n "${FW_URL:-}" ]; then + mkdir -p "$(dirname "$FIRMWARE")" + echo "Firmware not found at $FIRMWARE. Downloading from $FW_URL" + curl -L --fail -o "$FIRMWARE" "$FW_URL" + else + echo "Firmware file not found: $FIRMWARE. Provide FW_URL to download automatically." >&2 + exit 1 + fi + fi + fi +} + +# Auto-detect PORT if not specified +if [ -z "$PORT" ]; then + candidates="$(ls /dev/tty/ACM* /dev/tty/USB* 2>/dev/null || true)" + # Some systems expose without /dev/tty/ prefix patterns; try common Linux paths + [ -z "$candidates" ] && candidates="$(ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null || true)" + # Prefer ACM (often for C3) then USB + PORT=$(printf "%s\n" $candidates | grep -E "/dev/ttyACM[0-9]+" | head -n1 || true) + [ -z "$PORT" ] && PORT=$(printf "%s\n" $candidates | grep -E "/dev/ttyUSB[0-9]+" | head -n1 || true) + if [ -z "$PORT" ]; then + echo "No serial port detected. Connect the board and set PORT=/dev/ttyACM0 (or /dev/ttyUSB0)." >&2 + exit 1 + fi + echo "Auto-detected PORT=$PORT" +fi + +# Preflight: ensure port exists +if [ ! -e "$PORT" ]; then + echo "Port $PORT does not exist. Detected candidates:" >&2 + ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null || true + exit 1 +fi + +ESPL="python -m esptool" + +detect_chip() { + # Try to detect actual connected chip using esptool and override if needed + out=$($ESPL --port "$PORT" --baud "$BAUD" chip_id 2>&1 || true) + case "$out" in + *"ESP32-C3"*) DETECTED_CHIP=esp32c3 ;; + *"ESP32"*) DETECTED_CHIP=esp32 ;; + *) DETECTED_CHIP="" ;; + esac + if [ -n "$DETECTED_CHIP" ] && [ "$DETECTED_CHIP" != "$ESPT_CHIP" ]; then + echo "Detected chip $DETECTED_CHIP differs from requested $ESPT_CHIP. Using detected chip." + ESPT_CHIP="$DETECTED_CHIP" + case "$ESPT_CHIP" in + esp32c3) FLASH_OFFSET=0x0 ;; + esp32) FLASH_OFFSET=0x1000 ;; + esac + fi +} + +detect_chip + +# Now that we know the actual chip, resolve the correct firmware for it +resolve_firmware + +# Validate firmware matches detected chip; if not, auto-correct by fetching the right image +EXPECTED_BOARD_ID="ESP32_GENERIC" +case "$ESPT_CHIP" in + esp32c3) EXPECTED_BOARD_ID="ESP32_GENERIC_C3" ;; + esp32) EXPECTED_BOARD_ID="ESP32_GENERIC" ;; + +esac + +FW_BASENAME="$(basename "$FIRMWARE")" +case "$FW_BASENAME" in + ${EXPECTED_BOARD_ID}-*.bin) : ;; # ok + *) + echo "Firmware $FW_BASENAME does not match detected chip ($ESPT_CHIP). Fetching correct image for $EXPECTED_BOARD_ID..." + pattern="${EXPECTED_BOARD_ID}-.*\\.bin" + if ! FW_URL=$(resolve_latest_url "$pattern"); then + echo "Failed to resolve a firmware matching $EXPECTED_BOARD_ID" >&2 + exit 1 + fi + mkdir -p .cache + FIRMWARE=".cache/$(basename "$FW_URL")" + if [ ! -f "$FIRMWARE" ]; then + echo "Downloading firmware from $FW_URL to $FIRMWARE" + curl -L --fail -o "$FIRMWARE" "$FW_URL" + else + echo "Firmware already downloaded at $FIRMWARE" + fi + ;; +esac + +$ESPL --chip "$ESPT_CHIP" --port "$PORT" --baud "$BAUD" erase_flash + +echo "Writing firmware $FIRMWARE to $FLASH_OFFSET..." +$ESPL --chip "$ESPT_CHIP" --port "$PORT" --baud "$BAUD" write_flash -z "$FLASH_OFFSET" "$FIRMWARE" + +echo "Done." + + diff --git a/src/controllers/preset.py b/src/controllers/preset.py index 059b83e..51e576f 100644 --- a/src/controllers/preset.py +++ b/src/controllers/preset.py @@ -3,7 +3,7 @@ from microdot.session import with_session from models.preset import Preset from models.profile import Profile from models.espnow import ESPNow -from util.espnow_message import build_message, build_preset_dict +from util.espnow_message import build_message, build_preset_dict, ESPNOW_MAX_PAYLOAD_BYTES import asyncio import json @@ -161,7 +161,7 @@ async def send_presets(request, session): msg = build_message(presets=chunk_presets, save=save_flag, default=default_id) await esp.send(msg) - MAX_BYTES = 240 + MAX_BYTES = ESPNOW_MAX_PAYLOAD_BYTES SEND_DELAY_MS = 100 entries = list(presets_by_name.items()) total_presets = len(entries) diff --git a/src/main.py b/src/main.py index f6d95d6..18e2e4c 100644 --- a/src/main.py +++ b/src/main.py @@ -19,6 +19,7 @@ import controllers.scene as scene import controllers.pattern as pattern import controllers.settings as settings_controller from models.espnow import ESPNow +from util.espnow_message import split_espnow_message async def main(port=80): @@ -98,9 +99,17 @@ async def main(port=80): except Exception: print("WS received raw:", data) - # Forward raw JSON payload over ESPNow to configured peers + # Forward JSON over ESPNow; split into multiple frames if > 250 bytes try: - await esp.send(data) + try: + parsed = json.loads(data) + chunks = split_espnow_message(parsed) + except (json.JSONDecodeError, ValueError): + chunks = [data] + for i, chunk in enumerate(chunks): + if i > 0: + await asyncio.sleep_ms(100) + await esp.send(chunk) except Exception: try: await ws.send(json.dumps({"error": "ESP-NOW send failed"})) diff --git a/src/util/espnow_message.py b/src/util/espnow_message.py index a377e2b..86cc09b 100644 --- a/src/util/espnow_message.py +++ b/src/util/espnow_message.py @@ -2,10 +2,15 @@ ESPNow message builder utility for LED driver communication. This module provides utilities to build ESPNow messages according to the API specification. +ESPNow has a 250-byte payload limit; messages larger than that must be split into multiple +frames. """ import json +# ESPNow payload limit (bytes). Messages larger than this must be split. +ESPNOW_MAX_PAYLOAD_BYTES = 240 + def build_message(presets=None, select=None, save=False, default=None): """ @@ -54,6 +59,82 @@ def build_message(presets=None, select=None, save=False, default=None): return json.dumps(message) +def split_espnow_message(msg_dict, max_bytes=None): + """ + Split a message dict into one or more JSON strings each within ESPNow payload limit. + If the message fits in max_bytes, returns a single-element list. Otherwise splits + "select" and/or "presets" into multiple messages (other keys like v, b, default, save + are included only in the first message). + + Args: + msg_dict: Full message as a dict (e.g. from json.loads). + max_bytes: Max payload size in bytes (default ESPNOW_MAX_PAYLOAD_BYTES). + + Returns: + List of JSON strings, each <= max_bytes, to send in order. + """ + if max_bytes is None: + max_bytes = ESPNOW_MAX_PAYLOAD_BYTES + + single = json.dumps(msg_dict) + if len(single) <= max_bytes: + return [single] + + # Keys to attach only to the first message we emit + first_only = {k: msg_dict[k] for k in ("b", "default", "save") if k in msg_dict} + out = [] + + def emit(chunk_dict, is_first): + m = {"v": msg_dict.get("v", "1")} + if is_first and first_only: + m.update(first_only) + m.update(chunk_dict) + s = json.dumps(m) + if len(s) > max_bytes: + raise ValueError(f"Chunk still too large ({len(s)} > {max_bytes})") + out.append(s) + + def chunk_dict(key, items_dict): + if not items_dict: + return + items = list(items_dict.items()) + i = 0 + first = True + while i < len(items): + chunk = {} + while i < len(items): + k, v = items[i] + trial = dict(chunk) + trial[k] = v + trial_msg = {"v": msg_dict.get("v", "1"), key: trial} + if first_only and first: + trial_msg.update(first_only) + if len(json.dumps(trial_msg)) <= max_bytes: + chunk[k] = v + i += 1 + else: + if not chunk: + # Single entry too large; send as-is and hope receiver accepts + chunk[k] = v + i += 1 + break + if chunk: + emit({key: chunk}, first) + first = False + if not chunk: + break + + if "select" in msg_dict: + chunk_dict("select", msg_dict["select"]) + if "presets" in msg_dict: + chunk_dict("presets", msg_dict["presets"]) + + if not out: + # Fallback: emit one message even if over limit (receiver may reject) + out = [single] + return out + + def build_select_message(device_name, preset_name, step=None): """ Build a select message for a single device.