From 7f6eeab39e4d35ba970344cc6e927e2a8a1cb923 Mon Sep 17 00:00:00 2001 From: matze19999 Date: Tue, 6 Oct 2026 22:01:38 +0200 Subject: [PATCH] Release 0.12.0 --- .clang-format | 7 + HyperDRM.manifest.json | 8 +- ..._0.11.0_arm.ipk => HyperDRM_0.12.0_arm.ipk | Bin 1203394 -> 1204332 bytes README.md | 19 +- VERSION | 1 + app/app.js | 445 +++++++++--- app/appinfo.json | 15 +- app/calibration.js | 110 ++- app/navigation.js | 84 ++- app/remote.js | 75 +- app/wizard.js | 285 +++++--- build_manifest.py | 2 + build_package.py | 8 +- docs/architecture.md | 46 ++ docs/publishing-review.md | 21 + docs/release-0.12.0.md | 11 + native/autostart.h | 80 +- native/blackbars.h | 33 +- native/calibration.h | 208 ++++-- native/capture_loop.h | 150 ++++ native/color_boost.h | 5 +- native/continuity.h | 95 ++- native/diagnostics.h | 239 ++++-- native/handover.h | 360 ++++++--- native/histogram.h | 18 +- native/interpolation.h | 8 +- native/layout.h | 120 +-- native/layout_refresh.h | 71 +- native/playback.h | 112 ++- native/receiver_stream.h | 274 +++++-- native/retry.h | 60 +- native/runtime_states.h | 12 + native/service.c | 683 +++++++++--------- native/service_api.h | 202 ++++++ native/service_calibration.h | 107 +++ native/service_status.h | 111 +++ native/source_bridge.h | 145 +++- native/spatial_image.h | 58 +- native/target.h | 254 +++++-- native/test_receiver_stream.c | 135 +++- native/version.h | 4 + package/packageinfo.json | 11 +- release.py | 99 +++ service/histogram-service | Bin 81868 -> 81868 bytes sync_version.py | 33 + tests/root-repair.test.js | 3 + tests/test_version.py | 50 ++ tests/wizard.test.js | 6 +- 48 files changed, 3617 insertions(+), 1266 deletions(-) create mode 100644 .clang-format rename HyperDRM_0.11.0_arm.ipk => HyperDRM_0.12.0_arm.ipk (90%) create mode 100644 VERSION create mode 100644 docs/architecture.md create mode 100644 docs/release-0.12.0.md create mode 100644 native/capture_loop.h create mode 100644 native/runtime_states.h create mode 100644 native/service_api.h create mode 100644 native/service_calibration.h create mode 100644 native/service_status.h create mode 100644 native/version.h create mode 100644 release.py create mode 100644 sync_version.py create mode 100644 tests/test_version.py diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..0f53e92 --- /dev/null +++ b/.clang-format @@ -0,0 +1,7 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +ColumnLimit: 100 +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +SortIncludes: Never diff --git a/HyperDRM.manifest.json b/HyperDRM.manifest.json index 2180f76..c4480df 100644 --- a/HyperDRM.manifest.json +++ b/HyperDRM.manifest.json @@ -1,15 +1,15 @@ { "id": "org.webosbrew.histogramhyperion", - "version": "0.11.0", + "version": "0.12.0", "type": "web", "title": "HyperDRM", "appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR", "iconUri": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/app/icon.png", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM", "rootRequired": true, - "ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.11.0_arm.ipk", + "ipkUrl": "https://gitea.adnau.de/matze19999/HyperDRM/raw/branch/main/HyperDRM_0.12.0_arm.ipk", "ipkHash": { - "sha256": "4e2be7de5f6e06cca4a389d17b18c770aa47ef2d862aac651c16ed02f231d711" + "sha256": "b4fcc609d763b8e1a72d84484a5d454627416dde0ef51ef65ab705173e3c0810" }, - "ipkSize": 1203394 + "ipkSize": 1204332 } diff --git a/HyperDRM_0.11.0_arm.ipk b/HyperDRM_0.12.0_arm.ipk similarity index 90% rename from HyperDRM_0.11.0_arm.ipk rename to HyperDRM_0.12.0_arm.ipk index 098b57fd0a803e892732e2134b5213b1f6c1998f..4c7cbc0ee0ad75c9e8dbd2e7e0e021454b71d9a0 100644 GIT binary patch delta 107864 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�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

V-W9>@c_0Q9}7zaoth3vH`htJXD22USt* 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 QXUD$*t^QC2V*vOT0PB2$lK=n! delta 106924 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

r1LRP68{6v-vU)QHfv!M8nd9@KXvjbBO8`QTJM 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

xKvDyK zwi5OpdE$q%4$&tu+#V<&g6gC+6SO5gV8_9o<3RRm5c6$MO0&@BHrDBaGV$Uj$|7%1 zZK+Qz=_!h15F^q)j|LvmyJVaQ4>h;hf~rK~zQyk7q!Jc?4?F4|!u96wGR>ofLa7F} zg2oR9z2ji7=?o+RHrTHBZe3Qe@o7!ZpiP(>vaOcSgHRtpwqI0|zq9IlwiggPxIrdc z{2}}AjghBalKsVFoMtt^HUFe+hw_sOZM5%v@5wlI1+Mwrzi0xuANrGxaTAMdG~WBb zy}We^lTDX@Xy}$+pixwT)s@Of@2ifpc;7`dyCS~xLmvf_Z<%o4W6{k2|pjURgbj*kUNoq+U&M7gv&1Mg}3E*Sg?)DJ|=AT zZiOh%U(@=yRY_+x{5t@)xVp`8HoiM#Bwm!q6q1UheDv=p%J@>99sVNVj7|=8IQM%! z9Tt>-mogvFryTo1xFnHnA)P+eatZL~nV(?bWgl*f$rk8^V-jqGf0A-N;=%Qjpj>XH z?_NsshgV<^Y_e6iKSr>tEI!~qGW-jx9zHeTZnPgr#;QH`TPaSYws)_}KI&TJbWZA% zRw^ZeN3^$d^{b@|^C3gRxC%NeaUnked+7v!@Y=)nJ&W(xl}Dau`;X3`8a*}7mM6@! z-TY0a?@7S+7!gP7msuKR&S!Ez81!g_x;Nl&Zcasfp_k7MTy*nnw2z3MbXG$6R?j|e zeL56zD)~Bq*9gb0#3w!K$*y^+%q+HVjy4m!$m_0-KW?o?yJ{E65XIHtc~jKe$F6aI zJ#LNS<2M4_K2EjUajVGT3cxR5|J1nEk1?zx@W@AOn{W185Lsk7*?PbABO4R%JFJBF`8EV~ak71P1uwjc z&3@jPC!O{WXotObO`)F;-+P>u`Zt1qPjW_>XSJ`_pN~yG7TE(&jQuIbRG2ZQfzI|8 zSqQcpFz?srSrpVif1Xv*W}b!U@)gDs#3DxTk3;I|MfO{vH`lFUX9(jH+dFoAiQjZq zbF)MJ`s}csk@EW9q<(xXdw=_wmJavRC6D)e#epuKs*8wYzJM;@Ad$ZEL>wxA;M?v8 zQNQLGD}nc&4*vGy<@p$=5_N9EVs=iN$j^vG_#__OHiMQGU;S36Gg=;q{7OM`fyJF`8#=Bu7l7<0&mNpUk)d)lFw~AuLLfmt>mYFuU`TG2DB5*%-?3Z zKLB(N)EBY+k?x=ab_EMndakEpv8OUd0SEMDyo8_XUe-`gjC1I_4Sjp@zTxQWtuD08 z9l6+|z?!O)nD4@LKH)wNzLFVnC!H7gmu-;D=0!F|*<#ys!i0*i0Rw)va?jP3)sg%h zDy2ZG#QyEe&&;EL;8Uc74mKZi#hMyfUOvL!dk(U40C58O8fKIkZAlie29hINpt)ij z+HbaETyegQ?$R55wA7Y1yHpAQPh9pawsnS|7G^H8sfvqifq;qdSp?Nwi!0=UWX(Im}-72-wUu}=~ngMCpdY;7K!ZFNE1S*voj`CB$fy+jvV+qQG8x#dN+ zK7e^zmfA$c5{p7vWN{ITEUu~|OM;-5qLil~gQu})&Vc{V!_FOmFM0s@9ssQmzy~k1 zeGVK*u0-{J2FVH>+rWm-lJCl}_xEG%`&oQQymnFU2QBwYc6p>Y$5B2*&MSoc?SgQ% zOc2C(1VI5}-P$b(N%$Uyr2m#4HCp-z`D5b}>9LZK7H5Wk8Mi|a0^Svbyh_=ZAzhN- zRw`|VzEP;;`w_bH>;<00flmQk4(N`=exEc7umE0vrdX5k8V;DJfBl^i!3F_V<8<#6 z4_Kwsy-#nzF8JB+FXcT&@Mypf_}T9|<@=y6OwUaKy~g{|o(W_z4Ba6=>(_$aS#Dr{ zmLNRW;7-}qZ1z={>0bFc=0B2+k47w(W-gL4kjQ5_hWw{){5a`uGq|Z5(r$=Am{S^n%UZu-w$2K9!e87NcOQSAqTe}Lt8<<7s?R% zeH7}fXq&;?y5svY+R}FviyPIpM9dGHf1Cf3G(;EN0R6^(ug<)8)TipCy^vfl~m)wZUf zMbCP=w9JQHNPu1@+d$)m(Ee4HT=f?>u)81~=-Gp>2R=zlM>?Jcu4>E?k9-I8s}5xv z+l9wV_HS2wS7YtOPaVI%BKdUj-B*G52C?M^cDCmUxu_BMS;*fN0pASWc0nM$XsLLA zH$?V}zQs689N?Qc9CU1XL2}=eU)Y2TXg5Bt$X0`Yx(n#n3iV~Y-izv6%ljEi``lHO^EqvSMp4v`CTk^Z| zpo?@!CL}87BP~EG=BXMw>ll)7QX$mV-gsH|l`MVe+;KcVt8i15z$dLg7o{_QM*I22 z@3pnNqj~ub@D1VTod<5$pI!V+XW^hjTNS&vtEdh-z|JWL9ajj&+c0Ba`L4kg`ojgb z{|fvBTZH}Y!fd^ZvHW2d>F&XP)=KyFqtM1jm49^u>T59X_>qOu#Nr51>oaJJ}tX{*D90_kiX0CLz2+nEonZZXZ3734&FmPi|PsZG*3wD zEwqovSTZ6D$;Ym6Q6039ZPnr14dXTwKB?S$q7Zw0JNEc%Y@a)hwasCy#D8n7Pbw*Q z#h9(5$tPKI^?l_r#LvehI@{ZMjNZVC!Y(L->aF1OR+*-SPW)r%@!W2I5l$IcdyrTn zsgxyBKf2pVQrm75uN=>&^i&?ld~WJX{%iDY**BvxhB)w<`cnN%fFZuAh|?EJ2{Qk$ zE%*X{w!4g1NZ(S4opS=W0LJmD~Wao5U`5OV|>!{iJ zh<6a$cHz3PIdt1uCA~d=DUsy(r^x2+@b;uV#bf!hAKRAq^{hKnX7>QlZ}qTYSVygV z&bp3d+&&haY0Cbpd{*g6`>yZa%HF@%UUTfPrhBVE**~N6y4lSNZSfe>aV!-ds|qxt zJ4SbYcW;01QUUn(vxsHD)4OlNS3t@|%Ge|bC1(_jo^!5%e%LpEL?^^V)=VDr#nF8M zd|^-MB{zN#pvL@4_)$JYf8g=jXP+_K=n1-fdBbe&_d}Lt!DnV`K^wrzCt9|4nqzep zKeLbz78Wtb>a&mF>n$#-O_l=0G~0U@SeF3SR9tWQvd<<9?O6l%S{Ywgl4=EP@Cv$* zwLVUMI)|_2ai)QPVqWl`&XX2!7{v|wbgrtBV@oYaP!w%b|&J%&uFb!>zpf~ zKV(tO$NnBP8^p&Vy2d%2k^l0mOq*TMRU1)OF3YxVonyBC3+?C*_fm* z_I4chK47AM9cGsMzQfk_EO4FR2im(rF8ticZZTUu4)M)wSMn3(d{hPEX}o{KP;ccY{vM+Gk5OSxy8A&w`n9}-MK#S<#(HIVt?iXHsSyQe&A*ZTu5Bi`dUyQRqY zHNqT!S$@+;KwnZ^pe(GDUw3hkWrTzL!gsz%^RaWS;sf7%{A$0$CHb9OC%>bcyJ!IC6>;A%#ZW6@^q~}O(qaEb7gxxD|fb0%rvK!+dw}pNE zCE5>-`1xYAgWS5=;~dyW*kC_59=p)|U)_@JwxbMO_*@}eg-j-WF3ay8vmoq3Dn~kh zjC2g?ERz0p$n)#+_py!g{m|W{Qc20r-RVw^*`Np9tI;{=S+1MycWX+zYlz!hIkl$# zZtYixyR~2Fe1zY5-hkZL@3GBT!xHi_EgP7hc{Q?_y#;YaJ8xmb51{oTJ)etgX9mjj zHg6^D-2OhWZ#_~ivti$swjrA}%JS}i@g-J8`7sOGwKgY~u(O7y;tGq8a-(Ioui5(g z)3dE~@9hfM{yrNmqv0n7@bMADUAqe3YY^zM{WFCShMa6jz(#q0SI3u1saNFtvq16- z5DziBU@sJdCULMAbk9zHEYXDSjjr9Sts=j~{w|Hq0JGr7kS!bAyTG=&g3f(^3fP&= zOMr{W$#3lN(^6Xq^hZ=#ku5+?cT8kcR#r9=<|Dm^w79Y}y_YZ}-c+$RHgCu-$zE&d z5%QTxKYQ|b!t6~i^8?itBKc&p-(-6VWxK!hw3EL?`?D7Kdq=+dJ&l8y(W(&hZT^DL zK@e6+|3sYv{)oc72>g1EVmw!Wh060Wb`PVTYirDXGImB~FK<;tqQGaXj3GaaJX#M^m2Ay&P~Y1g`lU z*O!3n0mM4x`xHXrN6@QB6+&UDAly2l5Kbcbpd5u<4_}hreVGn_%RaoCzt!P!PenR# zC3{5voKU_&T4CLXIBtXVS%lfzH#Ehvq#u0r$LOt0qGkJ2v#qYmeHI1uRZy$hR=2(c zFR=_trgw9jETo^wSKka@eO%Ldw!*%2FIocInQ!}S*(QrC=ql0~G+=^it1V!*{96*G z>srY(et|?X=F??=ft1&Op7arDq@p`z;CiUp0-F-`{zw7whou%szQCJ&hckT^(C-1P z&j3~yx1a9#cDq5Q>8!6e+U&BwMFcM|wJwmh^H?>}Ro;)Z2gi4jV>|Lg61x;i>XY2h z@3NNSSu366<%;s%3F%`se~WMpI8uIza(X)nIU(EV<=`)Wc|5>hdh%wyzZAsf?2=-o zbehKog?r_DXXPPDV7cpl3+ZWklNC>IvRus8 zC7r=<)o9Cq_6NM>@=>j{&^s=&Vf1E;;w9wnFG>RAayS?sB3vC&r3#E-TAMkp% zmVAY0iEk~pgT{^c8yBX}h!635E9cyCVEBAzm$62Y>rxg^;^O5NdWSgafrEGMy8TeqxvHv4&HwRq4P(E5Hqht3ANgI0DQCY#ZCgH)}O{g~qY zGEd8Yn}MfZNtQX}pMa-L!Bg^2T)2PI7XAtGv?+M%3u~|iG+UE^;mmC+fpf^Yl-~co9@r(w*);4ZE-@8?GpGJg8C#)iInCpw+Y05 zn1Nkn8Ek&M(B@gb#74ApD=%XD!LyjoqKYIsqgBHHqPl8|t5hz z`)m&EWO28HmO4ENd+EaMWn0)wyPi~l=ClU~&>p0>uD~`efHeg;tclj+6DiY=`q0{FUA=&Jjo5Hq&+|7K zG(OS&s=i@AqI>uQ=&q(c+OC9u-NElf|DAQe(WswKL_ageGds*l<4~J8^k1Tv@qY#7 z62v#TWrA=L`&5l`OiZK@@`@k~qJPi|-@_8*eNAW3aqxW(L+=J6zl!|oYQ&V~3gHs+ zV&wUNnNhybl=IvUi!V&nozWBu8L=vJ;Iy1^Zc>DPNbVdW) ze)opM-8Vm*p*FW~@VE8F3tcHL4MAV~-1K`=(&LC#*>6}!A&w10z1=6JwGu6S8P03! z*4qT@74c1LRbqaB$F-8*sfweuCX!ED*Uy9hW`y2sUm~3hyW?iwz;p)P7ufk6!0*#t z%x1q_;tkr7e<=H3kXdt~G_IQN7&b`PVJ}qBgDVyLEF`~l{?XSf)j~4-cXXk%e?dCX zE~mR5<8Kb-^J4ioL5S%SgxOMc|9#fsSj*bZavP=>A>%cFZu^-W+vS(NjTc7Jn^o|! zND@Jpz+(8i;A;r@I*H`D%v=$~`KcVxvxU5MS0E-d5s(n%w2O745oZ*31aoT2#hGZfMR@u=I*`Dpz~)+ycT zzRS0N{`W!O{3OQ{(e!(y8}eCxXa4r(z%O#2vGnGGzXwY!SSbnl`5=bmoj3X5y`cFPjLUhiGN&OPZYl;)#<^E|rWn|Z6Y%6^thzsEg^HPO3ETFWuW z{2pEhxwy8|;hxkn7P|FIeJm8l@y%2H&*AII?_~+!5x3~PLdJviY6ve={8;^=LMWLF zpM=tfJZ@YH{{v}zp&;xmLR^9}#UN3T0lR-sc;Cai1F$D`-Amay(sIOb{_6U;JiniR z**do~ojoL4%)s59;~oUu>3l|rE3rgPp|=NH*!j%dk!I`W=jU28+MSo^d`5x#eUrCX z=xmH)w%o3(;VUOv^C(xHv#fwEs!5mcZH9HNf6K1s?GkxA4~5X-EN|CKCAZr_Z-KkX zG8UJ#68dCp#cDe9k>>`cz*JKArF@K-;FygiS2^Ss4>Eq#4E zm!tu09?#Bh{YHwpHLbTd+FtD{uh*?T`RAkPt$;V(PqMq2qX{Fx; zwL2rZvb!w$&AquIIlRE;_IQEBY?{!|`W@ORJex>D_2-0>; z3v8r+0|BFVA#R9A9p8x9b3yf*MDIh6l3xa#?YQ+S27R&aWcnQ`XiYK9ChP@I_-6KV zTl)7D@*zLE=Bp7C$bOvCp`P^lFmbh%(epD-0Q_YhSkOrX*-SgLfod>i#Xst;& zs7S>4`gSX&TsOKCOthF#mU*VO5)EC%QMY&?}){hj`wKgzmuFxEPi%lH6?`=fYz!#DRld-s93rsa5*yhkRgCb z{bPI2v-zk%=lmk+7@Y@y%v~)Vrr&Ppix@rV&SX!Q`Z$bqJKe!;XK$fPEV=aW(x|rB z@{kkVGTaZ=js1I{65Du{Tt5*x+4o$Goy*TX^2OyWCT72hAU?pJ((kFB|55&3G4_UE zG1f55wSN73f2@0J-ZGk>g-lg_#nwKAew)L~1KDqL##TH-^4jfxD~aCJ`&5)lj~}1M z-UvI&6Wtkk<`bQ$OmAS1Nwh|ii$Pp22)_r(hAW*9x1T2sgM17kTHrgG&dKQaO5y-3 zZ2GmeDW%d{{%(Ts%~#H|DH_kWJ;v7py0lDaw?;Y;xth@<0N*-?a}4>tpU~2i?dL?R z59Q)HivfCMkp16(-oT!0fi)8Sruv4Ec^Djz@1{I153Ww*yy zNaSxrK8NN#cO{Qqb7>lI^$Js$pHflIqT+ zfvYn8kmQy=U#j*vWToGg(!Uwf5;RlV=kIn}{&xV1&AxF-E1JVj_^pCJb_@2Deh6E9pK4Qhj;h#KDwX~T3$u`t}LTF)OAcxHFmf|^#afCkWMMyz--Gc-rvjN-tr52-vF4skNhT) zXfwLveblAgp!==>DL;zNj{_i2W4LZTWXWiMT`1{-pexGF6*}KSCTsav+umk{Hn;LZ ziM?eQvA0rp9dR>_qY9ARFjlXt&TlLaJ@kzw*~|A;$1L<41hSj<_YsNL<=>0a`$@!- ze)KzB!n+&hu~hJXr;yL*JlCV%a?1933A?i({peMYauabFx z2cPw#kBZ)m7qNfOfqqj;G^T$aNvh(%B^8RTgZR6UCJwZS{{Pr}ANZ(>b8mdk?j|H5 zh7AM_n3_X~3N*k5B7{J7g{YvQ3yDZIN3ok^pM3kpx5qvnsXJa&2mD z>-Ey!RidIri;9)2)Sf-bsS&89x7;>=Rb+qPXJ*ds*$`WMZ{Od2KcBbJ<;*iPXU@zs z&ph)yGtWHJ`_*w_){*9btbFRDef`g=!rt@+I%hQ@(0enUxBj4!`*X4ZNA;jj+SB3t zUFy|*9a(YSuU#$a?n=-p?W5OEr#0V3$bCyg^`Ox`;Ol{D;yk(CwOuPkeVG4$yU_1C z4LVoPZGfTUI{aHA{XB3YeEe6RM%(7q|DLu9yWCBv|Fy0od|sq=lUne8G zEAn4*6gag9s60`J0q9f-ffmDBPa2DBr$axUF6!t-@!y^{MUijJXtzJKn2;r6$*wqTAS8b5h*9-y&I%zsf&`xi2J= zcgb&vpW`GD+9dH8N3y8rM$Lh6#bUJ0|C&bY4OMp{pW0%aUeRMTqHP5vD-2;jkSy() zYAyZIul@bBHdt=G2<7fUIpkA--X$At%5$rHk7$$os6!vn*s*cmx39l{TRjwF@hzW_ zE%(}T^h>$umn8Bdfb!^lQ`RxdG2d$;UmX9UusKbPrES7Ifw|&zt8l=62Kpe)6>WG{ zfMHK)hK`8e>Aecez_W29V=nxPztP2gHxiCIe!WOfrnQ40jS0wdtXVGrJueGxHA*nP zXUcIs)y|UC$zl%8!24T&8il=2FcmVe6aDC%uQx&-rSJc^$Yf1IBY zPvB?7KjLM^@iXFo59;gLgZk~qr7wYCz)tonEq6#6cEcu}WZ-{4F3yS11x{_KcLnNF z@tCL!)q%cE-z2{r^j*)_q1)zmemv}V_v!2X_i^6NeMykb^H{OJLwbip+iE8skDM`;gjE61G^W5_kB%cy(n zKE5ZL5EOW$v$e^hXaVZI~yZcwKbX2z^9; z6#YfeQ_xqQ1KOEr)2A*aWoUWaxHzkQf< zj`Q(H?GC3|`kpnH$xIvVojoty8&vm)PHXTt@gd)3RbRo8so zF8LF+VJzCd%#mjF&FnIYCv@>OaDM)vC(S5r-e!1vw;3hbY5$Nl@dWfyiu#T=543r5 zrocu2rRamPD}_8n`V#7t(7n~5{$(_NX^#Vcv~l2*@QI*&$BF()`vhagwJp>=liOI> z`8p8B4fAZ$0@5!p`A73K|Ip}rsogjlxH6P{XUE4x2R+a!2KKxe;`A}ZIPbQ0W8TsoG2?9T7|Z*v@M+}O0XZMC{5_Z>?0Ax|BIk||xO{&f^nv1zM|rp%>5hkg zMW0-R{yrnu6<7Skb`1{D677ec zKY5Ez;iJG?4qz|x4+P%ieKrC8qk@lrke}NO@Y`jmWAXHz5guSG@aZ&`=Ag|k*-kp& zIYEZU$gy&kd~(V)*EE!$HY2)L^mFGsM(~Mk#xb)-A2;LLVXBOMB6BUFc}PSQk1FZ*=~5;elTn`M}F|%x5k<8-pA5 zXMw{E!WD3M{rF$;v&oC_oP*~?_peN-^U&Aym z5cfvT^MVd0WS@JM>K@YL>qWhtc>=F@%Cl5=Qh6=HhV#ty*mG8^h>v%O@zRg9e!J`w zM*0cll|YB_y?!w^mw~SzlJCfJJ}VwOmw{jh*t@ULAmjn>d8 zjeIDz9T02!TV$Hs#qDN)xNmEczgCkE5UF2RnROU1a7KAf%RZ*{O^-}pIG+5BZZ&8Q zn}G3~jXqI9zZp(J>tw$`G(MjRe-or%ldheN`I=-bcf!{GThR6r=Wb&eKYI}fEkM6? z{2KDKTn{)Y?WSn$v@c8a%PR7(_MGUeRI))_A^I^4-gM|?(N3~|lk1Vbf{8comp*&D z>HOxjD2~4ac<(mRKRXZ(Y!v-SKG}B4Z*N4n9`bnGiYUF*g~J~-+M;X1`LYjJ)4Hjw zTNU2(%5VJ$;Syco_i2QSW&DXR!p9fN?}OhSFO>1$B|UG(PTof&$cABBl>hx4`LZQ0 z-Z~RLo*~O^C;M=JS=PhIlPtp{XkE|p{$dl%{4 zrKG!im)xZv|G($moX^aec{6A3b6=k;2xXJyeoM9?HG3Z?{iQCDM0|lRVi|r)(BgL{ zQmR>)6YPgcnak}1usgqb|6|h#Q^xzM_ZDwo21dVhXfux3EAVVBph}D|#WQqiNdNln z1xUH$48P*a?&r!A@51jaOlj0=kZ`{he}r-(8$kPZ)h=$ln<8_$AkQ^dfmCHwNP9-u z8k5-nqNFX|Q}mW;lb-2hu#jwX>hQUZm~H)Zf1bb7uT!T`Xd1Y0S@c!!iPzE;jQDW& z(Hxcd!81o0c;tg(@5JS;)~d%CIK+I)8>nTwfd>p7eri7F4RXd&#il{2Gm;eOL#xGq%=ju!R4n##Z_?KbZ){4gDyRK6UE#Xrc9T`VES zzzBcY{2!o1FE78e5E6tRG?m4P7%NOU(`>!npf3x1bch!=&ak%26zMoEy_^-B0@jD7 zd)|Sh%2n1T-ZxTg>^{Ur7kxbBGx8&^2$ z#lp-plh!{~+6(N-)kDJfe+oU10U8nyudd_z(bAM^7eu2m3f@XoaBKN)IO969yMNV( zxRPJFdQxET#!-CP{7(tRRKbS)_}Q%$k7w$W|KY%oc{cKUY1ZUzDcW5vepxf@u^G<0 zD}j>o_Y?&2scNS@yWY3l$9(sj(j15qk3Slk1VvE}f=ZikM5vOaa|g*%U{GLb;m

    gOJZ9aS$_rvO-_yW;VzytG(S1Ro1-Kwwt52_JTs%W; z7OqyIrp?_^*3X%nu^&PrUoe)x=)94D@O#MMxi1Ym%5iVRFAIxIa>8^{Wx{twEAOyw zx<~UV5-4VAY+spbO-ft=v&gw}AzJ*olQVay?&mWJMG_L4-Cq({+W-)Yd?lSAuqT z+;B25{y)`t(!0K#F`6_eMNs#|DJXoX)qC-fuCCpeM~YLnKwdAvL=EV{YM$Sd~>cHYhC%=GuQny+t4)=5?>vGuywD-t~*LgUVC28 zC0CMF61&F@>aNhtZ8d6Yet`=v!lYLpHly)9#XYCDV1TAI@wfF}-SFKDnJ^8&<$)=& z^MCum{41N@Y;FacUs2l;f=URc+4+m%`KDi3T#KrbuIs~V(M9Pm=S;8D%BRdD$5^!WXoQ=-F6s`~NKMg@oFXAC*N+t56`u8Q-RBE#y0s;0t2xdIJ>`{+_HK}P+J&mVQ(lul` zk3Y)0UrO-YVa)W;63>#J(PZE~xrM+ee0+3vrC@K(s-F{XdV{#XZh2}NMfuM@wpyq7 zCsK7YllHs&Z)gs=$*8k7*!wmQ2U z&d`gAa{U=_AcQzQK9GSF1XuF4ful|pB0VVD`tcWPQr`8Be=x73Xc@B?ZXPWyjUe}r zWX#&XO4#jwLG{zw(7Fp9sAf}Z=s8qfo3Da@UVO!At=f%0m>_n?3UTD9-4xCNv&5R6 zw_R&X?Jj{EhZ*%(_^nI-)Pf*{qyHAMrd`-Gkgn1V{-K+r)0GaM{^7gS$khT-MrVBY zsToU`8py(`+p1@RPH~`nlD_B9R!!T+F+BI|pg)x9N(z2fC&*_Ax(89VrjSX2YD3GwOb4m91dqWtG{@f&P{b9<^qP~tXA)(x5e()+Qfdud)*!m;qS(5+V zb&Z!Y3r-tHdp?Kzbn)du*3tR)$cD&?&jSDQO)A-CZ1%c%!Y_ZaXfQXH99^~{_Ixgz zYjH>MFLvVot9bVe0`W-{cSUuMeCQbcr?T)f-f6a&KY4R1tF7DIX*~qE8SdLK9Wja4 zqo7{q*oOmpRTsfLQvk)88vFI1wH`*liy;UJ9^0<3gbgrzRSece_MOP1=P)J#CV}~}I4^)jc4-W_rEpku904Kqx+qHDP%!Xu@V2jMU zyCd8)nTqlh9<|>Z>W*)3o~RE&ohdiI+prj($Ntosi;12a%nb0#+F<{|w`d-5+Rl(Z zn1)nO+uh6)s2vqV>_tQfEI&*~MWd|?DnG*-P`BqRj&nAzRJGS|@O6(_zpsI9^>LWZ_!!))7bH4)bMq`^>2#oVG&-P)HWc?QF9xEf*Q!DA8+QwJ z&ASJs_VNi8xLMSbX;-RGMSLoHG~dMKDgDJ6^50O)TR%qD{&@G`3{YyrXWW3QnV(ajL$iKQ{6D&qR5)-VL>KNK$>g7O+&pqws0NZ?WQ9RfkpJ26 zr3%R|vZf=%D3PYupAW9&pgb12q|8wZH=A~X%>|5^D{$ub%?YlY|~#9wpqXT(zc__Yp+Q!6$-z)TsE zm#vl{U4mCv$b3F*L1c3I-1ydl5`Hb zPU&qGw-cI6K54%50PlTw3+I|w+beBZ;YZ}?14DvQ~alPWd@1t_!-y9~Sp(eeo`;s}AUq(~On zljFxl<1f@%FZ^~=ih7A72W)g45w zM)iAihtX!mbuK7A+RZkZHCWG+tsHY$C;yLy+stpk$eJ}( zO=U;l%k6I(>)LKBe>+{GZb9mLdG&rw#+h}F_(y$EGurEkqc3&Hq%>KFJA%jXYOsm34 z8P=whHv?9oW+P&vTh>ESql8D3Ie2bzVKiy_^s(Jnq#?v)aXqhUX{EOHsqw?R3JE<; zpqAFF%6XKt!+pIpu;{2_I#^VUh@bpyZ`8UIe}CPJegarbEik3=|A#TuFX-?-F7jkv z_WHs-Z#No`S|uZ$vTM|fnQKK}Z?GPDG?9la6A#O?Bb!wBrH7~9j9Nu@op}{euHzM& z{fZ9W6f!UI9!fc+`mlJZodKG)@0ijTG}^%Jf0IP0uQH@vDN!qNNW@n!&Yf1zll)Ic zNltQx2p~}$-U!egpt!f3{uEq@Y>Lcn7qfOhn{6D53K}5?#HQ(*Kh83GlQbvc&M#G4 z%N5pcnh`EibOoyggLdv}*UyPp4(|Bpn80(^N^o`BOi$q|ABU~kS32gOmx{zk3ifg< z3dh17m1Y$OVT6$9QalO%-H(wECp>idf_Cy%fguR{_j*ROyl7-jhOES1&%`(XRYl2} zxaf5iF1%t&OzD(w2=i3#@5CAUnLz7%aOns9hW!4EcK!5PPxeuYW}QRb8~>gnkH(2j z;p2#t2cQy*k~>T0L!)@z*NLOqziYmdigNF=C$@T(X?4srz6S*|QSjPt`g~ z1k}45iZ^?ew(UO>@_HcIcSK-rCOv~i+>pzLZg&YeQ=4C~no|>L7_uMJac02BmX1}r z))(t`O+whUt{&$%fg4PFp2Cx+BqZ^d_cv$rW*#+;TA2k6HCf}XNX_a@^EL#r zo)mL;Q;Z})s^^aP;R;2cGO1@rLJ_9cWTm+Y(u?XlxNa~auZy=VE z%VBp|;B6R~4I0>OIAJmkjb@2DMd8HyvfN*SZ&W0RVm%Rze$KK!*ZRhxK=wbMZMKWJ zPS&-HLAHRE3ibs`FQ^)Q$!z_sE#@pc=)A z2a0hwW2B+`LXyQlI`GU;bE)|O{P%>e;Mi{t`aQd-7o;};Thl^Fd$yMdH=ON!ht|4X z`HTkE@6&s`-Br2jgTTR;;h-~jI&O02cyq=T_8P^*xP>L@<>XR}o9`R$*6q%ht0i=L z!ud=#M{nV$Yz7oXlr@-Rz`pi~(`JKo1MmSO5~`DXoF2k}$Bu6EEDwU==(D5B`R|G* z0_#c)xxTAEhuRRMu(ia9S`xhG>{bXDk&i3Bej=DaBH43)@2>&w z(A_`j()c)o8V*@eqPg|x@;2^TuQB4POK+)vmDtl1b&^IqC0?V%-1`I-e{t7#?P`4s z7HDyk2ab$T2S37k{o%A}G4C|QZ>=MX+`D!{&4DhMLTAauS_;>hESTziR53F?ldjmb z_VitSrpSabrn?L#E!r`ycf%r}o8wkMSM23w-jmPrcPRQEarir6ZtJNONB@W(*8jzM zU&3khUhzl0;w2r!NVC?JwhTUVvxS9y;-%=?(woMrilg3wS<&o5JilphH_b-_6Ayn^ z2B33t``H{!fwP#*|$X)Hd@k=aJ+}VVtNXfsmK74%Rpmd6$TW*AC(p#={UWL#k^(zEj<4hh(XK6Vo z1!(+2=0Jfbe6KGmj@ew%0*DP56E4v?-?qwnY9Gy znw-Et^(JQF;rdkBUI01uU5M`Eb-$!Y&8;m}vA|O?HwZT$X@(8V6I&iI@kJ4}&T~df zu)^wCw+mFVMS}496=HEV2gIt_>w}4ElZv^=qB2L-AN0n8Gg7)>ysrNoKrQrvacM?_ z9ki@Z5Z;mR-E5w9&QAVTrL#B6h(H>Zx^vwvX%dzgC4e^4+@|-o3d^}xMPC{aY^D2h z5xTr|2cz2>Mp?sBQ=t-q^zN%j*7Y6>3B?q5ws?1Qm#+;Ko1~TMf5Zv%++8!)_l)CA zN8`&T1u`45;Yqqid}!%d%rFp6xae$&$Aq3WdkDGU1G%}(Kl+$Nv|PlLI|j9R$tFqO zG2T8q`D{8GxnaAh16vzv!M%X7(__q`+yhHyC6t(B>Imw2UI+ScT6tP|!h8DTXPerN z1RYst$?tB(WmY9vz6w|iRd)%Ii@)xPQN%1$26j+ab!{Qhr zyJ8(B+4Z`6NbjIUzvk=sbjGT^9!HCIl*Dt3Urs$AAL&9;^ZBK8vb8%J+K&CPNl1QOVpR zWQ39-p8EP=OG*5?u51Y%Xo*0XO9NtpRoIh#zhbeHmcOb{pe4Vew5$ACJT@g*cyouk zh1pg4Bd643ehY*ySl%S^iP@^adcPplcxC__!JgW5d19;p5B&{#Kqcj{oBUSh ziPLRQ=YM9|H0gB<8C_%2NZjp(c zkjx;&(g72QkEbYad6{}pqZEASa)%M zJ2o=S&fI*(dA_7cx^y=B$=r!Gcq(_sYNbHBaHePDVfuu$hxF87XoeQ%OSjVslr8Uf z+>o_DZ@%=g$o6QwV`kNiT=fkzVmKGvvvAiUcI69WR=mml%Pvsrq}hv0dm+4e{H63U zzQYPIXY`KCsn+N3z^CbPF!U1G7Zs6HmA&00TVGgAP$pqJw(V07ru2da!p^|=_EfW) zTy+@2FJ)p@NVdyfER0_;j_ouoBK}1+FGEQ=gJiDNpr*f#gMswJt8W*L-{w)HUj?*9 z^5fC6VLdl_IP0hCoQ=h4f6cWA?F3GFAA10lM*hL0>VnhLeBICjriJ#lr90MJPHcZZ zi`A?g(kyA-zvRsF;PwdS+qWMDE6loTNpg=Tscl83cUe}lBz%*Ob)(T}qg(O;jn=B! z1vng&xm^AnH+h2(faKJ%mQgM(#Qj4gE;EB##91fJnAF(kP2?_6=I&`88t$pG=KoUl zG;A~qBqV};wED#`fgW_|eJeiFyn88;96#J$kj<)n@_Q|@4&)0;F5hZ7o8Hby&lYq> zda$I=a7n5Vwa6y+V2tlh-ARvB6+TVkU?{-(t;w?{(Ob8J-k-n1*$+HZ_+?+L;pG}! z?9>+z4!sb52C0Q4ZJZRm_%7XnXfgmi3Z(jFtxq+dqaF#zx9H-YtozV;&JSGJJx5tz znV~^^tP6^fO$|&3g(bvO;cHd@h;pyL14r_W)-WN|XxZZf6o;v`8}NGJpO9d|n|AG| z^_dONpzCc+d;LPvi?tCqY0S5Sl;62}$&cwUS6;t0%H;j8vyP#IlR_7eh!fx*?(Vu( zmF9SkrcKL4#gEJ0*~uMZ40obm#?@QrZQf~R4XCq?3CCsM8$3@>yl3`Nw)Q^CJOkj! zd&kKbaod)3?B;X-W;~`k0)E?;x@9c7?x4zh{j3F!^GKy!8`a7?q;=Q#0QKqd@4l|I z9&J3}Wq|p$f9<@SNk1rQl@v<+w)9=9 z7$VJ)QYInEGoLIm)C11ByJw;ETiusFU(t662A$y1dsvmOBj)Cn`v8~ruocB-|6KN~ zZKFv1w_{du^(baN@ypLF9#x5Q!=iO6?p9U0Updb@UVwmsYWR<6fr--q8)up2t&#(F zZ)fsDUbUTNEY443t0j?dV8`$*O_JDW51jGwl+qh+T`s;#8Bz_8%Tkd+S@6g)_YS=r49proAuO&gRg#W z=nS-gAE}64eSeRNH6|YX^+FSQ*XW~U4fm!|Ch;)5?a${1>q}S!;+aLpUGN#BlPoAg zpz>hVuXzOMTl3%B&7Ysxx^_7Z(w(ui;l5jLvIF>|7B+rOk96{D9WW}Fe?<>Gd_ z2Wx(<-Xld-KH8H%>gr%T{`a9cfu>EO3Pk$%3Y8rmj~n6A zSYW(QB#A1RLK3uM?QRP%yLDassrbV-)y|89)nboCT=(fM@730X3aBuR{7v5vFWqnf zr%jW@@7aSfmST+4;i-{GYf;9D+Z56&NwUy4{~R|yGMoS?xd{GIv5^jnZ1c47zH`$( z;gK)g1?dEN<*(@d*A==Xu`R*qju(&A7J(xzrJf(V_3bT>q{t5$E$XOrnUVYr#Y58_5D3?9jC>x=lywTPM|E*=od;c}asdK_tF zX6t>b{lN6|p7*)4)}+B^C9IgL*7~9OiFe}S!~^zMjGFk;Ws?|b_AgnP;yC+;jEW4y z_ovUBp+eh{zBP**-TeQ+%MJwaHUsI*Rg!IF)+_X(o7$Tery&*o0R(+zUVz(cmHVM{ zcaDDH)gO$krhhSnc%egV8rT2F`PPDM2mjelM1vCz-Vq%T*g-uT5PU8}x2;#WEHdt|_Bk6iU9;@OC%@qnA+--HS#W!0zWGK4h zVSSc1tHy>hoE%2gHtPeN`f&uahZ)LqLs@??Pq+AX6b7t4%HGV`)jzXdZ>T$z-gxMQ zGtsO{yfqi*LEZ??IA-m`V97=ZlZ(*iQE?wW@m9~s(|Ao67}3w+UQn!CPbmfIKFPnY ze`F3+h*9S5@SD-Jtz65cD^o56IYqu#U`+?({_7Mbuk(CjZV*F z!{h#RiVW}QS5uTer$5epdpAFZxt_p2(tG1GRP$_^GU!sXUg{@p4@hJ>ZJ}l6(6yS8&hLbvIq64PQx2c&Cc&dm%zx(O#fBDrfA`wt{4jz_r08Ie?1FOuVG#If~=gs z>(BEj-FwVlKqi!S;)r_-u3H^&%(k6_?Z5cll*Ntoa!$?opkgf~b^gtGwuaD53Q3UAuw1%^&)2m)yei-;#&?rHOh1;8=dR@^afDIiuzvj7w7`h3#WE z30-f9?a71@(p)aRT|`M__5WBawF~?%%FqGF`+TK-=I!^fkqJ)ilZ3L`GxVFyEoVn^ zN3NWUXs8YG{uQ6;4)RqLdwOz4?;fj~bjtXB4%UoFThWn2L7(Owa$KKZ%A*?gc&u4k zqE`d(>M|oKU7-TTX-XPRKy(|`XZePe6}FYr5AU=Rc)OCf3?pjloCU@gS~}|!&<66@ zLqDO&gVErUW|#9~1;ORAl*Ts&_3yq=-a?&Ts52p1aBW-Qhz%qD{+5KKNa0)**fV~9 z&sXBD>yI#t6)pLm!x!!4orD6BTSYgR-Blpmme@3&sS76X4#q@rG0pK0#LlGvbHTp8 ziBiT}b`i`PPYQp#x38WS+Iv_3;f4Z@JK*`I(YoI#3uAEoVx}q7?l|=p84FeYbkc>~C2-KW7(tCg@o~AwWFj zJm@QEljLAR(dP?7@J-h-31Wuz1kak4%y$XL@@d3_!Eo6lxA%sv{W;fQk=@6AFxoX_ zf4>EqV_H#Q({|(BAXEQ?F6v4E^gk8yeB|Nkj;IMbA^JgKzKVk%adn2X=EX= z%V+u;K0TVOfAwES=qtzhmCy%RU|WyCe5koUHwHId7WKH@Xo&=5?Vc+=qS( z%%Q)8IGCx=A8tiN5@>qyI#|;5ioKcv{=f9<5RVJK9$^)yJ}SI+VOT_?kjFpQqqmi( zlCD{AD3gy-soZHaDWz5aa&LYM9^2 zSeM>4BJQZ#t-`q$wY$NSysD9YqxUr8R>d;9FR@nN?$_AN4nB2NT%^23*v4n zSv>>{xS_;dE=>oJ*hf@0-K881QO;I}FsPH^t#rE-9w;X=2@v3j$JS{1C?__73Y$-b z%uF&;(aJdkwL(jDhIbmfErj}h{`al;=g(ftmEBg+75K(g8>p81wOye7%H~Or{W@zQdPdMMWFLY!8%Fov3Ujw#nPouivK~HcM?XE(mA5@Ds>Q z-pB^k4L6)K53xay#NAQ+6yiRIdWFv0bR%1yd=LS%m4nCg4}vBaY)tosTWL6P5+Yah}X#ss@O6n zyD?vZJ<(+qG24!UU#2Qn*4gBroiJXpu!)hOl_i%yE@CI6PHc>a)4>Xj3-<{tc&i}O zl_ue&>DP4`ZtM)42Rq&66p%+KbHZR4nbNV(V@b*04hyI+m1_r^F}n1hcvjVVnNkX} zTJL(7<&HDVNR?!Fboo8}>ER4cy)7iQJR}BC7sMGiJ``*h9b9?=d%UNO3FA2p77cLO ztUiL#NOG?xo~8yH8klQ@?JLQ=jx(B(uP(rG5#-Wlk0hl&%We+6_G$}Gu51-gseE>v z!p=89Ll^c0FStA{DA=H;y0XtO?fk_^Ramw5=u*Rb$nE(y;)_>Wr{BYC5M$KGN(x^< z{1`;Sm`dCr)XTg7IXFL;``Yy6$sE_UOwg%l&qjc>3rfSisc&=tw?g_Qarz4h7U7?` zNHr(FUQAskjGk+iZ_!E|@6+@_PiK1L%~YYH;OYU@kWpjRKWTsjKuWMh9VaRJpS|`t zU>j8yOSCT>;jhoWmoJZ@!vaF_SX*GQIdVM=gx5Q+r4OK;iIB!r(_@*YzUhXABLKrlP$Z*h$hDE9qXqX@xR8fk4Tgr>+>pz67KJfjh&=g~=A0M}0anVKUQ{=|mC`&;me5 zDDq@ka!r5W%y{)(;o?#cxT3(HQAl>mKn7(^44OJV@q5lvyUb=Sq zsw>0DYGb*+v7~$ylzL(j{hJrzbH$O4rs|wN#4cZ}f72k5$*VcEW*?N|9%b33Fp0Ho zetq<8VJBOXDJ)F};kb^BRBfuGn{TBVTl;TwERR_JDkhdm5Br)6@P*Pj5-qJ@m3+8+ zCK)wQn~=C5c%V!=(Pe{br_2_|2-}dersn(uqvnz+nw*89@|C2z(6ot%7X{um^5O1# zqmm20AuC&@rg!Sf^=#MY_=4pt>2u*ZKDbTd;i}MRBiWBcg(E$g$^oh{T5kdiY=aY57ur_XP>BuV;B zDt?fJyFysr_nmPUvkOzWD@UQ;J8~MaCFo^*mRL)R*`ZYR@hcvx-Gphf&xURM><4?W z*T3gvtSb61{tssHmwH$EwVbHdhlYp?x{K#&+Ho%(bUS^W!(A64ZJfctN7kEzE9i9m zI|A2`$2?sHfE_Z$#msj5zO(|_uin}dk`9)duZh}3VO~=}*P|!A zgvIPF{BrAML^exqM_RoM&uTcWqw)Y-%y57Tq^Y8V3A$)eBPxQ4N_< z2ReWpRIc)MI->=6yE@q2l=|3919-t+-s+@^E!f?L7KiFS=4JWlwLRlBj z-(vnxyo;c=Xu|D1xtV(WJ9dB8a@WOkv+cf%>&y0<@ENk>O9`VlSFb_M4WG}gf(We? zWeX($SZd=iWJ6}Ir1++Gs^?l}&3?;z+b^a&1ZnRsVy@8?d#YF;4Ih{rRI!!v=w-~~ zYxuFyJ0vLN741y1U0!SMv`gc5zqCA-Nf8Q)A?vylZ(L*g<|B0PX#2xyqrx?tvI&^3 zTO%eysg}(nEIri%I*z&E(8$qJgl=_BR_oN>gWb?mU{#+~mm9#VHiAAO%q3ijkvw2gGm0k1bhU(=0$b;~BQ>3afnrZcUdP7s2Uj-r)%B%Veod9eFucGi0ed%Zj= z5cJlcGU*2mr=n#b+`;!l+m`54?r#dBJwAerdmm*f!el6D(<&!_j7)di$MlQNlEP=_ zEVS>XM3{ayLU%|dkG!-<3#=1Oui}uyr^l999(t!RLVo%EwNcd9U8sTjvz!3Nh6g@l z&{B$}yVJyS#!C6ir$i~gyT2tAqigig{QJvpIwlNz2x!Nyy7X1qBm`+piYR2rY;l3S~GtUXpFVv7GOp- zRqoM|remMr`RR+KQJ7>*G$0h0_balXd(BYoEFY>=%iHe#hB0Z=!@YfqCY3z5;@pub_q<6Y!wvlx@O-sDzSeH+=Q<+ zr4}-8&YbfslNvrMYKNR(q~cn-8jKZkq(EmAyANkoJLqNNO7C6I0Ozz(RKovBcS9>W zZa7g75#qN^(t-}%k#cA23}eHc#+PW$^R72sOB;{*laMRy3;HUwNz^v;0+xYwgA)BZ zx$zL9?7Ra6sgc`E9yHJ4enZ|$;KcLFDUqAVCZ&m}y$I>5w(oK~4ZH*3=@N*UzkH}l z6~X?;HE~xQQo-#6XeH3BkP)5_YE{#!OGSHMi4Sa?xjF7VS0RO+Nlcl!k;MQY@Z!c8 z3$Oj1(i1#gSX#wOT$cilxx#ujP-ZH)5^rl98h^3L;z1Dt_6_Sy1)k!W^8Bc3H|~e1 zw_ zH&;*W?*o@VcXPU)LFUXg?H^bWD|jEHmp!H0ZR==mN>W43^x&21 zjQnZd%*TYIRNn;p!OPhX7yT56e&Q@6sGO;;&YiuMg{A945SK4QwMc>9Fbfm47wvYf zMK#on;w(SoICGr&`_bj6aDJGDyd8&!rxoi<=>hs9_ zuHf+jnXV@EInrKLXI}RF!}{qq*T@LHAIH?3^7a0_QhWUeX_Gl0{b#Fgcjf*ul!v}i z&n2FJ#A>lk*USp{2u-hK6)4=LE|M2akOj_;VnCjnp3i`yY%P<@mFo4ivj}2&zE$;> z5{Vz-hp(K=*uF|d3akYdHSlv8CTELPIt`6eraVP_ucexn3F2YII^03km*BH;W7>wv z#tVgBVY@?934{+V-#55oi4Sq_Jg|@YNh47b`Z?FRu(|w^)A#OMwc0_ePZuM%-vB#m z$Hpo_Wd)Pzw?htNZY+EeXjU%n8Tk&xzl=0Yb@{A{zyAf{ zkTk{n(+Ap}!uXGv9?@ya#-IvCM;;OFch~d+r~-{rS@61Da+zey)Zg9JLktMH_f|hE z|A~bkK6hNWt&~f!?{~PrR%FrMD->k4v7lmRo8TSJ* z_Flq^j{e_)E?U_Uns5JqsfTd)f2Gma^boTDt*7E&S$XomEnEd<$p2SbjX5Cy->P`y zu9^QoCra-u|Gynx37W|MZvY6;WBzw^^?jFEoIvP4MMU=N6TSRKIq1~e6r5&$q+MO? z-qf;5wmg`iYEF0b{lfZc=BIHLxP0toIj<+vW)ySXTGWJb&*q9<`*I5n4J^%Fc&79vj(H0$ogVy3t92#NdeNo0NWZvM)`f7a zctt0+5d=XoY%)XPjKgkE>od@4taJGh+clV0CK>sz;u=Z!lWpy=)E-e9Db>FWx2k9r zk<<^=!zovMwN;vQh0ntn&XlyI^;j`Ml4pkWAyT#f%@Cfif@99)2WvmEnm~m}%M=YY z51hFU!fi24t)T$tJim9_6f;*RU3qp>QY&D1(cK(>e)1I-$8Bd) zFQSP#h7Hn1vzP9dp!Ox@h_w-9xSSX7FFMVl;we$4%BmuoVd*H$i{cp%Vq7B4Y$0dP z!pC@`tl8Mrq$p<19M7f2@6eC8n*>wly{7Ltk6yD9Y;OSzusaKLJ1+KFYW`TMUZEAW zj9iKrSRgmH zu{fyA@y}m7U4wJale;5l69(>Q2h%o6yHYJ4OFMhG&%f~Iz#*oEGY((rc`h@mk3L2h z$TWzRiU4gnFVm5V{t*OvWbqqOwR>1)ow9XzW@LKtB%cif`}Xd0rkQX5I>1~L_k0V6 zO=1;yefHMjK4EBc!`-{gKhSyp%smqikXM2hJ%We*2~Qt{94SPRf%BtP=tZ1f9GtqG zf;(2IJ5H7_Vh=yk-1C)iG&8Y#4q>tL>ru<`eGC{ItT=>g$s#i4mGM@vuB&XYhfw4P z+!T!eHgvlyHg=;vGG*H!3rvw&UP!SQnWpPN=N`V9F$?rPy7|F)4a6~CGn6K{7;um6 zPK!S}rhaY$Fz@jOm2<1!X;~?#39Vcg4s)0qN>8i~9g7=(eDsw39|PN>R{N~2MX`?G z0mu>%+)j<}#58or9Jz^|HGx#G{`of3VGYNyd2g_O9h8rw z#bs!JnlDfPQ*Qg=6b5OG`bv&SK(tv@0BrV*BJ{KYjPj5gn)`lWVE+=NDa+`S8Q)JO zxua%QM0_ydoMSS!klL%qLV2o;A8m>Fn$r~X3!&?dy7Ro0-j)LvP#59oRu?wDJheMT z!^LU(yF)9;fFB}HohxF0;Bl9eQT?YFMF1ccZp}3nAFJ7(|E!PHZ9ctk?I8yN5b*ww ze~@vAh<4_uqQvXxulHLY@amaG3iHWX`J{Q7I&PdKScaW{J2jo7Ip;+YyF)=qJ~p6~ z;S36!7cZyfMaj&ziH7!Kf6ta%T>FsV(lXWaVR*`XrUVx|D5&%5Lx=3>qX@AT8ERMN zulAxV$E?e6>CJ!sHec(i1C|RP0VH+B8r95~&%Uijsn%{C2M@R$ttHvF@N067 zwCa^P-T|uCMDh1YQshV31Q9%JNaMs`Ck87G=~9NDp*Z1b#)HXZtsI>%AA~j8V@ymW z&nW$Og8%*IzsSVKO<~R(A|70_XQq{-sAbI97(%2}kZ@{)AP@LkeBdw?$dBiJus$|B z6;rV3$L3u4L1FHFe34xKGe3010K+>^(J8*Z66JgRV+w1-(|%bpZdUF#7Nl)?Ca~#< z_4jwurL{irEj?Z9bd_cw+G&3FSmi;YP1#YZoCeOc=`^wHiY#FKG=aKA<&= ze`R5?nkUse>qXF4f4D!A44jsf3Gv47*inOZDHb=Ccczs3*ERR5@0%!@N7KE(RMCEy7+b+lCXd?AH$ zc^Fbq_NqADK&RpJwlR`g#FN_&230RDDJwxNr%SerG-OwnA+2;Ar?$WM%xLLdG$&YU zwID^1MGS8>c#kf2@UXf@fXZmD8Jz?&WieV zwa=Ol@6j-F3Le%Qn8M^8Pev+>?js^Fy@!9t$H!cIxEsSf%vE1c&ADy$x@A=n=!GeluX?f#p(#hXVn{O09OH1`&X&KvZNQ#_mQ1Envs%5XYnLk*! zQ-WSz8(5wIMM`_LI^%zD>+Xz~ntag8Gch6#M42t=gk&49i4^PQ#1THPZCC0bOVW5C zueA;JQGCUV%b{sJ&hn1>((gH*y(lSqD;_Qs-^LyugFr#`^6HRst4l5+()wFzD!zD3y;;wNwB}@4 za&`Ge$ctOyD#3k&+Qjoj2ia<+^p>^EQ>Eq^ptAPQ;$;mHmj!DW7=+(nlo+4U&<`G}kPDG%DB3MoT zm1Pa!DMQccJLq3>_?I7FYFS}Rr5{~XC3|35Q)9W4sCQQM&tQlwUBOw`H;fBoIC_hN ziV^EvXr1tv+ur*MHsAQi<2|_-b@kGbfJdv z@b9ND&;$N-8Gjc)tszyiZ*11f-&~LuP5jUSbbSB1T{g*i2&fd<7Z&O1U2JcERTJ}1 z5O|@_>8&I0Ih9h`$F65V<;)lqXUZ&7~{(=6)@8>D+b>xK+@tw9B z+CU^nHEJ&O^R}QwR$99;4<$w~y{=3ujOEK|Ws!ktGBn*_=Do2xk8NS)-Gyv19<_c` zEkjM8PKULn#|8bL?KT#&C&F9g7vCHz>c?;XxYqG*%G{JRrCzeKl-K#Y+IVOH+MH3z zDt!6<$+6D8K<=OC69w~tKRkYD&N@O#HP1|$7KEpFba?` z7A3TFMDK9>7fGDNj_H@{j*ZO%P)5?+d zIZP+vZ!>~zt;e>_P@i+^D+X*B+2hE?U|N%1a!Jkwd`nLLnO)w9tTc*1CH#kzDQ9mP zhc-R#{AS0BH$$gJlCwJey!?T(- z#(UxQYs-y>0we<6!zenf#e49lckhkdCY2Q+VPk&q<5~CvoBaI;joEIDMcBf!hT@bn zd;4t(_!GTH^o3|W^3wue0JbJ#5=ZnU{w4HyZ2Yl$GEW=vhXBge``*bK+cVP zywnL6RaI73zmP}sadPD<-y%w>s`8rIuc8qb;cc7`UM0#t0v-NT2+B+qTA;Qshi6!g7IEm=Jg3UQZt$#@-?dAPuR;Qw-uKwJa z(d^6!(UV+LMAIaFT{NM%tQJgjUd?K--Aa;NMOuss)W9oDdRp!3rKsC~%F4)kMNZc0 zGCttXiLc?9ohNl8Q!BbPrW4HJBL>*TbV-I;vw?mFP23(G^t~As!zHi!$B9 z^)LA@wBb6R`6eU@F$k_HBlQiPgLi95eZ(A5XNi>GSWsQ>tM%7ZnW~@pskvpfg}!=0 zfkRC35&3*5ncY)HY<*~dAzsE@Szk|FHfG2#h%A3tx-GiC6pf1=tyyK9_Z9I`7oiPs z8{#P*!B#B{u?iBOuXaVvTG2dHweSm;$^@5yOhl}MWM;_+g-o^T|H%5Cg>{)rCtpEc zk)ltTD&{TO0L;mppPO)onhxii!{3?9VSRw5^VZ?ZbUY^v&-oR_1D z#v+`1*+ecs`K^-<%lR_qN;Jvm5OWRjtLg)^&|}pA%Qz;~H9`pkY9ItgmhkHlK zuaIYodZkn>r6{Eei4$L4r;F%SFsmW@eExCP!ZqOACB$rlAVFfEMRv|J3@{U<`ZB9-U3?=Hf9lN+>hb ze$mH&^|fWyrbIHgrq;`MLuyv|26dOEAL%RBA*`FUz=qb@ zp>@}*cU91ehE#pzj)OJ7w^B?75Wt|^qCTsT+lS9Zva*Gy7&fTlT#lBj-{$K0kn0gc zeS;Uz8~mkpr*B^&a89bcw?Jj=25^`v->R~I@^#Ub9(8r)s#@!(llcC`ntF*I+M>u6 zdlp1?lO?H5qxk6}eieR`r%iEDl#9M=?j7OVT+FFmSHL%r*3{1vg)XO28C@xe#sfhf zjwb6ef+c-bOiDC*NV7)xB!(m!&le(d?OY9TsR6_tr01JPrQT@N~;btj^KNvV%;=2!&IMtv;#8sqCAd=$s5uYo?xcs}bY zlc%h%j>@}Q(t3rYSCa=)SWfkfbTQuUJcN-(`!7QB$9tj_7e> zotQ#JvgxcM(({a2ST3Ws&WkRwF^i;b4Htnp2B-7o7%IaOw?wi)wun`DC~f?IxTz~D ztEbjgB#l?2yvww>;6gxjY_7>onvkTbWkQMkh6`cAl5SBauT}p(QwQ}{^^!~QVpg^+ zE=!$URj=kwbE&t^R=qVSTb27|-^iXIi44iA9QNR(ldIKSb;yPOFPGEBe)OgHY`9dN zcKIc_6ZvP#%OK!pWs9$6VUFg1v8p=h(pd5H(n%8`Bv;m}E_L>7p2t^LR?Y(^PO>LV zkU{n9YVJf)eqEK%=T|R7*jrQW!^2lzQKMcu1ir98z_rRyR~tsh1>)HgJ2AhkZYK^wCG(xMG;Jwb^-pY*BG>@sTU+ z5zwRc9I&<6kc z_s_q-{+E6sW8HM-^3RNQ;upZL8^4429mB8h&w{Q$QN-Arg8@W;kg6yOXA|JT`Y%|5(G<2`4X7?a83vP`zmCdQOgj!|>W(5RttGl2Kf4dX*u19(pPpb1NJdc7IeTL_hzXK=EuoAULO!O7W73IV8h#m;d2?@wvHoU}Z#}~7DiJ<{ za6-8V7b5?WB~rFRcvqGPmm<7g72zs`OB+RaEyAb1A;MdK5Z<{?ggX)5bF~QXrTR$S zZ$H8lW}I8^Yv#=>P$v{}jZ@9dn=);RYjWS*i+3a_ ziONXFC?!o9tz2MFSH>t~ZA0UwUNPl)5!e}l#S8l)u#otZZ6@sC&zxZV0Mc_0cmqp;wG5Uy|oi8zLlkf%nsVXL%&YxP636F{C z7dzL`tnh_QvA_=YhcIF6`N(U@93!U&!DjnuRVCSf(q7WgMo3uY>mjK;-NYZQz9d`f z3+*$ehUWJNEZ(bB)9SteTgk2KZELzG`QD8FXOpEpGQj0>aRyIjKQ zmio9ZjIB?e1-@3o*8KAa}3w2{6{T+@Sh~?koi;bJa#eTcsAd!x+M1fwHA1i zBH(3?X#Vy|gX(92XDR}&x8&!WLo)qpq^sux`7hHQKV$g&g_iUi6alwc-rqTW(Dy8G zl_KCOOMCM22JsUMyiO5tu?2tYFB?RU7MSKbf_GZVS7*fLpJ0LaC<69a$}gEY2)`D8 z_@|11{g(Va`LX%?E$}OffahE2m(7mN@3O$}C;|>z^1twn*!-s~@NX3XtCso zd4+|(6anX0`YUNBaD&psDkJL^{1+wJ@4~i%q~4@s{zvB=&npEbuCufDCnsn7GxgT|)?UYH`_(!^-~inc-X@7CD){O}`#=Br1e z@ZB~6SA5;VPxcPN_cO8O6~7Xj-uFsu`pH*g)1&LaBK=T$EIh#?-;J;fxRHwh=}du0 zzZdX)MZo7H{glndC4Yh^19sSdqi`PJ1i*w(f(ywbMtl@rOyws<^OpmjV}YvyyDc!6 zN0ZF@jp20DA4wm>ngE})!1q!8EN~fKJ#B$Ikv?F7y8xfDz~3bmXSj`JOZdltvjHm- zewFG!JevP)z&RH9H-KG$6D0gQ!1)&VFVsE@oP-zW15TIlG1RwoxQXw7F>EB#_d?-v z%k&EY_a{fcM||xhDP%JL9Kff}iTN;+X8n{dsCwI`ypVDCH?0JEK4!zZwz}K@KH^z3p*(?fUuYf&i(e}Opc(Db34{))i zJaU|~EG^o;-xEAKn*L|NrK6+YOB{y&u)t#h`z`Q9z-ujV0pLbUc}oDdSm08?TP*o& z0Jo2hj<0(F2QB%39{_yNlK(Nlhb{0vz(+0b{{(Qt`O)-O06Wi*mUjqnx+R_K7Xwj! z^B9!}_zRAo6!v?9KSQ4~0ViA5J7_2PPs0S9!P7^xv%_NbaXuee))NvFqOc*qjeNgVqm9E%4V8qOh4iFm}-Thh=`F_q#3Wu7oImY`*WX=<8P}i1iu8g0EAN z_NTEW2~k)9oGAC`Bl<`}6gJC08F_ycYfXs43gBeRdJxTbdlJrF-*CPbE&sV8($~Kp zP5)U!Ec{-Abw%k9;SYy+f6FhT`A-g!KL3}|^e=`;f8pI|`tZba)BBEC`g@4WzpD24 zuS}AEef9nFYm;QJWyU+2aDM^i-^cjvO|r){vp z0_7X>F1GCP`c%svZ_l=D`|LEH$K#holfwMgrKR#eVPMSvw1Hn}Rr3#@=Ks;be|PYI z&Yd>C)xn1y{80nrW5u46pBor$cxtM8p56v~J+ia1-!!cEk2~d`bnsISzU1KFIrtR^ zpV|fWzA#bNH?hmC{}$2QW%Iww!FwHi(vf%8$?rKhckqJ_{;Gp7I`|m_<8AHpPX5ad zzUtt~DI34j!M8j3gkdk#&sirwcJTdw4t{vbw*TW!{_h?8d}oT@zuctyHzj`T@IU9^ zmmK`P-H!d8{8yd)T@Jp@!QO7$z8{;a-Z#AsX5YNS|7%Cz$9L27iMmfsoSgMl$yY0+J(0?)h;nFNFT8>?fF(K$><5>xuJ4 zl*=p{Bz`Q$qd4kq2=ddCDzu1DZdH6vUbRxW01Nz}U7{a?T#b?dy(BQ&R8Hv}s89@} zIF7QgH%fv`P#lt=a;{*n3#mPSPPpRpMOXk(q2+X)82QTsFC*XTK&ZOS#rd zax=3s$iqZJ%b*qH*)6rFno7*P1DdIyj)qc22{a{i(G5ZVe0 z(1FHEQGSoyywXdD29YItfT8oK2!*K-l_Di7RvTUQMU2ULNtQ7Z#GvrgK#rqvSe^!I z<|uC`_F5Fmg)ZBAlnr z6LkL~6H&u);73p-Uy~HLq3XCdXK6&os#7Ak^U9?eJvM2AWa_ks;W$KOhQ}zIto#gl zHGol`%XH8c6y!v#<>+-6Wh+*-AS;UX#2Bx@Fhxs%5I}bZEym*jg_%~)O-^(3}Mlr2tXS9If94DNf6L*|HK}E)lvi4Y=8HuMiX*U21Oubrjq*%@ZrG7j=IVlu5z?d#Msg63x<2S8 zAElY}i*@wa2D?HBLFSemYCdytQ0Pj!K~r4@#c;R*MGT}2WU&Gm%3%=){dw4Z$Rdc= zvItK-K9$*jYJ_8ZZI%|~Y$bxhr;7POkQH4nspUj#MnZ2m4*GK%6$uOq1NWo#BFTb& z7O52cYECK%FcdH)SQ`d1PS$n9AUTLSh~&w$3ri>Di93!m#u%-K1Bj>dFhz8<*CIpM;8|5nUg2aomiI33r81E zpoIs2xusUSRROwGTYU_x_E`NX$OL*I+9L%O0}ByeVf$ugY?v*;A=FcT|3^+Oo<4d^ z&Uqb?`mmKYFc=CK?J{M)Bl_7!Vip0dN8KK+UCI^QWQLs*G|1xat^1|7ypaK%`6xCy zPcJXYYIOn4;@YyT0=G!n%`&c13Jil`6dZ6B)SzAbN zykRk@Xa{r~<1jisgjE*fbd1LN!H%g6YBg8|+6>(d3bR;|zy)O0NB}7z3^XGJc1Mxd93(~5kU{GdiYUM&2<)&T=<+W@iT=1iOY&8MxX{iNonvANkRoQia zyN?N*pdcAyvr!0L9R;>MTdKz=#OSernzAgWvUf!ew`7262y>Z3SG;RO=2hljXO`TGfR6Ijt=L;<_3d) z5_{qVv3dkxGWoFiGV6lv*O~!s{tn(Dm>TC@pOGRw=wa@6;TO!KepmEa0vm2`kQ83G zh~i)$+;DhwPa;$*+)1nmFW5*x0{!MG<**;N1tjFbT{7~~2`+EjWNQbTxu>T>L* zBekk{3ul)+Sb>6d;e{)-&*2s=96h}cpR^Df!{!!ms=R9ZL!$IKic=M)Bh^ZO2>V%Y zFv=-Qcre6-hfgGMuc+$vbGW7J7P*RSdt-#J9=G!w0sPJ%k3H5qDI$+P`Bi-QO{DhM z)ui88;-`>*yLN4~jr{`fxW|xxa}ys&1o;bZV}AiW+BcJL@r0;+@IN9d-+S~Uz~fun zHrIP6`ERIt(Z2wXuN(Dl;{)UDz0Q2-cYp`Ji@BL)X@2~kbdt-UKLQ@uXR{ypYO`Rz zqwJ@E$E(cu4ukJ=EdMS1eLEgIx6t#3!KeLpcTCuPcznU&!{?uPeEBDTo8>)Z@Zozt zJa%kR?<0nN@%b_yA9_#4r^`G>eB;lv-vAyvMAa89?`wuUeBX)3OGWoKZ*}-CAGY<<_&5Bz{`JU^x zL&*Fm+COvu-EYEwI5Oq@=RMaO&VS8_A@tvP>fg}(pFi{gXa3I}Iy7I;|No)+kMpLC z#MCasv}0ltU{vz<%p%-F#yT7-?z=JDACGTPx;d?HcsAdS1=YjlyV6*#71T0J-b diff --git a/README.md b/README.md index 199626a..53e5fe1 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Only the own service is modified. Optional Auto PicCap can configure its receive ## Installation -The current package is [HyperDRM_0.11.0_arm.ipk](HyperDRM_0.11.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow. +The current package is [HyperDRM_0.12.0_arm.ipk](HyperDRM_0.12.0_arm.ipk). Install it using the TV's webOS/Homebrew package installation workflow. Elevate the installed own service through Homebrew Channel: @@ -192,3 +192,20 @@ service, verifies UID 0, and retries the switch. Root and boot-hook failures hav separate messages. The explicit launch action `{"action":"repairRoot"}` invokes the same visible repair flow. Homebrew Channel must be installed and permit the request; denied elevation is reported without changing autostart. + +### Development and releases + +`VERSION` is the single source for app, package and native runtime versions. +Run `python sync_version.py` after editing it; `--check` rejects stale metadata. +Run `python release.py` to execute every JavaScript and portable native test, +compile the ARM service, build a reproducible IPK and verify its embedded versions, +ARM architecture, size and SHA-256 against the manifest. On Windows this requires +Node.js and Debian WSL with GCC, JSON-C and arm-linux-gnueabi-gcc; on Linux the +same tools must be on PATH. No TV connection or installation occurs. + +After reviewing the working tree, `python release.py --push` also commits changes, +creates a new version tag and atomically pushes main and the tag to origin. +An existing tag or a branch other than main is rejected. Attach the exact verified +IPK to the matching Gitea release using an authorized session, then compare the +downloaded attachment's size and SHA-256. Git access does not authorize web uploads. +See [runtime architecture](docs/architecture.md) for module boundaries. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..ac454c6 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.12.0 diff --git a/app/app.js b/app/app.js index c7b9e13..16353ea 100644 --- a/app/app.js +++ b/app/app.js @@ -1,117 +1,352 @@ 'use strict'; -var pending=[]; -var targetDirty=false,receiver='Hyperion'; -var profileDirty=false,profile='auto',advancedProfile=false; -var profileNames={auto:'Auto',g4:'LG G4 (measured)',average:'Average (uniform)',custom:'Custom model'}; -function showProfile(){ - document.getElementById('profile').textContent='Profile: '+profileNames[profile]; - var visible=profile==='custom' && advancedProfile; - document.getElementById('customEditor').style.display=visible?'block':'none'; - document.getElementById('customProfile').disabled=!visible; - document.getElementById('editProfile').disabled=profile!=='custom'; - document.getElementById('editProfile').textContent=visible?'Hide profile JSON':'Edit profile JSON'; +var pending = []; +var actionError = ''; + +function showActionError(message) { + actionError = message || ''; + document.getElementById('error').textContent = actionError; } -function call(method,done,data){callUri('luna://org.webosbrew.histogramhyperion.service/'+method,done,data);} -function callUri(uri,done,data){ - if(typeof PalmServiceBridge==='undefined'){done({returnValue:false,errorText:'This app requires an LG TV with the native service installed.'});return;} - var bridge=new PalmServiceBridge(),timer,finished=false; - function finish(value){if(finished)return;finished=true;clearTimeout(timer);var i=pending.indexOf(bridge);if(i>=0)pending.splice(i,1);done(value);} - bridge.onservicecallback=function(raw){try{finish(JSON.parse(raw));}catch(e){finish({returnValue:false,errorText:'Invalid service response'});}}; - pending.push(bridge);timer=setTimeout(function(){bridge.cancel();finish({returnValue:false,errorText:'The service is not responding. Check the installation and service log.'});},uri.indexOf('hbchannel')>=0?30000:10000); - bridge.call(uri,JSON.stringify(data||{})); +function clearActionError() { + showActionError(''); } -function display(result){ - if(typeof result.uid==='number'){ - document.getElementById('grantRoot').style.display=result.uid===0?'none':'inline-block'; - document.getElementById('rootHint').style.display=result.uid===0?'none':'block'; - } - if(result.calibrationProfile && !profileDirty){ - profile=result.calibrationProfile;showProfile(); - document.getElementById('customProfile').value=JSON.stringify({model:result.customModel||result.tvModel||'TV-MODEL',points:result.customPoints&&result.customPoints.length?result.customPoints:[]},null,2); - } - if(result.effectiveCalibration)document.getElementById('profileStatus').textContent='TV: '+(result.tvModel||'Unknown')+' · Active profile: '+profileNames[result.effectiveCalibration]+(result.effectiveCalibration==='average'?' · Spatial calibration needed for this model':''); +var targetDirty = false, + receiver = 'Hyperion'; +var profileDirty = false, + profile = 'auto', + advancedProfile = false; +var profileNames = { + auto: 'Auto', + g4: 'LG G4 (measured)', + average: 'Average (uniform)', + custom: 'Custom model' +}; - if(result.host && !targetDirty){ - document.getElementById('host').value=result.host;document.getElementById('port').value=result.port;document.getElementById('piccapPort').value=result.piccapPort||19400;receiver=result.receiver; - document.getElementById('receiver').textContent='Receiver: '+receiver; - } - document.getElementById('token').placeholder=result.tokenSet?'Saved token (leave blank to keep)':'No token saved'; - if(result.endpoint)document.getElementById('endpoint').textContent=result.receiver+' · '+result.endpoint+' · Priority 149 · 10 updates per second'; - var t=result.connectionTest; - if(t)document.getElementById('testResult').textContent='Connection test: '+t.state+(t.state==='Passed'?' · '+t.ledCount+' LEDs · '+t.elapsedMs+' ms':'')+(t.error?' · '+t.error:''); - document.getElementById('testConnection').disabled=!!t && ['Connecting','Sending request','Checking authentication','Reading LED layout'].indexOf(t.state)>=0; - if(result.version)document.getElementById('diagnostics').textContent='Version '+result.version+' · '+(result.uid===0?'Root service':'Service needs root elevation')+' · '+result.captureState+' · Playback: '+result.playbackProtection+' · Capture recoveries: '+result.captureRecoveries+' (last: '+result.lastRecoveryMs+' ms)'+(result.lastOutputAgeMs>=0?' · Last histogram output: '+result.lastOutputAgeMs+' ms ago':'')+(result.retryScheduled?' · Retry in '+result.retryInMs+' ms · Attempts: '+result.retryAttempts:'')+(result.piccapError?' · '+result.piccapError:'')+(result.piccapCooldownMs?' · PicCap retry in '+Math.ceil(result.piccapCooldownMs/1000)+' s':'')+' · Layout updates: '+(result.layoutUpdates||0)+(result.layoutError?' · Layout: '+result.layoutError:''); - var handover=document.getElementById('handover');handover.disabled=!result.piccapInstalled; - handover.setAttribute('aria-checked',String(!!result.piccapHandover)); - handover.textContent=result.piccapInstalled?'Auto PicCap: '+(result.piccapHandover?'On':'Off'):'Auto PicCap: Not installed'; - if(typeof result.autostart==='boolean'){var toggle=document.getElementById('autostart');toggle.setAttribute('aria-checked',String(result.autostart));toggle.textContent='Autostart: '+(result.autostart?'On':'Off');} - document.getElementById('error').textContent=result.errorText||result.error||''; - document.getElementById('status').textContent=result.returnValue===false?'Service unavailable':(result.running?'Running':'Stopped')+' · '+(result.frames||0)+' acknowledged color updates'; - if(result.activeSource==='PicCap'){ - document.getElementById('status').textContent='PicCap · '+(result.running?'Running':'Switching capture source')+' · '+(result.piccapConnected?'Receiver connected':'Waiting for receiver'); - document.getElementById('regions').textContent='';return; - } - if(result.piccapHandover)document.getElementById('status').textContent+=' · HyperDRM · '+result.playbackProtection; - if(result.piccapHandover && !result.piccapTargetCompatible)document.getElementById('status').textContent+=' · Auto PicCap unavailable for remote IPv6 target'; - if(result.colorValid && result.effectiveCalibration==='average')document.getElementById('status').textContent+=' · Average color output'; - if(result.colorValid)document.getElementById('status').textContent+=result.spatialOutput?' · '+result.ledCount+' LEDs from 16 color regions'+(result.receiver==='HyperHDR'?' · Spatial image output':''):' · Output RGB: '+result.outputColor.join(', '); - else if(result.returnValue!==false)document.getElementById('status').textContent+=' · No current color output'; - if(result.blackBarDetection && result.running)document.getElementById('status').textContent+=result.letterboxDetected?' · Top/bottom black bars detected':' · Full-screen capture'; - if(result.spatialInterpolation)document.getElementById('status').textContent+=' · Smooth color transitions'; - if(result.holdingColors)document.getElementById('status').textContent+=' · Holding last valid colors (missing data)'; - if(result.sourceFade)document.getElementById('status').textContent+=' · Blending capture sources'; - if(result.retryScheduled)document.getElementById('status').textContent+=' · Retrying receiver or capture startup'; - if(result.retryPaused)document.getElementById('status').textContent+=' · Automatic retry paused until Start'; - var box=document.getElementById('regions');box.textContent=''; - (result.regions||[]).forEach(function(rgb,index){var e=document.createElement('div');e.className='swatch';e.style.backgroundColor='rgb('+rgb.join(',')+')';e.title='Histogram '+index;box.appendChild(e);}); +function showProfile() { + document.getElementById('profile').textContent = 'Profile: ' + profileNames[profile]; + var visible = profile === 'custom' && advancedProfile; + document.getElementById('customEditor').style.display = visible ? 'block' : 'none'; + document.getElementById('customProfile').disabled = !visible; + document.getElementById('editProfile').disabled = profile !== 'custom'; + document.getElementById('editProfile').textContent = visible ? 'Hide profile JSON' : 'Edit profile JSON'; } -document.getElementById('start').onclick=function(){call('start',function(r){if(r.returnValue===false)display(r);else call('status',display);});}; -document.getElementById('stop').onclick=function(){call('stop',function(){call('status',display);});}; -document.getElementById('refresh').onclick=function(){call('status',display);}; -call('status',display);setInterval(function(){if(!pending.length)call('status',display);},3000); + +function call(method, done, data) { + if (['start', 'stop', 'autostart', 'handover', 'settings', 'testConnection', 'calibration'].indexOf(method) >= 0) clearActionError(); + callUri('luna://org.webosbrew.histogramhyperion.service/' + method, function(result) { + if (method !== 'status' && result.returnValue === false) showActionError(result.errorText || 'The action failed.'); + done(result); + }, data); +} + +function callUri(uri, done, data) { + if (typeof PalmServiceBridge === 'undefined') { + done({ + returnValue: false, + errorText: 'This app requires an LG TV with the native service installed.' + }); + return; + } + var bridge = new PalmServiceBridge(), + timer, finished = false; + + function finish(value) { + if (finished) return; + finished = true; + clearTimeout(timer); + var i = pending.indexOf(bridge); + if (i >= 0) pending.splice(i, 1); + done(value); + } + bridge.onservicecallback = function(raw) { + try { + finish(JSON.parse(raw)); + } catch (e) { + finish({ + returnValue: false, + errorText: 'Invalid service response' + }); + } + }; + pending.push(bridge); + timer = setTimeout(function() { + bridge.cancel(); + finish({ + returnValue: false, + errorText: 'The service is not responding. Check the installation and service log.' + }); + }, uri.indexOf('hbchannel') >= 0 ? 30000 : 10000); + bridge.call(uri, JSON.stringify(data || {})); +} + +function display(result) { + if (typeof result.uid === 'number') { + document.getElementById('grantRoot').style.display = result.uid === 0 ? 'none' : 'inline-block'; + document.getElementById('rootHint').style.display = result.uid === 0 ? 'none' : 'block'; + } + if (result.calibrationProfile && !profileDirty) { + profile = result.calibrationProfile; + showProfile(); + document.getElementById('customProfile').value = JSON.stringify({ + model: result.customModel || result.tvModel || 'TV-MODEL', + points: result.customPoints && result.customPoints.length ? result.customPoints : [] + }, null, 2); + } + if (result.effectiveCalibration) document.getElementById('profileStatus').textContent = 'TV: ' + (result.tvModel || 'Unknown') + ' · Active profile: ' + profileNames[result.effectiveCalibration] + (result.effectiveCalibration === 'average' ? ' · Spatial calibration needed for this model' : ''); + + if (result.host && !targetDirty) { + document.getElementById('host').value = result.host; + document.getElementById('port').value = result.port; + document.getElementById('piccapPort').value = result.piccapPort || 19400; + receiver = result.receiver; + document.getElementById('receiver').textContent = 'Receiver: ' + receiver; + } + document.getElementById('token').placeholder = result.tokenSet ? 'Saved token (leave blank to keep)' : 'No token saved'; + if (result.endpoint) document.getElementById('endpoint').textContent = result.receiver + ' · ' + result.endpoint + ' · Priority 149 · 10 updates per second'; + var t = result.connectionTest; + if (t) document.getElementById('testResult').textContent = 'Connection test: ' + t.state + (t.state === 'Passed' ? ' · ' + t.ledCount + ' LEDs · ' + t.elapsedMs + ' ms' : '') + (t.error ? ' · ' + t.error : ''); + document.getElementById('testConnection').disabled = !!t && ['Connecting', 'Sending request', 'Checking authentication', 'Reading LED layout'].indexOf(t.state) >= 0; + if (result.version) document.getElementById('diagnostics').textContent = 'Version ' + result.version + ' · ' + (result.uid === 0 ? 'Root service' : 'Service needs root elevation') + ' · ' + result.captureState + ' · Playback: ' + result.playbackProtection + ' · Capture recoveries: ' + result.captureRecoveries + ' (last: ' + result.lastRecoveryMs + ' ms)' + (result.lastOutputAgeMs >= 0 ? ' · Last histogram output: ' + result.lastOutputAgeMs + ' ms ago' : '') + (result.retryScheduled ? ' · Retry in ' + result.retryInMs + ' ms · Attempts: ' + result.retryAttempts : '') + (result.piccapError ? ' · ' + result.piccapError : '') + (result.piccapCooldownMs ? ' · PicCap retry in ' + Math.ceil(result.piccapCooldownMs / 1000) + ' s' : '') + ' · Layout updates: ' + (result.layoutUpdates || 0) + (result.layoutError ? ' · Layout: ' + result.layoutError : ''); + var handover = document.getElementById('handover'); + handover.disabled = !result.piccapInstalled; + handover.setAttribute('aria-checked', String(!!result.piccapHandover)); + handover.textContent = result.piccapInstalled ? 'Auto PicCap: ' + (result.piccapHandover ? 'On' : 'Off') : 'Auto PicCap: Not installed'; + if (typeof result.autostart === 'boolean') { + var toggle = document.getElementById('autostart'); + toggle.setAttribute('aria-checked', String(result.autostart)); + toggle.textContent = 'Autostart: ' + (result.autostart ? 'On' : 'Off'); + } + document.getElementById('error').textContent = actionError || result.errorText || result.error || ''; + document.getElementById('status').textContent = result.returnValue === false ? 'Service unavailable' : (result.running ? 'Running' : 'Stopped') + ' · ' + (result.frames || 0) + ' acknowledged color updates'; + if (result.activeSource === 'PicCap') { + document.getElementById('status').textContent = 'PicCap · ' + (result.running ? 'Running' : 'Switching capture source') + ' · ' + (result.piccapConnected ? 'Receiver connected' : 'Waiting for receiver'); + document.getElementById('regions').textContent = ''; + return; + } + if (result.piccapHandover) document.getElementById('status').textContent += ' · HyperDRM · ' + result.playbackProtection; + if (result.piccapHandover && !result.piccapTargetCompatible) document.getElementById('status').textContent += ' · Auto PicCap unavailable for remote IPv6 target'; + if (result.colorValid && result.effectiveCalibration === 'average') document.getElementById('status').textContent += ' · Average color output'; + if (result.colorValid) document.getElementById('status').textContent += result.spatialOutput ? ' · ' + result.ledCount + ' LEDs from 16 color regions' + (result.receiver === 'HyperHDR' ? ' · Spatial image output' : '') : ' · Output RGB: ' + result.outputColor.join(', '); + else if (result.returnValue !== false) document.getElementById('status').textContent += ' · No current color output'; + if (result.blackBarDetection && result.running) document.getElementById('status').textContent += result.letterboxDetected ? ' · Top/bottom black bars detected' : ' · Full-screen capture'; + if (result.spatialInterpolation) document.getElementById('status').textContent += ' · Smooth color transitions'; + if (result.holdingColors) document.getElementById('status').textContent += ' · Holding last valid colors (missing data)'; + if (result.sourceFade) document.getElementById('status').textContent += ' · Blending capture sources'; + if (result.retryScheduled) document.getElementById('status').textContent += ' · Retrying receiver or capture startup'; + if (result.retryPaused) document.getElementById('status').textContent += ' · Automatic retry paused until Start'; + var box = document.getElementById('regions'); + box.textContent = ''; + (result.regions || []).forEach(function(rgb, index) { + var e = document.createElement('div'); + e.className = 'swatch'; + e.style.backgroundColor = 'rgb(' + rgb.join(',') + ')'; + e.title = 'Histogram ' + index; + box.appendChild(e); + }); +} +document.getElementById('start').onclick = function() { + call('start', function(r) { + if (r.returnValue === false) display(r); + else call('status', display); + }); +}; +document.getElementById('stop').onclick = function() { + call('stop', function() { + call('status', display); + }); +}; +document.getElementById('refresh').onclick = function() { + call('status', display); +}; +call('status', display); +setInterval(function() { + if (!pending.length) call('status', display); +}, 3000); document.getElementById('start').focus(); -var repairingRoot=false,rootWaiters=[]; -function repairRoot(done){ - rootWaiters.push(done);if(repairingRoot)return;repairingRoot=true;document.getElementById('grantRoot').disabled=true; - document.getElementById('error').textContent='Granting root access to HyperDRM...'; - function finish(error){repairingRoot=false;document.getElementById('grantRoot').disabled=false;if(error)document.getElementById('error').textContent=error;var waiters=rootWaiters;rootWaiters=[];waiters.forEach(function(callback){callback(!error);});} - callUri('luna://org.webosbrew.hbchannel.service/elevateService',function(r){ - if(r.returnValue===false){finish('Homebrew Channel could not grant root access: '+(r.errorText||'Check Homebrew Channel installation.'));return;} - call('restartService',function(r){ - if(r.returnValue===false){finish(r.errorText||'Could not restart the HyperDRM service.');return;} - var attempts=0; - function check(){call('status',function(s){if(s.uid===0){display(s);finish();}else if(++attempts<12)setTimeout(check,1000);else finish('The service is still missing root access. Elevate HyperDRM in Homebrew Channel and reopen the app.');});} - setTimeout(check,1500); - }); - },{id:'org.webosbrew.histogramhyperion.service'}); +var repairingRoot = false, + rootWaiters = []; + +function repairRoot(done) { + clearActionError(); + rootWaiters.push(done); + if (repairingRoot) return; + repairingRoot = true; + document.getElementById('grantRoot').disabled = true; + showActionError('Granting root access to HyperDRM...'); + + function finish(error) { + repairingRoot = false; + document.getElementById('grantRoot').disabled = false; + if (error) showActionError(error); + var waiters = rootWaiters; + rootWaiters = []; + waiters.forEach(function(callback) { + callback(!error); + }); + } + callUri('luna://org.webosbrew.hbchannel.service/elevateService', function(r) { + if (r.returnValue === false) { + finish('Homebrew Channel could not grant root access: ' + (r.errorText || 'Check Homebrew Channel installation.')); + return; + } + call('restartService', function(r) { + if (r.returnValue === false) { + finish(r.errorText || 'Could not restart the HyperDRM service.'); + return; + } + var attempts = 0; + + function check() { + call('status', function(s) { + if (s.uid === 0) { + display(s); + finish(); + } else if (++attempts < 12) setTimeout(check, 1000); + else finish('The service is still missing root access. Elevate HyperDRM in Homebrew Channel and reopen the app.'); + }); + } + setTimeout(check, 1500); + }); + }, { + id: 'org.webosbrew.histogramhyperion.service' + }); } -document.getElementById('grantRoot').onclick=function(){repairRoot(function(ok){if(ok)call('status',display);});}; -document.getElementById('autostart').onclick=function(){ - var button=this,enabled=button.getAttribute('aria-checked')!=='true';button.disabled=true; - function save(){call('autostart',function(r){ - if(r.rootRequired){repairRoot(function(ok){if(ok)save();else button.disabled=false;});return;} - button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display); - },{enabled:enabled});} - save(); +document.getElementById('grantRoot').onclick = function() { + repairRoot(function(ok) { + if (ok) call('status', display); + }); +}; +document.getElementById('autostart').onclick = function() { + var button = this, + enabled = button.getAttribute('aria-checked') !== 'true'; + button.disabled = true; + + function save() { + call('autostart', function(r) { + if (r.rootRequired) { + repairRoot(function(ok) { + if (ok) save(); + else button.disabled = false; + }); + return; + } + button.disabled = false; + if (r.returnValue === false) showActionError(r.errorText); + else call('status', display); + }, { + enabled: enabled + }); + } + save(); }; -document.getElementById('handover').onclick=function(){var button=this;button.disabled=true;call('handover',function(r){button.disabled=false;if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:button.getAttribute('aria-checked')!=='true'});}; -['host','port','piccapPort','token','clearToken'].forEach(function(id){document.getElementById(id).addEventListener('input',function(){targetDirty=true;});}); -document.getElementById('receiver').onclick=function(){receiver=receiver==='Hyperion'?'HyperHDR':'Hyperion';this.textContent='Receiver: '+receiver;targetDirty=true;}; -document.getElementById('saveTarget').onclick=function(){ - var port=Number(document.getElementById('port').value),button=this; - if(!Number.isInteger(port)||port<1||port>65535){document.getElementById('error').textContent='JSON port must be an integer from 1 to 65535.';return;} - var flatPort=Number(document.getElementById('piccapPort').value); - if(!Number.isInteger(flatPort)||flatPort<1||flatPort>65535){document.getElementById('error').textContent='PicCap FlatBuffer port must be an integer from 1 to 65535.';return;} - var settings={piccapPort:flatPort,host:document.getElementById('host').value.trim(),port:port,receiver:receiver,clearToken:document.getElementById('clearToken').checked}; - var token=document.getElementById('token').value.trim();if(token)settings.token=token; - button.disabled=true;call('settings',function(r){button.disabled=false;if(r.returnValue===false){document.getElementById('error').textContent=r.errorText;return;}targetDirty=false;document.getElementById('token').value='';document.getElementById('clearToken').checked=false;call('status',display);},settings); +document.getElementById('handover').onclick = function() { + var button = this; + button.disabled = true; + call('handover', function(r) { + button.disabled = false; + if (r.returnValue === false) showActionError(r.errorText); + else call('status', display); + }, { + enabled: button.getAttribute('aria-checked') !== 'true' + }); }; -document.getElementById('testConnection').onclick=function(){if(targetDirty){document.getElementById('error').textContent='Save receiver settings before testing the connection.';return;}this.disabled=true;call('testConnection',function(r){if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;call('status',display);});}; -document.getElementById('profile').onclick=function(){var keys=['auto','g4','average','custom'];profile=keys[(keys.indexOf(profile)+1)%keys.length];profileDirty=true;showProfile();}; -document.getElementById('customProfile').addEventListener('input',function(){profileDirty=true;}); -document.getElementById('saveProfile').onclick=function(){var data={profile:profile},button=this;if(profile==='custom'){try{var parsed=JSON.parse(document.getElementById('customProfile').value);data.model=parsed.model;data.points=parsed.points;}catch(e){document.getElementById('error').textContent='Custom profile must be valid JSON.';return;}}button.disabled=true;call('calibration',function(r){button.disabled=false;if(r.returnValue===false){document.getElementById('error').textContent=r.errorText;return;}profileDirty=false;call('status',display);},data);}; -document.getElementById('editProfile').onclick=function(){advancedProfile=!advancedProfile;showProfile();if(advancedProfile)document.getElementById('customProfileField').focus();}; -setupRemoteNavigation(['overviewTab','receiverTab','calibrationTab','grantRoot','start','stop','refresh','autostart','handover','calibrationShortcut','receiver','hostField','portField','piccapPortField','tokenField','clearToken','saveTarget','testConnection','calibrate','validateCalibration','profile','saveProfile','editProfile','customProfileField','cancelCalibration'].map(function(id){return document.getElementById(id);}),document,window); +['host', 'port', 'piccapPort', 'token', 'clearToken'].forEach(function(id) { + document.getElementById(id).addEventListener('input', function() { + targetDirty = true; + }); +}); +document.getElementById('receiver').onclick = function() { + receiver = receiver === 'Hyperion' ? 'HyperHDR' : 'Hyperion'; + this.textContent = 'Receiver: ' + receiver; + targetDirty = true; +}; +document.getElementById('saveTarget').onclick = function() { + var port = Number(document.getElementById('port').value), + button = this; + if (!Number.isInteger(port) || port < 1 || port > 65535) { + showActionError('JSON port must be an integer from 1 to 65535.'); + return; + } + var flatPort = Number(document.getElementById('piccapPort').value); + if (!Number.isInteger(flatPort) || flatPort < 1 || flatPort > 65535) { + showActionError('PicCap FlatBuffer port must be an integer from 1 to 65535.'); + return; + } + var settings = { + piccapPort: flatPort, + host: document.getElementById('host').value.trim(), + port: port, + receiver: receiver, + clearToken: document.getElementById('clearToken').checked + }; + var token = document.getElementById('token').value.trim(); + if (token) settings.token = token; + button.disabled = true; + call('settings', function(r) { + button.disabled = false; + if (r.returnValue === false) { + showActionError(r.errorText); + return; + } + targetDirty = false; + document.getElementById('token').value = ''; + document.getElementById('clearToken').checked = false; + call('status', display); + }, settings); +}; +document.getElementById('testConnection').onclick = function() { + if (targetDirty) { + showActionError('Save receiver settings before testing the connection.'); + return; + } + this.disabled = true; + call('testConnection', function(r) { + if (r.returnValue === false) showActionError(r.errorText); + call('status', display); + }); +}; +document.getElementById('profile').onclick = function() { + var keys = ['auto', 'g4', 'average', 'custom']; + profile = keys[(keys.indexOf(profile) + 1) % keys.length]; + profileDirty = true; + showProfile(); +}; +document.getElementById('customProfile').addEventListener('input', function() { + profileDirty = true; +}); +document.getElementById('saveProfile').onclick = function() { + var data = { + profile: profile + }, + button = this; + if (profile === 'custom') { + try { + var parsed = JSON.parse(document.getElementById('customProfile').value); + data.model = parsed.model; + data.points = parsed.points; + } catch (e) { + showActionError('Custom profile must be valid JSON.'); + return; + } + } + button.disabled = true; + call('calibration', function(r) { + button.disabled = false; + if (r.returnValue === false) { + showActionError(r.errorText); + return; + } + profileDirty = false; + call('status', display); + }, data); +}; +document.getElementById('editProfile').onclick = function() { + advancedProfile = !advancedProfile; + showProfile(); + if (advancedProfile) document.getElementById('customProfileField').focus(); +}; +setupRemoteNavigation(['overviewTab', 'receiverTab', 'calibrationTab', 'grantRoot', 'start', 'stop', 'refresh', 'autostart', 'handover', 'calibrationShortcut', 'receiver', 'hostField', 'portField', 'piccapPortField', 'tokenField', 'clearToken', 'saveTarget', 'testConnection', 'calibrate', 'validateCalibration', 'profile', 'saveProfile', 'editProfile', 'customProfileField', 'cancelCalibration'].map(function(id) { + return document.getElementById(id); +}), document, window); diff --git a/app/appinfo.json b/app/appinfo.json index 11e45ad..da045a5 100644 --- a/app/appinfo.json +++ b/app/appinfo.json @@ -1 +1,14 @@ -{"id": "org.webosbrew.histogramhyperion", "version": "0.11.0", "vendor": "Local development", "type": "web", "main": "index.html", "title": "HyperDRM", "icon": "icon.png", "resolution": "1920x1080", "disableBackHistoryAPI": true, "largeIcon": "large-icon.png", "appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR", "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"} +{ + "id": "org.webosbrew.histogramhyperion", + "version": "0.12.0", + "vendor": "Local development", + "type": "web", + "main": "index.html", + "title": "HyperDRM", + "icon": "icon.png", + "resolution": "1920x1080", + "disableBackHistoryAPI": true, + "largeIcon": "large-icon.png", + "appDescription": "Ambient lighting for webOS, Hyperion and HyperHDR", + "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM" +} diff --git a/app/calibration.js b/app/calibration.js index 54084ef..1940c1c 100644 --- a/app/calibration.js +++ b/app/calibration.js @@ -1,35 +1,81 @@ /* Pure measurement processing; the video uses 32 clockwise edge patches. */ -function calibrationFit(baseline,patches,model){ - if(!model || baseline.length!==16 || patches.length!==32)throw new Error('Incomplete calibration measurements.'); - var candidates=[],edges=[0,0,0,0]; - for(var region=0;region<16;region++){ - var responses=patches.map(function(p){if(!p || p.length!==16)throw new Error('Missing video measurements.');return Math.max(0,p[region]-baseline[region]);}); - var peak=Math.max.apply(Math,responses),best=responses.indexOf(peak); - if(peak<8)continue; - // Reject regions responding equally around the screen (global histograms). - var strong=responses.filter(function(v){return v>=peak*.65;}).length; - if(strong>10)continue; - var distant=responses.some(function(v,i){var d=Math.min(Math.abs(i-best),32-Math.abs(i-best));return v>=peak*.65 && d>6;}); - if(distant)continue; - var sum=0,weighted=0; - for(var offset=-3;offset<=3;offset++){ - var index=(best+offset+32)%32,weight=Math.max(0,responses[index]-peak*.35); - weighted+=offset*weight;sum+=weight; - } - var position=((best+.5+weighted/sum)/8+4)%4; - candidates.push({position:position,region:region,score:peak/strong}); - } - // Nested LG regions can measure the same edge position. Keep the most - // localized response rather than making duplicate interpolation anchors. - candidates.sort(function(a,b){return b.score-a.score || a.region-b.region;}); - var points=[]; - candidates.forEach(function(p){if(points.some(function(q){var d=Math.abs(p.position-q.position);return Math.min(d,4-d)<.06;}))return;points.push({position:p.position,region:p.region});edges[Math.floor(p.position)]++;}); - points.sort(function(a,b){return a.position-b.position;}); - if(points.length<4 || edges.some(function(n){return !n;}))throw new Error('No reliable spatial response on every edge. Keep the video full-screen; this TV may not expose local histogram regions. Your saved profile is unchanged.'); - return {model:model,points:points}; +function calibrationFit(baseline, patches, model) { + if (!model || baseline.length !== 16 || patches.length !== 32) throw new Error('Incomplete calibration measurements.'); + var candidates = [], + edges = [0, 0, 0, 0]; + for (var region = 0; region < 16; region++) { + var responses = patches.map(function(p) { + if (!p || p.length !== 16) throw new Error('Missing video measurements.'); + return Math.max(0, p[region] - baseline[region]); + }); + var peak = Math.max.apply(Math, responses), + best = responses.indexOf(peak); + if (peak < 8) continue; + // Reject regions responding equally around the screen (global histograms). + var strong = responses.filter(function(v) { + return v >= peak * .65; + }).length; + if (strong > 10) continue; + var distant = responses.some(function(v, i) { + var d = Math.min(Math.abs(i - best), 32 - Math.abs(i - best)); + return v >= peak * .65 && d > 6; + }); + if (distant) continue; + var sum = 0, + weighted = 0; + for (var offset = -3; offset <= 3; offset++) { + var index = (best + offset + 32) % 32, + weight = Math.max(0, responses[index] - peak * .35); + weighted += offset * weight; + sum += weight; + } + var position = ((best + .5 + weighted / sum) / 8 + 4) % 4; + candidates.push({ + position: position, + region: region, + score: peak / strong + }); + } + // Nested LG regions can measure the same edge position. Keep the most + // localized response rather than making duplicate interpolation anchors. + candidates.sort(function(a, b) { + return b.score - a.score || a.region - b.region; + }); + var points = []; + candidates.forEach(function(p) { + if (points.some(function(q) { + var d = Math.abs(p.position - q.position); + return Math.min(d, 4 - d) < .06; + })) return; + points.push({ + position: p.position, + region: p.region + }); + edges[Math.floor(p.position)]++; + }); + points.sort(function(a, b) { + return a.position - b.position; + }); + if (points.length < 4 || edges.some(function(n) { + return !n; + })) throw new Error('No reliable spatial response on every edge. Keep the video full-screen; this TV may not expose local histogram regions. Your saved profile is unchanged.'); + return { + model: model, + points: points + }; } -function calibrationLuma(regions){return regions.map(function(c){return (c[0]+c[1]+c[2])/3;});} -function calibrationAverage(samples){ - if(samples.length<3)throw new Error('Not enough fresh histogram samples. Keep the app visible and repeat calibration.'); - return samples[0].map(function(_,r){return samples.reduce(function(sum,s){return sum+s[r];},0)/samples.length;}); + +function calibrationLuma(regions) { + return regions.map(function(c) { + return (c[0] + c[1] + c[2]) / 3; + }); +} + +function calibrationAverage(samples) { + if (samples.length < 3) throw new Error('Not enough fresh histogram samples. Keep the app visible and repeat calibration.'); + return samples[0].map(function(_, r) { + return samples.reduce(function(sum, s) { + return sum + s[r]; + }, 0) / samples.length; + }); } diff --git a/app/navigation.js b/app/navigation.js index b6377bc..b4f7474 100644 --- a/app/navigation.js +++ b/app/navigation.js @@ -1,21 +1,67 @@ -function showPanel(name,focus){ - ['overview','receiver','calibration'].forEach(function(key){document.getElementById(key+'Panel').style.display=key===name?'block':'none';document.getElementById(key+'Tab').setAttribute('aria-pressed',String(key===name));}); - if(focus!==false)document.getElementById({overview:'start',receiver:'receiver',calibration:'calibrate'}[name]).focus(); +function showPanel(name, focus) { + ['overview', 'receiver', 'calibration'].forEach(function(key) { + document.getElementById(key + 'Panel').style.display = key === name ? 'block' : 'none'; + document.getElementById(key + 'Tab').setAttribute('aria-pressed', String(key === name)); + }); + if (focus !== false) document.getElementById({ + overview: 'start', + receiver: 'receiver', + calibration: 'calibrate' + } [name]).focus(); } -['overview','receiver','calibration'].forEach(function(name){document.getElementById(name+'Tab').onclick=function(){showPanel(name);};}); -document.getElementById('calibrationShortcut').onclick=function(){showPanel('calibration');}; -function setupFormEditors(ids,doc,win){ - var pairs=ids.map(function(id){return {input:doc.getElementById(id),field:doc.getElementById(id+'Field')};}); - pairs.forEach(function(pair){ - var input=pair.input,field=pair.field;input.readOnly=true; - field.onclick=function(){if(input.disabled || !input.readOnly)return;input.readOnly=false;input.focus();}; - input.addEventListener('blur',function(){input.readOnly=true;}); - input.addEventListener('mousedown',function(event){if(input.readOnly)event.preventDefault();}); - }); - function finishEditing(){ - pairs.forEach(function(pair){if(!pair.input.readOnly){pair.input.readOnly=true;pair.input.blur();pair.field.focus();}}); - } - doc.addEventListener('keyboardStateChange',function(event){var detail=event.detail||{};if(detail.visibility===false || detail.isShowing===false)finishEditing();}); - doc.addEventListener('keydown',function(event){if((event.keyCode===461 || event.key==='Escape') && pairs.some(function(pair){return !pair.input.readOnly;})){event.preventDefault();finishEditing();}}); +['overview', 'receiver', 'calibration'].forEach(function(name) { + document.getElementById(name + 'Tab').onclick = function() { + showPanel(name); + }; +}); +document.getElementById('calibrationShortcut').onclick = function() { + showPanel('calibration'); +}; + +function setupFormEditors(ids, doc, win) { + var pairs = ids.map(function(id) { + return { + input: doc.getElementById(id), + field: doc.getElementById(id + 'Field') + }; + }); + pairs.forEach(function(pair) { + var input = pair.input, + field = pair.field; + input.readOnly = true; + field.onclick = function() { + if (input.disabled || !input.readOnly) return; + input.readOnly = false; + input.focus(); + }; + input.addEventListener('blur', function() { + input.readOnly = true; + }); + input.addEventListener('mousedown', function(event) { + if (input.readOnly) event.preventDefault(); + }); + }); + + function finishEditing() { + pairs.forEach(function(pair) { + if (!pair.input.readOnly) { + pair.input.readOnly = true; + pair.input.blur(); + pair.field.focus(); + } + }); + } + doc.addEventListener('keyboardStateChange', function(event) { + var detail = event.detail || {}; + if (detail.visibility === false || detail.isShowing === false) finishEditing(); + }); + doc.addEventListener('keydown', function(event) { + if ((event.keyCode === 461 || event.key === 'Escape') && pairs.some(function(pair) { + return !pair.input.readOnly; + })) { + event.preventDefault(); + finishEditing(); + } + }); } -setupFormEditors(['host','port','piccapPort','token','customProfile'],document,window); +setupFormEditors(['host', 'port', 'piccapPort', 'token', 'customProfile'], document, window); diff --git a/app/remote.js b/app/remote.js index 72cbda6..ace8ec9 100644 --- a/app/remote.js +++ b/app/remote.js @@ -1,32 +1,49 @@ /* Directional control for webOS remotes and standard keyboards. */ function setupRemoteNavigation(buttons, doc, win) { - var lastFocused=buttons[0], enterDown=false; - buttons.forEach(function(button){ - button.addEventListener('focus',function(){lastFocused=button;}); - button.addEventListener('mouseenter',function(){if(!button.disabled)button.focus();}); - }); - doc.addEventListener('keydown',function(event){ - var code=event.keyCode||event.which; - var direction=event.key==='ArrowLeft'||event.key==='ArrowUp'||code===37||code===38?-1: - event.key==='ArrowRight'||event.key==='ArrowDown'||code===39||code===40?1:0; - var enter=event.key==='Enter'||code===13; - if(!direction&&!enter)return; - var editing=doc.activeElement && !doc.activeElement.readOnly && (doc.activeElement.tagName==='INPUT' && doc.activeElement.type!=='checkbox' || doc.activeElement.tagName==='TEXTAREA'); - if(editing)return; - event.preventDefault(); - var enabled=buttons.filter(function(button){return !button.disabled && (typeof button.getClientRects!=='function' || button.getClientRects().length) && (!win.remoteNavigationScope || win.remoteNavigationScope.indexOf(button.id)>=0);}); - if(!enabled.length)return; - var focused=doc.activeElement,index=enabled.indexOf(focused); - if(direction){ - if(index<0){index=enabled.indexOf(lastFocused);if(index<0)index=0;enabled[index].focus();} - else enabled[(index+direction+enabled.length)%enabled.length].focus(); - }else{ - if(enterDown||event.repeat)return; - enterDown=true; - if(index<0){index=enabled.indexOf(lastFocused);enabled[index<0?0:index].focus();} - else focused.click(); - } - }); - doc.addEventListener('keyup',function(event){if(event.key==='Enter'||event.keyCode===13||event.which===13)enterDown=false;}); - win.addEventListener('blur',function(){enterDown=false;}); + var lastFocused = buttons[0], + enterDown = false; + buttons.forEach(function(button) { + button.addEventListener('focus', function() { + lastFocused = button; + }); + button.addEventListener('mouseenter', function() { + if (!button.disabled) button.focus(); + }); + }); + doc.addEventListener('keydown', function(event) { + var code = event.keyCode || event.which; + var direction = event.key === 'ArrowLeft' || event.key === 'ArrowUp' || code === 37 || code === 38 ? -1 : + event.key === 'ArrowRight' || event.key === 'ArrowDown' || code === 39 || code === 40 ? 1 : 0; + var enter = event.key === 'Enter' || code === 13; + if (!direction && !enter) return; + var editing = doc.activeElement && !doc.activeElement.readOnly && (doc.activeElement.tagName === 'INPUT' && doc.activeElement.type !== 'checkbox' || doc.activeElement.tagName === 'TEXTAREA'); + if (editing) return; + event.preventDefault(); + var enabled = buttons.filter(function(button) { + return !button.disabled && (typeof button.getClientRects !== 'function' || button.getClientRects().length) && (!win.remoteNavigationScope || win.remoteNavigationScope.indexOf(button.id) >= 0); + }); + if (!enabled.length) return; + var focused = doc.activeElement, + index = enabled.indexOf(focused); + if (direction) { + if (index < 0) { + index = enabled.indexOf(lastFocused); + if (index < 0) index = 0; + enabled[index].focus(); + } else enabled[(index + direction + enabled.length) % enabled.length].focus(); + } else { + if (enterDown || event.repeat) return; + enterDown = true; + if (index < 0) { + index = enabled.indexOf(lastFocused); + enabled[index < 0 ? 0 : index].focus(); + } else focused.click(); + } + }); + doc.addEventListener('keyup', function(event) { + if (event.key === 'Enter' || event.keyCode === 13 || event.which === 13) enterDown = false; + }); + win.addEventListener('blur', function() { + enterDown = false; + }); } diff --git a/app/wizard.js b/app/wizard.js index 1f66c98..01f029c 100644 --- a/app/wizard.js +++ b/app/wizard.js @@ -1,87 +1,208 @@ 'use strict'; -var wizard=null; -var video=document.getElementById('calibrationVideo'); -function wizardFinish(error){ - if(!wizard)return; - clearInterval(wizard.timer);wizard=null;window.remoteNavigationScope=null;video.pause();video.style.display='none'; - if(typeof video.removeAttribute==='function'){video.removeAttribute('src');video.load();} - document.getElementById('cancelCalibration').disabled=true; - document.getElementById('calibrate').disabled=false;document.getElementById('validateCalibration').disabled=false; - call('calibrationSession',function(r){if(r.returnValue===false)document.getElementById('error').textContent=r.errorText;else call('status',display);},{enabled:false}); - call('calibrationResult',function(){},{message:error||document.getElementById('wizardStatus').textContent}); - if(error)document.getElementById('wizardStatus').textContent=error; - document.getElementById('calibrate').focus(); +var WizardPhase = { + STARTING: 'starting', + WAITING: 'waiting', + MEASURING: 'measuring', + VALIDATING: 'validating', + COMMITTING: 'committing' +}; +var wizard = null; +var video = document.getElementById('calibrationVideo'); + +function wizardFinish(error) { + if (!wizard) return; + clearInterval(wizard.timer); + wizard = null; + window.remoteNavigationScope = null; + video.pause(); + video.style.display = 'none'; + if (typeof video.removeAttribute === 'function') { + video.removeAttribute('src'); + video.load(); + } + document.getElementById('cancelCalibration').disabled = true; + document.getElementById('calibrate').disabled = false; + document.getElementById('validateCalibration').disabled = false; + call('calibrationSession', function(r) { + if (r.returnValue === false) document.getElementById('error').textContent = r.errorText; + else call('status', display); + }, { + enabled: false + }); + call('calibrationResult', function() {}, { + message: error || document.getElementById('wizardStatus').textContent + }); + if (error) document.getElementById('wizardStatus').textContent = error; + document.getElementById('calibrate').focus(); } -function wizardSample(){ - if(!wizard || wizard.busy)return; - if(Date.now()-wizard.started>100000){wizardFinish('Calibration timed out. Saved profile unchanged.');return;} - if(wizard.validation)return; - var t=video.currentTime; - if(t>=68){ - try{ - var measured=calibrationFit(calibrationAverage(wizard.baseline),wizard.patches.map(calibrationAverage),wizard.model); - profile='custom';profileDirty=true;showProfile();document.getElementById('customProfile').value=JSON.stringify(measured,null,2); - document.getElementById('wizardStatus').textContent='Measured '+measured.points.length+' edge regions. Select Save profile, then Validate profile to check all four colored edges. The previous saved profile is still active.'; - wizard.busy=true; - call('calibrationDraft',function(r){if(wizard)wizardFinish(r.returnValue===false?r.errorText:undefined);},{profile:'custom',model:measured.model,points:measured.points}); - }catch(e){wizardFinish(e.message);} - return; - } - var step=t<4?-1:Math.floor((t-4)/2),local=t<4?t:(t-4)%2; - if(step<0 && (t<.8 || t>3.2) || step>=0 && (local<.65 || local>1.75))return; - wizard.busy=true; - call('status',function(s){ - if(!wizard)return; - wizard.busy=false; - if(s.returnValue===false || !s.calibrating){wizardFinish(s.errorText||'Calibration session ended. Saved profile unchanged.');return;} - // Ignore duplicates, held output and replies crossing a stimulus boundary. - var current=video.currentTime,nowStep=current<4?-1:Math.floor((current-4)/2); - if(nowStep!==step || s.sampleSequence===wizard.sequence || s.sampleAgeMs<0 || s.sampleAgeMs>350 || !s.colorValid || !Array.isArray(s.regions) || s.regions.length!==16)return; - wizard.sequence=s.sampleSequence; - (step<0?wizard.baseline:wizard.patches[step]).push(calibrationLuma(s.regions)); - document.getElementById('wizardStatus').textContent=step<0?'Measuring black reference...':'Measuring edge patch '+(step+1)+' of 32...'; - }); + +function wizardSample() { + if (!wizard || wizard.busy) return; + var session = wizard; + if (Date.now() - wizard.started > 100000) { + wizardFinish('Calibration timed out. Saved profile unchanged.'); + return; + } + if (wizard.phase !== WizardPhase.MEASURING) return; + var t = video.currentTime; + if (t >= 68) { + try { + var measured = calibrationFit(calibrationAverage(wizard.baseline), wizard.patches.map(calibrationAverage), wizard.model); + profile = 'custom'; + profileDirty = true; + showProfile(); + document.getElementById('customProfile').value = JSON.stringify(measured, null, 2); + document.getElementById('wizardStatus').textContent = 'Measured ' + measured.points.length + ' edge regions. Select Save profile, then Validate profile to check all four colored edges. The previous saved profile is still active.'; + wizard.busy = true; + wizard.phase = WizardPhase.COMMITTING; + call('calibrationDraft', function(r) { + if (wizard === session) wizardFinish(r.returnValue === false ? r.errorText : undefined); + }, { + profile: 'custom', + model: measured.model, + points: measured.points + }); + } catch (e) { + wizardFinish(e.message); + } + return; + } + var step = t < 4 ? -1 : Math.floor((t - 4) / 2), + local = t < 4 ? t : (t - 4) % 2; + if (step < 0 && (t < .8 || t > 3.2) || step >= 0 && (local < .65 || local > 1.75)) return; + wizard.busy = true; + call('status', function(s) { + if (wizard !== session) return; + wizard.busy = false; + if (s.returnValue === false || !s.calibrating) { + wizardFinish(s.errorText || 'Calibration session ended. Saved profile unchanged.'); + return; + } + // Ignore duplicates, held output and replies crossing a stimulus boundary. + var current = video.currentTime, + nowStep = current < 4 ? -1 : Math.floor((current - 4) / 2); + if (nowStep !== step || s.sampleSequence === wizard.sequence || s.sampleAgeMs < 0 || s.sampleAgeMs > 350 || !s.colorValid || !Array.isArray(s.regions) || s.regions.length !== 16) return; + wizard.sequence = s.sampleSequence; + (step < 0 ? wizard.baseline : wizard.patches[step]).push(calibrationLuma(s.regions)); + document.getElementById('wizardStatus').textContent = step < 0 ? 'Measuring black reference...' : 'Measuring edge patch ' + (step + 1) + ' of 32...'; + }); } -function wizardStart(validation){ - if(wizard)return; - if(typeof showPanel==='function')showPanel('calibration',false); - if(targetDirty || profileDirty){document.getElementById('wizardStatus').textContent='Save receiver and profile edits before starting.';return;} - document.getElementById('calibrate').disabled=true;document.getElementById('validateCalibration').disabled=true; - document.getElementById('cancelCalibration').disabled=false; - window.remoteNavigationScope=['cancelCalibration']; - wizard={validation:validation,started:Date.now(),busy:false,sequence:-1,model:'',baseline:[],patches:Array.from({length:32},function(){return [];})}; - document.getElementById('wizardStatus').textContent='Starting full-screen capture. Keep the app visible; do not change zoom or aspect ratio.'; - call('calibrationSession',function(r){ - if(!wizard)return;if(r.returnValue===false){wizardFinish(r.errorText);return;} - // Start the video plane before waiting for histogram data. Without another - // running video, waiting first would prevent calibration from ever starting. - video.src='calibration.mp4';if(typeof video.load==='function')video.load(); - video.style.display='block';var warmup=video.play(); - if(warmup && warmup.catch)warmup.catch(function(){wizardFinish('The TV could not play the calibration video. Saved profile unchanged.');}); - call('start',function(){waitCapture();}); - },{enabled:true}); - function waitCapture(){ - if(!wizard)return; - call('status',function(s){ - if(!wizard)return; - if(s.running && s.activeSource!=='PicCap' && s.tvModel && s.colorValid){ - wizard.model=s.tvModel;video.currentTime=validation?68:0;video.style.display='block'; - document.getElementById('cancelCalibration').focus(); - var playing=video.play();if(playing && playing.catch)playing.catch(function(){wizardFinish('The TV could not play the calibration video. Saved profile unchanged.');}); - wizard.timer=setInterval(wizardSample,250); - }else if(Date.now()-wizard.started<15000)setTimeout(waitCapture,500); - else wizardFinish('No fresh full-screen histogram capture or TV model. Saved profile unchanged.'); - }); - } + +function wizardStart(validation) { + if (wizard) return; + if (typeof showPanel === 'function') showPanel('calibration', false); + if (targetDirty || profileDirty) { + document.getElementById('wizardStatus').textContent = 'Save receiver and profile edits before starting.'; + return; + } + document.getElementById('calibrate').disabled = true; + document.getElementById('validateCalibration').disabled = true; + document.getElementById('cancelCalibration').disabled = false; + window.remoteNavigationScope = ['cancelCalibration']; + wizard = { + phase: WizardPhase.STARTING, + validation: validation, + started: Date.now(), + busy: false, + sequence: -1, + model: '', + baseline: [], + patches: Array.from({ + length: 32 + }, function() { + return []; + }) + }; + var session = wizard; + document.getElementById('wizardStatus').textContent = 'Starting full-screen capture. Keep the app visible; do not change zoom or aspect ratio.'; + call('calibrationSession', function(r) { + if (wizard !== session) return; + if (r.returnValue === false) { + wizardFinish(r.errorText); + return; + } + // Start the video plane before waiting for histogram data. Without another + // running video, waiting first would prevent calibration from ever starting. + video.src = 'calibration.mp4'; + if (typeof video.load === 'function') video.load(); + video.style.display = 'block'; + var warmup = video.play(); + if (warmup && warmup.catch) warmup.catch(function() { + if (wizard !== session) return; + wizardFinish('The TV could not play the calibration video. Saved profile unchanged.'); + }); + wizard.phase = WizardPhase.WAITING; + call('start', function() { + if (wizard === session) waitCapture(); + }); + }, { + enabled: true + }); + + function waitCapture() { + if (wizard !== session) return; + call('status', function(s) { + if (wizard !== session) return; + if (s.running && s.activeSource !== 'PicCap' && s.tvModel && s.colorValid) { + wizard.phase = validation ? WizardPhase.VALIDATING : WizardPhase.MEASURING; + wizard.model = s.tvModel; + video.currentTime = validation ? 68 : 0; + video.style.display = 'block'; + document.getElementById('cancelCalibration').focus(); + var playing = video.play(); + if (playing && playing.catch) playing.catch(function() { + if (wizard !== session) return; + wizardFinish('The TV could not play the calibration video. Saved profile unchanged.'); + }); + wizard.timer = setInterval(wizardSample, 250); + } else if (Date.now() - wizard.started < 15000) setTimeout(waitCapture, 500); + else wizardFinish('No fresh full-screen histogram capture or TV model. Saved profile unchanged.'); + }); + } } -document.getElementById('calibrate').onclick=function(){wizardStart(false);}; -document.getElementById('validateCalibration').onclick=function(){wizardStart(true);}; -document.getElementById('cancelCalibration').onclick=function(){wizardFinish('Calibration cancelled. Saved profile unchanged.');}; -video.addEventListener('ended',function(){if(wizard && wizard.validation)wizardFinish('Validation finished. Top-left red, top-right green, bottom-right blue, bottom-left yellow. Check matching LED positions.');else wizardSample();}); -video.addEventListener('error',function(){wizardFinish('Calibration video playback failed. Saved profile unchanged.');}); -document.addEventListener('keydown',function(e){if(wizard && (e.keyCode===461 || e.key==='Escape')){e.preventDefault();wizardFinish('Calibration cancelled. Saved profile unchanged.');}}); -window.addEventListener('pagehide',function(){wizardFinish();}); +document.getElementById('calibrate').onclick = function() { + wizardStart(false); +}; +document.getElementById('validateCalibration').onclick = function() { + wizardStart(true); +}; +document.getElementById('cancelCalibration').onclick = function() { + wizardFinish('Calibration cancelled. Saved profile unchanged.'); +}; +video.addEventListener('ended', function() { + if (wizard && wizard.validation) wizardFinish('Validation finished. Top-left red, top-right green, bottom-right blue, bottom-left yellow. Check matching LED positions.'); + else wizardSample(); +}); +video.addEventListener('error', function() { + wizardFinish('Calibration video playback failed. Saved profile unchanged.'); +}); +document.addEventListener('keydown', function(e) { + if (wizard && (e.keyCode === 461 || e.key === 'Escape')) { + e.preventDefault(); + wizardFinish('Calibration cancelled. Saved profile unchanged.'); + } +}); +window.addEventListener('pagehide', function() { + wizardFinish(); +}); // An explicit launch action also supports starting the TV's guided test remotely. -function wizardLaunch(raw){try{var params=typeof raw==='string'?JSON.parse(raw):raw;if(params && params.action==='calibrate')wizardStart(false);else if(params && params.action==='repairRoot'){showPanel('overview');repairRoot(function(ok){if(ok)call('status',display);});}}catch(e){document.getElementById('wizardStatus').textContent='Invalid launch parameters.';}} -if(typeof PalmSystem!=='undefined' && PalmSystem.launchParams)setTimeout(function(){wizardLaunch(PalmSystem.launchParams);},1000); -window.addEventListener('webOSRelaunch',function(){if(typeof PalmSystem!=='undefined')wizardLaunch(PalmSystem.launchParams);}); +function wizardLaunch(raw) { + try { + var params = typeof raw === 'string' ? JSON.parse(raw) : raw; + if (params && params.action === 'calibrate') wizardStart(false); + else if (params && params.action === 'repairRoot') { + showPanel('overview'); + repairRoot(function(ok) { + if (ok) call('status', display); + }); + } + } catch (e) { + document.getElementById('wizardStatus').textContent = 'Invalid launch parameters.'; + } +} +if (typeof PalmSystem !== 'undefined' && PalmSystem.launchParams) setTimeout(function() { + wizardLaunch(PalmSystem.launchParams); +}, 1000); +window.addEventListener('webOSRelaunch', function() { + if (typeof PalmSystem !== 'undefined') wizardLaunch(PalmSystem.launchParams); +}); diff --git a/build_manifest.py b/build_manifest.py index 8ec192f..c584a8a 100644 --- a/build_manifest.py +++ b/build_manifest.py @@ -1,6 +1,8 @@ """Describe the exact built IPK for Homebrew Channel.""" import hashlib, json from pathlib import Path +from sync_version import synchronize +synchronize(check=True) ROOT=Path(__file__).resolve().parent BASE='https://gitea.adnau.de/matze19999/HyperDRM' RAW=BASE+'/raw/branch/main/' diff --git a/build_package.py b/build_package.py index 6e9ddab..32c692c 100644 --- a/build_package.py +++ b/build_package.py @@ -1,16 +1,18 @@ """Package own app and executable; no Signify or LG binaries are included.""" -import io, json, tarfile +import gzip, io, json, tarfile from pathlib import Path +from sync_version import synchronize +synchronize(check=True) ROOT=Path(__file__).resolve().parent ID='org.webosbrew.histogramhyperion' VERSION=json.loads((ROOT/'app/appinfo.json').read_text(encoding='utf-8'))['version'] def tar_bytes(entries): stream=io.BytesIO() - with tarfile.open(fileobj=stream,mode='w:gz') as tar: + with tarfile.open(fileobj=stream,mode='w') as tar: for name,body,mode in entries: info=tarfile.TarInfo(name);info.size=len(body);info.mode=mode;info.mtime=0 tar.addfile(info,io.BytesIO(body)) - return stream.getvalue() + return gzip.compress(stream.getvalue(), mtime=0) entries=[] for directory,target in [('app','applications/'+ID),('service','services/'+ID+'.service'),('package','packages/'+ID)]: for path in sorted((ROOT/directory).iterdir()): diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..985fd1c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,46 @@ +# Runtime boundaries + +The native service remains one translation unit because LG symbols are resolved +at runtime. Its implementation headers share the context declared in service.c; +they are included once in dependency order, not independent public libraries. +Formatting must preserve include order (`SortIncludes: Never`). + +| Module | Responsibility | +| --- | --- | +| service.c | Runtime context, LG/Luna loading, registration and shutdown | +| service_api.h | Start/stop, root restart, settings and autostart actions | +| service_calibration.h | Bounded calibration lease, proposals, profiles, model discovery | +| service_status.h | Read-only status serialization | +| capture_loop.h | Scheduler, histogram acquisition, missing-data recovery and output | +| receiver_stream.h | Independent, nonblocking receiver LED subscription | +| source_bridge.h | Bounded color continuity during source transitions | +| handover.h | Exclusive PicCap/histogram capture ownership | + +Connection stages are explicitly named in runtime_states.h: +Disconnected → Connecting → Authenticating (when configured) → Layout → Subscribed. +Any connection, authorization or parsing failure closes the connection and +schedules a bounded retry. Up to eight buffered messages are consumed per tick, +including ticks where no new bytes arrive. Buffer capacity and read budgets remain +bounded. LED count/profile changes trigger a fresh layout subscription. + +The color bridge uses Idle → Holding → WaitingFrame → Fading → Idle. It holds the +last histogram frame while PicCap starts, clears its priority before observing a +fresh PicCap frame, then blends for 500 ms. The transition expires after 3 seconds; +real black bypasses the fade. The opposite direction seeds histogram continuity +from a fresh receiver frame. Capture ownership remains in handover.h; the observer +never acquires LG capture. Stop cancels the bridge and releases owned resources. + +The UI calibration session uses Starting → Waiting → Measuring/Validating → +Committing → finished. Each asynchronous callback checks its original session +identity, so a cancelled session cannot modify a newly started session. Proposals +remain unsaved until the user selects Save profile. The native lease still bounds +the session if the UI disappears. + +UI action errors take precedence over transient status errors. Polling cannot +erase them; starting the next action clears them. Receiver settings, capture state, +calibration state and presentation errors remain distinct. + +VERSION generates app/package metadata and native/version.h. release.py tests +portable logic locally, builds the ARM service and verifies the exact package. +Hardware capture and Magic Remote behavior still require a later TV check; a host +test is not evidence of LG hardware behavior. diff --git a/docs/publishing-review.md b/docs/publishing-review.md index 6551c70..3c98eec 100644 --- a/docs/publishing-review.md +++ b/docs/publishing-review.md @@ -136,3 +136,24 @@ symlink were verified. Actual autostart disable/enable requests succeeded, with Autostart restored to On. No receiver color correction, WLED configuration or PicCap autostart setting was changed. A local browser preview was unavailable because the browser policy rejected file URLs; no alternate browser path was used. + +## Version 0.12.0 + +Receiver stream processing now drains complete buffered messages when no new +bytes arrive. A regression fixture buffers seventeen updates and verifies all +three processing batches without additional socket writes. Action errors survive +status polling and are cleared by the next user action. Tests also cover delayed +calibration replies after cancellation and a new session. + +Connection and bridge states are named enums; UI calibration phases and session +identity checks isolate asynchronous work. The native service is separated into +runtime bootstrap, Luna actions, calibration, status and capture-loop modules. +Production C and JavaScript are formatted consistently; include dependency order +is preserved explicitly. + +VERSION generates app/package/runtime versions. release.py passes all JavaScript, +portable native and Python version-drift tests, cross-compiles the ARM service and +checks the IPK's embedded metadata, executable architecture, size and SHA-256. +Package timestamps are deterministic. No TV installation, application launch or +service restart was performed for this version while the user was watching a film. +Live LG capture and physical remote validation remain pending for this release. diff --git a/docs/release-0.12.0.md b/docs/release-0.12.0.md new file mode 100644 index 0000000..ecafc67 --- /dev/null +++ b/docs/release-0.12.0.md @@ -0,0 +1,11 @@ +# HyperDRM 0.12.0 + +- Process buffered receiver updates even when no additional network data arrives. +- Keep action errors visible across background status polling. +- Name connection, transition and calibration states; ignore stale calibration callbacks. +- Split and format native service modules and application JavaScript. +- Generate versions from VERSION and add a tested, reproducible release workflow. + +Validation: all portable native, JavaScript and Python version tests pass; the +ARM IPK's embedded versions, architecture, size and SHA-256 match its manifest. +This release has not yet been installed or validated on the TV. diff --git a/native/autostart.h b/native/autostart.h index 5edf6e1..314c80e 100644 --- a/native/autostart.h +++ b/native/autostart.h @@ -4,34 +4,66 @@ #define BOOT_DIRECTORY "/var/lib/webosbrew/init.d" #endif #ifndef BOOT_SCRIPT -#define BOOT_SCRIPT "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/boot.sh" +#define BOOT_SCRIPT \ + "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/boot.sh" #endif #define BOOT_HOOK BOOT_DIRECTORY "/60-histogramhyperion" /* Exact pre-0.6.1 hook; only this known own file may be migrated. */ -static const char legacy_boot_script[] = "#!/bin/sh\n# Histogram Hyperion own boot hook v1\n(\n attempt=0\n while [ -x /var/lib/webosbrew/init.d/60-histogramhyperion ] && [ \"$attempt\" -lt 120 ]; do\n result=$(timeout 12 luna-send -n 1 luna://org.webosbrew.histogramhyperion.service/start '{}' 2>&1)\n case \"$result\" in\n *'\"running\":true'*) exit 0 ;;\n esac\n attempt=$((attempt + 1))\n sleep 5\n done\n echo 'Histogram Hyperion boot start cancelled or timed out'\n) >/var/lib/webosbrew/histogramhyperion-boot.log 2>&1 &1)\n case \"$result\" in\n *'\"running\":true'*) exit 0 ;;\n esac\n " + "attempt=$((attempt + 1))\n sleep 5\n done\n echo 'Histogram Hyperion boot start " + "cancelled or timed out'\n) >/var/lib/webosbrew/histogramhyperion-boot.log 2>&1 0 && access(BOOT_HOOK,X_OK)==0;} -static bool set_boot(bool enabled){ - int kind=boot_hook_kind();if(kind<0)return false; - if(!enabled)return kind==0 || unlink(BOOT_HOOK)==0; - if(access(BOOT_SCRIPT,X_OK)!=0)return false; - if(kind==2)return true; - if(mkdir(BOOT_DIRECTORY,0755)!=0 && errno!=EEXIST)return false; - /* Never overwrite another process's temporary file or a foreign hook. */ - if(symlink(BOOT_SCRIPT,BOOT_HOOK ".tmp")!=0)return false; - bool ok=rename(BOOT_HOOK ".tmp",BOOT_HOOK)==0; - if(!ok)unlink(BOOT_HOOK ".tmp"); - return ok; +static bool boot_enabled(void) { + return boot_hook_kind() > 0 && access(BOOT_HOOK, X_OK) == 0; +} +static bool set_boot(bool enabled) { + int kind = boot_hook_kind(); + if (kind < 0) + return false; + if (!enabled) + return kind == 0 || unlink(BOOT_HOOK) == 0; + if (access(BOOT_SCRIPT, X_OK) != 0) + return false; + if (kind == 2) + return true; + if (mkdir(BOOT_DIRECTORY, 0755) != 0 && errno != EEXIST) + return false; + /* Never overwrite another process's temporary file or a foreign hook. */ + if (symlink(BOOT_SCRIPT, BOOT_HOOK ".tmp") != 0) + return false; + bool ok = rename(BOOT_HOOK ".tmp", BOOT_HOOK) == 0; + if (!ok) + unlink(BOOT_HOOK ".tmp"); + return ok; } #endif diff --git a/native/blackbars.h b/native/blackbars.h index 49060e0..dee18c3 100644 --- a/native/blackbars.h +++ b/native/blackbars.h @@ -2,18 +2,25 @@ #define BLACKBARS_H /* Calibrated preset-0 edge probes on this G4. Return -1 for an ambiguous * or dark frame so darkness alone cannot switch the active capture mode. */ -static inline int letterbox_candidate(const unsigned black[16],const unsigned char color[16][3]) { - const unsigned interior[]={2,6,10,15};unsigned lit=0; - for(unsigned i=0;i<4;i++){ - unsigned r=interior[i],peak=color[r][0]; - for(unsigned c=1;c<3;c++)if(color[r][c]>peak)peak=color[r][c]; - if(black[r]<20 && peak>35)lit++; - } - if(lit<2)return -1; - unsigned top=(black[3]+black[11]+black[12])/3; - unsigned bottom=(black[1]+black[5]+black[9])/3; - if(top>=55 && bottom>=25)return 1; - if(top<20 && bottom<20)return 0; - return -1; +static inline int letterbox_candidate(const unsigned black[16], const unsigned char color[16][3]) { + const unsigned interior[] = {2, 6, 10, 15}; + unsigned lit = 0; + for (unsigned i = 0; i < 4; i++) { + unsigned r = interior[i], peak = color[r][0]; + for (unsigned c = 1; c < 3; c++) + if (color[r][c] > peak) + peak = color[r][c]; + if (black[r] < 20 && peak > 35) + lit++; + } + if (lit < 2) + return -1; + unsigned top = (black[3] + black[11] + black[12]) / 3; + unsigned bottom = (black[1] + black[5] + black[9]) / 3; + if (top >= 55 && bottom >= 25) + return 1; + if (top < 20 && bottom < 20) + return 0; + return -1; } #endif diff --git a/native/calibration.h b/native/calibration.h index 27010dd..823c59f 100644 --- a/native/calibration.h +++ b/native/calibration.h @@ -2,83 +2,155 @@ #define HYPERDRM_CALIBRATION_H #include #include "playback.h" -#define CALIBRATION_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/calibration.json" -typedef struct {double position;unsigned char region;} RimPoint; -typedef enum {CAL_AUTO,CAL_G4,CAL_AVERAGE,CAL_CUSTOM} CalibrationKind; -typedef struct {CalibrationKind kind;char model[64];unsigned count;RimPoint points[16];} Calibration; +#define CALIBRATION_PREF \ + "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \ + "calibration.json" +typedef struct { + double position; + unsigned char region; +} RimPoint; +typedef enum { CAL_AUTO, CAL_G4, CAL_AVERAGE, CAL_CUSTOM } CalibrationKind; +typedef struct { + CalibrationKind kind; + char model[64]; + unsigned count; + RimPoint points[16]; +} Calibration; static Calibration calibration; static char tv_model[64]; -static unsigned calibration_generation=1; -static inline unsigned calibration_revision(void){return calibration_generation;} -static const RimPoint g4_rim[]={ - {0.2078431373,7},{0.3176470588,3},{0.5490196078,11},{0.7529411765,12}, - {0.8392156863,13},{1.4941176471,10},{2.1843137255,9},{2.5490196078,1}, - {2.8666666667,5},{3.4,15} -}; -static inline const char *calibration_key(CalibrationKind kind){ - const char *keys[]={"auto","g4","average","custom"};return keys[kind]; +static unsigned calibration_generation = 1; +static inline unsigned calibration_revision(void) { + return calibration_generation; } -static inline bool calibration_model_valid(const char *model){ - if(!model[0] || strlen(model)>63)return false; - for(const unsigned char *p=(const unsigned char*)model;*p;p++) - if(!((*p>='A'&&*p<='Z')||(*p>='a'&&*p<='z')||(*p>='0'&&*p<='9')||*p=='.'||*p=='-'||*p=='_'||*p==' '))return false; - return true; +static const RimPoint g4_rim[] = {{0.2078431373, 7}, {0.3176470588, 3}, {0.5490196078, 11}, + {0.7529411765, 12}, {0.8392156863, 13}, {1.4941176471, 10}, + {2.1843137255, 9}, {2.5490196078, 1}, {2.8666666667, 5}, + {3.4, 15}}; +static inline const char *calibration_key(CalibrationKind kind) { + const char *keys[] = {"auto", "g4", "average", "custom"}; + return keys[kind]; +} +static inline bool calibration_model_valid(const char *model) { + if (!model[0] || strlen(model) > 63) + return false; + for (const unsigned char *p = (const unsigned char *)model; *p; p++) + if (!((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') || (*p >= '0' && *p <= '9') || + *p == '.' || *p == '-' || *p == '_' || *p == ' ')) + return false; + return true; } /* LG retail model identifiers: OLED + two-digit panel size + G4 series. */ -static inline bool calibration_is_g4(const char *model){ - return strlen(model)>=8 && !strncmp(model,"OLED",4) && model[4]>='0' && model[4]<='9' && model[5]>='0' && model[5]<='9' && model[6]=='G' && model[7]=='4'; +static inline bool calibration_is_g4(const char *model) { + return strlen(model) >= 8 && !strncmp(model, "OLED", 4) && model[4] >= '0' && model[4] <= '9' && + model[5] >= '0' && model[5] <= '9' && model[6] == 'G' && model[7] == '4'; } -static inline CalibrationKind calibration_effective(void){ - if(calibration.kind!=CAL_AUTO)return calibration.kind; - if(calibration.count>=2 && tv_model[0] && !strcmp(calibration.model,tv_model))return CAL_CUSTOM; - return calibration_is_g4(tv_model)?CAL_G4:CAL_AVERAGE; +static inline CalibrationKind calibration_effective(void) { + if (calibration.kind != CAL_AUTO) + return calibration.kind; + if (calibration.count >= 2 && tv_model[0] && !strcmp(calibration.model, tv_model)) + return CAL_CUSTOM; + return calibration_is_g4(tv_model) ? CAL_G4 : CAL_AVERAGE; } -static inline bool calibration_parse(PlaybackJson *j,const char *text,Calibration *result){ - void *root=j->parse(text);if(!root)return false; - Calibration next=*result;bool valid=false; - for(unsigned k=0;k<4;k++)if(playback_is(j,root,"profile",calibration_key(k))){next.kind=k;valid=true;break;} - void *points=NULL; - if(j->type(root)==4 && j->get(root,"points",&points)){ - void *model=playback_field(j,root,"model"); - valid=valid && points && j->type(points)==5 && model && j->type(model)==6 && calibration_model_valid(j->string(model)); - size_t count=valid?j->length(points):0;valid=valid && count>=2 && count<=16; - if(valid){strcpy(next.model,j->string(model));next.count=count;memset(next.points,0,sizeof(next.points));} - for(size_t i=0;valid && iat(points,i),*position=playback_field(j,point,"position"),*region=playback_field(j,point,"region"); - valid=position && (j->type(position)==2 || j->type(position)==3) && region && j->type(region)==3; - if(valid){char *end;double x=strtod(j->string(position),&end);valid=end && !*end && isfinite(x) && x>=0 && x<4; - long r=strtol(j->string(region),&end,10);valid=valid && end && !*end && r>=0 && r<16 && (!i || x>next.points[i-1].position); - if(valid){next.points[i].position=x;next.points[i].region=r;} - } - } - } - if(next.kind==CAL_CUSTOM && next.count<2)valid=false; - j->put(root);if(valid)*result=next;return valid; +static inline bool calibration_parse(PlaybackJson *j, const char *text, Calibration *result) { + void *root = j->parse(text); + if (!root) + return false; + Calibration next = *result; + bool valid = false; + for (unsigned k = 0; k < 4; k++) + if (playback_is(j, root, "profile", calibration_key(k))) { + next.kind = k; + valid = true; + break; + } + void *points = NULL; + if (j->type(root) == 4 && j->get(root, "points", &points)) { + void *model = playback_field(j, root, "model"); + valid = valid && points && j->type(points) == 5 && model && j->type(model) == 6 && + calibration_model_valid(j->string(model)); + size_t count = valid ? j->length(points) : 0; + valid = valid && count >= 2 && count <= 16; + if (valid) { + strcpy(next.model, j->string(model)); + next.count = count; + memset(next.points, 0, sizeof(next.points)); + } + for (size_t i = 0; valid && i < count; i++) { + void *point = j->at(points, i), *position = playback_field(j, point, "position"), + *region = playback_field(j, point, "region"); + valid = position && (j->type(position) == 2 || j->type(position) == 3) && region && + j->type(region) == 3; + if (valid) { + char *end; + double x = strtod(j->string(position), &end); + valid = end && !*end && isfinite(x) && x >= 0 && x < 4; + long r = strtol(j->string(region), &end, 10); + valid = valid && end && !*end && r >= 0 && r < 16 && + (!i || x > next.points[i - 1].position); + if (valid) { + next.points[i].position = x; + next.points[i].region = r; + } + } + } + } + if (next.kind == CAL_CUSTOM && next.count < 2) + valid = false; + j->put(root); + if (valid) + *result = next; + return valid; } -static inline bool calibration_save(const char *path,const Calibration *value){ - char temp[512],data[2048];int used=snprintf(data,sizeof(data),"{\"profile\":\"%s\"",calibration_key(value->kind)); - if(value->count){ - used+=snprintf(data+used,sizeof(data)-used,",\"model\":\"%s\",\"points\":[",value->model); - for(unsigned i=0;icount;i++)used+=snprintf(data+used,sizeof(data)-used,"%s{\"position\":%.12g,\"region\":%u}",i?",":"",value->points[i].position,value->points[i].region); - used+=snprintf(data+used,sizeof(data)-used,"]"); - } - used+=snprintf(data+used,sizeof(data)-used,"}\n"); - if(snprintf(temp,sizeof(temp),"%s.tmp",path)>=(int)sizeof(temp))return false; - int fd=open(temp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false; - bool ok=write(fd,data,used)==used && fsync(fd)==0;if(close(fd)!=0)ok=false; - if(ok)ok=rename(temp,path)==0; - if(!ok)unlink(temp); - return ok; +static inline bool calibration_save(const char *path, const Calibration *value) { + char temp[512], data[2048]; + int used = snprintf(data, sizeof(data), "{\"profile\":\"%s\"", calibration_key(value->kind)); + if (value->count) { + used += snprintf(data + used, sizeof(data) - used, ",\"model\":\"%s\",\"points\":[", + value->model); + for (unsigned i = 0; i < value->count; i++) + used += + snprintf(data + used, sizeof(data) - used, "%s{\"position\":%.12g,\"region\":%u}", + i ? "," : "", value->points[i].position, value->points[i].region); + used += snprintf(data + used, sizeof(data) - used, "]"); + } + used += snprintf(data + used, sizeof(data) - used, "}\n"); + if (snprintf(temp, sizeof(temp), "%s.tmp", path) >= (int)sizeof(temp)) + return false; + int fd = open(temp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); + if (fd < 0) + return false; + bool ok = write(fd, data, used) == used && fsync(fd) == 0; + if (close(fd) != 0) + ok = false; + if (ok) + ok = rename(temp, path) == 0; + if (!ok) + unlink(temp); + return ok; } -static inline void calibration_load(PlaybackJson *j,const char *path){ - int fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return; - char data[2048];ssize_t n=read(fd,data,sizeof(data)-1);close(fd); - if(n<=0 || n==(ssize_t)sizeof(data)-1)return; - data[n]=0;calibration_parse(j,data,&calibration); +static inline void calibration_load(PlaybackJson *j, const char *path) { + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) + return; + char data[2048]; + ssize_t n = read(fd, data, sizeof(data) - 1); + close(fd); + if (n <= 0 || n == (ssize_t)sizeof(data) - 1) + return; + data[n] = 0; + calibration_parse(j, data, &calibration); } -static inline void calibration_colors(const unsigned char input[16][3],unsigned char output[16][3]){ - memcpy(output,input,48);if(calibration_effective()!=CAL_AVERAGE)return; - unsigned sum[3]={0};for(unsigned i=0;i<16;i++)for(unsigned c=0;c<3;c++)sum[c]+=input[i][c]; - for(unsigned i=0;i<16;i++)for(unsigned c=0;c<3;c++)output[i][c]=sum[c]/16; +static inline void calibration_colors(const unsigned char input[16][3], + unsigned char output[16][3]) { + memcpy(output, input, 48); + if (calibration_effective() != CAL_AVERAGE) + return; + unsigned sum[3] = {0}; + for (unsigned i = 0; i < 16; i++) + for (unsigned c = 0; c < 3; c++) + sum[c] += input[i][c]; + for (unsigned i = 0; i < 16; i++) + for (unsigned c = 0; c < 3; c++) + output[i][c] = sum[c] / 16; } #endif diff --git a/native/capture_loop.h b/native/capture_loop.h new file mode 100644 index 0000000..c8e65dc --- /dev/null +++ b/native/capture_loop.h @@ -0,0 +1,150 @@ +/* Capture scheduler and histogram processing. */ +static int tick(void *unused) { + (void)unused; + uint64_t now = milliseconds(); + model_tick(now); + test_tick(&playback_json, now); + layout_refresh_tick(&playback_json, now, capture || capture_source == SOURCE_PICCAP); + stream_tick(&playback_json, now, handover_enabled && !retries.paused && !retries.blocked); + bool owned = handover_tick(); + bridge_tick(now); + if (owned) + return 1; + if (!capture) { + retry_update(&retries, boot_enabled() && geteuid() == 0, false, milliseconds()); + if (retry_take(&retries, milliseconds())) { + start(NULL, NULL, NULL); + if (!capture) + retry_failure(&retries, milliseconds()); + } + return 1; + } + if (revoked) { + set_error("LG revoked capture; recovering after source transition"); + capture_recoveries++; + recovery_started = milliseconds(); + release_capture(false); + retry_source_transition(&retries, milliseconds()); + return 1; + } + if (settling) { + settling--; + return 1; + } + if (!CALIBRATION_ACTIVE && letterbox_mode && capture_preset == 1 && + milliseconds() >= next_border_probe) { + if (vt_preset(capture, 0) != 0) { + set_error("Border probe preset failed"); + stop_capture(); + return 1; + } + capture_preset = 0; + settling = 2; + return 1; + } + Histogram hist[16] = {0}; + int rc = vt_get(capture, hist); + bool populated = !rc && histogram_populated(hist); + if (!populated) { + unsigned char held[16][3]; + color_valid = false; + if (!continuity_frame(&continuity, false, rgb, now, held)) { + set_error("Histogram data unavailable beyond hold window; recovering capture"); + release_capture(false); + retry_source_transition(&retries, now); + return 1; + } + char *line = region_command(held, led_blends, led_count, + (unsigned)(1500 - (now - continuity.missing_at))); + if (!line || send_line(line)) { + free(line); + set_error("Receiver connection lost while holding colors"); + stop_capture(); + return 1; + } + free(line); + if (!receiver_ack()) { + set_error("Receiver did not acknowledge held colors"); + stop_capture(); + return 1; + } + last_output_at = now; + return 1; + } + sample_at = now; + sample_sequence++; + unsigned aggregate[3] = {0}; + for (int i = 0; i < 16; i++) { + black_percent[i] = 100; + for (int c = 0; c < 3; c++) { + uint64_t total = 0, black = 0; + for (int b = 0; b < 256; b++) { + total += hist[i].bins[c][b]; + if (b <= 12) + black += hist[i].bins[c][b]; + } + unsigned fraction = total ? (unsigned)(100 * black / total) : 0; + if (fraction < black_percent[i]) + black_percent[i] = fraction; + } + } + for (int i = 0; i < 16; i++) + for (int c = 0; c < 3; c++) { + rgb[i][c] = histogram_mean(hist[i].bins[c]); + aggregate[c] += rgb[i][c]; + } + if (!CALIBRATION_ACTIVE && capture_preset == 0) { + int candidate = letterbox_candidate(black_percent, rgb); + uint64_t now = milliseconds(); + if (candidate < 0) + pending_letterbox = -1; + else if (candidate != pending_letterbox) { + pending_letterbox = candidate; + candidate_since = now; + } else if ((unsigned)candidate != letterbox_mode && now - candidate_since >= 2000) + letterbox_mode = (unsigned)candidate; + if (letterbox_mode) { + if (vt_preset(capture, 1) != 0) { + set_error("Letterbox preset failed"); + stop_capture(); + return 1; + } + capture_preset = 1; + settling = 2; + next_border_probe = now + 2000; + return 1; + } + } + for (int c = 0; c < 3; c++) + sent_rgb[c] = (unsigned char)(aggregate[c] / 16); + unsigned char boosted_mean[3]; + boost_color(sent_rgb, boosted_mean); + memcpy(sent_rgb, boosted_mean, 3); + unsigned char mapped_rgb[16][3], stable_rgb[16][3]; + calibration_colors(rgb, mapped_rgb); + continuity_frame(&continuity, true, mapped_rgb, now, stable_rgb); + interpolation_enabled = led_count && calibration_effective() != CAL_AVERAGE; + char *line = region_command(stable_rgb, led_blends, led_count, 1500); + if (!line) { + set_error("Color buffer allocation failed"); + stop_capture(); + return 1; + } + int failed = send_line(line); + free(line); + if (failed) { + set_error("Receiver connection lost"); + stop_capture(); + return 1; + } + if (!receiver_ack()) { + set_error("Receiver did not acknowledge color data"); + stop_capture(); + return 1; + } + last_error[0] = 0; + color_valid = true; + last_output_at = milliseconds(); + frames++; + return 1; +} diff --git a/native/color_boost.h b/native/color_boost.h index 940f0c6..7d7a999 100644 --- a/native/color_boost.h +++ b/native/color_boost.h @@ -1,7 +1,8 @@ #ifndef COLOR_BOOST_H #define COLOR_BOOST_H /* Preserve the original saturation and brightness. */ -static inline void boost_color(const unsigned char in[3],unsigned char out[3]){ - for(unsigned c=0;c<3;c++)out[c]=in[c]; +static inline void boost_color(const unsigned char in[3], unsigned char out[3]) { + for (unsigned c = 0; c < 3; c++) + out[c] = in[c]; } #endif diff --git a/native/continuity.h b/native/continuity.h index 9594e05..39c731c 100644 --- a/native/continuity.h +++ b/native/continuity.h @@ -1,35 +1,76 @@ #ifndef HYPERDRM_CONTINUITY_H #define HYPERDRM_CONTINUITY_H /* Zero population is missing data, while populated bin zero is real black. */ -static inline bool histogram_populated(const Histogram hist[16]){ - for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++){ - uint64_t total=0;for(unsigned b=0;b<256;b++)total+=hist[r].bins[c][b]; - if(!total)return false; - } - return true; +static inline bool histogram_populated(const Histogram hist[16]) { + for (unsigned r = 0; r < 16; r++) + for (unsigned c = 0; c < 3; c++) { + uint64_t total = 0; + for (unsigned b = 0; b < 256; b++) + total += hist[r].bins[c][b]; + if (!total) + return false; + } + return true; } -typedef struct {unsigned char last[16][3],from[16][3];uint64_t valid_at,fade_at,missing_at;bool available,fading,holding;} Continuity; -static inline void continuity_seed(Continuity *s,const unsigned char colors[16][3],uint64_t now){ - memcpy(s->last,colors,sizeof(s->last));memcpy(s->from,colors,sizeof(s->from));s->valid_at=now;s->available=true;s->fading=true;s->fade_at=0;s->missing_at=0;s->holding=false; +typedef struct { + unsigned char last[16][3], from[16][3]; + uint64_t valid_at, fade_at, missing_at; + bool available, fading, holding; +} Continuity; +static inline void continuity_seed(Continuity *s, const unsigned char colors[16][3], uint64_t now) { + memcpy(s->last, colors, sizeof(s->last)); + memcpy(s->from, colors, sizeof(s->from)); + s->valid_at = now; + s->available = true; + s->fading = true; + s->fade_at = 0; + s->missing_at = 0; + s->holding = false; } -static inline void continuity_mix(const unsigned char from[16][3],const unsigned char to[16][3],unsigned elapsed,unsigned char out[16][3]){ - if(elapsed>500)elapsed=500; - for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)out[r][c]=(from[r][c]*(500-elapsed)+to[r][c]*elapsed+250)/500; +static inline void continuity_mix(const unsigned char from[16][3], const unsigned char to[16][3], + unsigned elapsed, unsigned char out[16][3]) { + if (elapsed > 500) + elapsed = 500; + for (unsigned r = 0; r < 16; r++) + for (unsigned c = 0; c < 3; c++) + out[r][c] = (from[r][c] * (500 - elapsed) + to[r][c] * elapsed + 250) / 500; } -static inline bool continuity_frame(Continuity *s,bool valid,const unsigned char input[16][3],uint64_t now,unsigned char out[16][3]){ - if(!valid){ - if(!s->missing_at)s->missing_at=now; - s->holding=s->available && now-s->missing_at<1500; - if(s->holding)memcpy(out,s->last,sizeof(s->last)); - return s->holding; - } - if(s->holding){memcpy(s->from,s->last,sizeof(s->from));s->fading=true;s->fade_at=0;} - s->holding=false;s->missing_at=0; - /* Near-black codec rounding is valid darkness, not a lost measurement. */ - bool black=true;for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)if(input[r][c]>3)black=false; - if(black)s->fading=false; - if(s->fading){if(!s->fade_at)s->fade_at=now;continuity_mix(s->from,input,(unsigned)(now-s->fade_at),out);if(now-s->fade_at>=500)s->fading=false;} - else memcpy(out,input,sizeof(s->last)); - memcpy(s->last,out,sizeof(s->last));s->available=true;s->valid_at=now;return true; +static inline bool continuity_frame(Continuity *s, bool valid, const unsigned char input[16][3], + uint64_t now, unsigned char out[16][3]) { + if (!valid) { + if (!s->missing_at) + s->missing_at = now; + s->holding = s->available && now - s->missing_at < 1500; + if (s->holding) + memcpy(out, s->last, sizeof(s->last)); + return s->holding; + } + if (s->holding) { + memcpy(s->from, s->last, sizeof(s->from)); + s->fading = true; + s->fade_at = 0; + } + s->holding = false; + s->missing_at = 0; + /* Near-black codec rounding is valid darkness, not a lost measurement. */ + bool black = true; + for (unsigned r = 0; r < 16; r++) + for (unsigned c = 0; c < 3; c++) + if (input[r][c] > 3) + black = false; + if (black) + s->fading = false; + if (s->fading) { + if (!s->fade_at) + s->fade_at = now; + continuity_mix(s->from, input, (unsigned)(now - s->fade_at), out); + if (now - s->fade_at >= 500) + s->fading = false; + } else + memcpy(out, input, sizeof(s->last)); + memcpy(s->last, out, sizeof(s->last)); + s->available = true; + s->valid_at = now; + return true; } #endif diff --git a/native/diagnostics.h b/native/diagnostics.h index 061fb2f..fcd5f47 100644 --- a/native/diagnostics.h +++ b/native/diagnostics.h @@ -2,77 +2,184 @@ #define HYPERDRM_DIAGNOSTICS_H /* An independent nonblocking connection. Tests never send colors or acquire * capture. Work per timer iteration is bounded to keep output responsive. */ -typedef enum {TEST_IDLE,TEST_CONNECT,TEST_SEND,TEST_LOGIN,TEST_LAYOUT,TEST_DONE,TEST_FAILED} TestStage; +typedef enum { + TEST_IDLE, + TEST_CONNECT, + TEST_SEND, + TEST_LOGIN, + TEST_LAYOUT, + TEST_DONE, + TEST_FAILED +} TestStage; typedef struct { - int fd;TestStage stage,after_send;TargetConfig target; - uint64_t started,deadline,elapsed;unsigned leds; - char request[384],error[128];size_t sent,used;char *response; - LedBlend *mapping; + int fd; + TestStage stage, after_send; + TargetConfig target; + uint64_t started, deadline, elapsed; + unsigned leds; + char request[384], error[128]; + size_t sent, used; + char *response; + LedBlend *mapping; } ReceiverProbe; -static ReceiverProbe connection_test={.fd=-1}; -static const char *probe_stage_name(const ReceiverProbe *probe){ - static const char *names[]={"Not tested","Connecting","Sending request","Checking authentication","Reading LED layout","Passed","Failed"}; - return names[probe->stage]; +static ReceiverProbe connection_test = {.fd = -1}; +static const char *probe_stage_name(const ReceiverProbe *probe) { + static const char *names[] = { + "Not tested", "Connecting", "Sending request", "Checking authentication", + "Reading LED layout", "Passed", "Failed"}; + return names[probe->stage]; } -static bool probe_busy(const ReceiverProbe *probe){return probe->stage>=TEST_CONNECT && probe->stage<=TEST_LAYOUT;} -static void probe_close(ReceiverProbe *probe){if(probe->fd>=0)close(probe->fd);probe->fd=-1;free(probe->mapping);probe->mapping=NULL;free(probe->response);probe->response=NULL;memset(probe->request,0,sizeof(probe->request));memset(probe->target.token,0,sizeof(probe->target.token));} -static void probe_finish(ReceiverProbe *probe,const char *error,uint64_t now){ - probe_close(probe);probe->elapsed=now-probe->started; - probe->stage=error?TEST_FAILED:TEST_DONE; - snprintf(probe->error,sizeof(probe->error),"%s",error?error:""); +static bool probe_busy(const ReceiverProbe *probe) { + return probe->stage >= TEST_CONNECT && probe->stage <= TEST_LAYOUT; } -static void probe_request(ReceiverProbe *probe,bool login,uint64_t now){ - if(login)snprintf(probe->request,sizeof(probe->request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",probe->target.token); - else snprintf(probe->request,sizeof(probe->request),"{\"command\":\"serverinfo\"}\n"); - probe->sent=probe->used=0;probe->after_send=login?TEST_LOGIN:TEST_LAYOUT; - probe->stage=TEST_SEND;probe->deadline=now+3000; +static void probe_close(ReceiverProbe *probe) { + if (probe->fd >= 0) + close(probe->fd); + probe->fd = -1; + free(probe->mapping); + probe->mapping = NULL; + free(probe->response); + probe->response = NULL; + memset(probe->request, 0, sizeof(probe->request)); + memset(probe->target.token, 0, sizeof(probe->target.token)); } -static bool probe_begin(ReceiverProbe *probe,const TargetConfig *target,uint64_t now){ - if(probe_busy(probe))return false; - probe_close(probe);memset(probe,0,sizeof(*probe));probe->fd=-1; - probe->target=*target;probe->started=now; - probe->response=malloc(2*1024*1024+1); - if(!probe->response){probe_finish(probe,"Cannot allocate connection test buffer",now);return true;} - probe->fd=target_open(target); - if(probe->fd<0){probe_finish(probe,"Cannot open receiver connection",now);return true;} - probe->stage=TEST_CONNECT;probe->deadline=now+3000;return true; +static void probe_finish(ReceiverProbe *probe, const char *error, uint64_t now) { + probe_close(probe); + probe->elapsed = now - probe->started; + probe->stage = error ? TEST_FAILED : TEST_DONE; + snprintf(probe->error, sizeof(probe->error), "%s", error ? error : ""); } -static void probe_tick(ReceiverProbe *probe,PlaybackJson *json,uint64_t now){ - if(!probe_busy(probe))return; - if(now>=probe->deadline){probe_finish(probe,"Receiver timed out",now);return;} - int fd=probe->fd; - if(probe->stage==TEST_CONNECT){ - struct pollfd p={fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error); - if(poll(&p,1,0)<=0)return; - if(getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){probe_finish(probe,"Receiver connection refused or unreachable",now);return;} - probe_request(probe,probe->target.token[0]!=0,now);return; - } - if(probe->stage==TEST_SEND){ - size_t length=strlen(probe->request); - ssize_t n=send(fd,probe->request+probe->sent,length-probe->sent,MSG_NOSIGNAL); - if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return; - if(n<=0){probe_finish(probe,"Receiver closed the test connection",now);return;} - probe->sent+=(size_t)n; - if(probe->sent==length){probe->stage=probe->after_send;probe->deadline=now+3000;memset(probe->request,0,sizeof(probe->request));} - return; - } - size_t available=2*1024*1024-probe->used;if(available>65536)available=65536; - if(!available){probe_finish(probe,"Receiver response exceeds 2 MiB",now);return;} - ssize_t n=recv(fd,probe->response+probe->used,available,0); - if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return; - if(n<=0){probe_finish(probe,"Receiver closed the test connection",now);return;} - probe->used+=(size_t)n;probe->response[probe->used]=0; - char *end=strchr(probe->response,'\n');if(!end)return;*end=0; - void *root=json->parse(probe->response);bool success=playback_true(json,root,"success");if(root)json->put(root); - if(!success){probe_finish(probe,probe->stage==TEST_LOGIN?"Receiver authentication failed":"Receiver rejected serverinfo; check authorization",now);return;} - if(probe->stage==TEST_LOGIN){probe_request(probe,false,now);return;} - LedBlend *mapping=NULL;unsigned count=0; - if(!layout_from_json(probe->response,&mapping,&count)){probe_finish(probe,"Receiver returned an invalid or empty LED layout",now);return;} - probe->leds=count;probe_finish(probe,NULL,now);probe->mapping=mapping; +static void probe_request(ReceiverProbe *probe, bool login, uint64_t now) { + if (login) + snprintf(probe->request, sizeof(probe->request), + "{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n", + probe->target.token); + else + snprintf(probe->request, sizeof(probe->request), "{\"command\":\"serverinfo\"}\n"); + probe->sent = probe->used = 0; + probe->after_send = login ? TEST_LOGIN : TEST_LAYOUT; + probe->stage = TEST_SEND; + probe->deadline = now + 3000; +} +static bool probe_begin(ReceiverProbe *probe, const TargetConfig *target, uint64_t now) { + if (probe_busy(probe)) + return false; + probe_close(probe); + memset(probe, 0, sizeof(*probe)); + probe->fd = -1; + probe->target = *target; + probe->started = now; + probe->response = malloc(2 * 1024 * 1024 + 1); + if (!probe->response) { + probe_finish(probe, "Cannot allocate connection test buffer", now); + return true; + } + probe->fd = target_open(target); + if (probe->fd < 0) { + probe_finish(probe, "Cannot open receiver connection", now); + return true; + } + probe->stage = TEST_CONNECT; + probe->deadline = now + 3000; + return true; +} +static void probe_tick(ReceiverProbe *probe, PlaybackJson *json, uint64_t now) { + if (!probe_busy(probe)) + return; + if (now >= probe->deadline) { + probe_finish(probe, "Receiver timed out", now); + return; + } + int fd = probe->fd; + if (probe->stage == TEST_CONNECT) { + struct pollfd p = {fd, POLLOUT, 0}; + int error = 0; + socklen_t size = sizeof(error); + if (poll(&p, 1, 0) <= 0) + return; + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 || error) { + probe_finish(probe, "Receiver connection refused or unreachable", now); + return; + } + probe_request(probe, probe->target.token[0] != 0, now); + return; + } + if (probe->stage == TEST_SEND) { + size_t length = strlen(probe->request); + ssize_t n = send(fd, probe->request + probe->sent, length - probe->sent, MSG_NOSIGNAL); + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) + return; + if (n <= 0) { + probe_finish(probe, "Receiver closed the test connection", now); + return; + } + probe->sent += (size_t)n; + if (probe->sent == length) { + probe->stage = probe->after_send; + probe->deadline = now + 3000; + memset(probe->request, 0, sizeof(probe->request)); + } + return; + } + size_t available = 2 * 1024 * 1024 - probe->used; + if (available > 65536) + available = 65536; + if (!available) { + probe_finish(probe, "Receiver response exceeds 2 MiB", now); + return; + } + ssize_t n = recv(fd, probe->response + probe->used, available, 0); + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) + return; + if (n <= 0) { + probe_finish(probe, "Receiver closed the test connection", now); + return; + } + probe->used += (size_t)n; + probe->response[probe->used] = 0; + char *end = strchr(probe->response, '\n'); + if (!end) + return; + *end = 0; + void *root = json->parse(probe->response); + bool success = playback_true(json, root, "success"); + if (root) + json->put(root); + if (!success) { + probe_finish(probe, + probe->stage == TEST_LOGIN + ? "Receiver authentication failed" + : "Receiver rejected serverinfo; check authorization", + now); + return; + } + if (probe->stage == TEST_LOGIN) { + probe_request(probe, false, now); + return; + } + LedBlend *mapping = NULL; + unsigned count = 0; + if (!layout_from_json(probe->response, &mapping, &count)) { + probe_finish(probe, "Receiver returned an invalid or empty LED layout", now); + return; + } + probe->leds = count; + probe_finish(probe, NULL, now); + probe->mapping = mapping; +} +static inline const char *test_stage_name(void) { + return probe_stage_name(&connection_test); +} +static inline bool test_busy(void) { + return probe_busy(&connection_test); +} +static inline void test_close(void) { + probe_close(&connection_test); +} +static inline bool test_begin(const TargetConfig *target, uint64_t now) { + return probe_begin(&connection_test, target, now); +} +static inline void test_tick(PlaybackJson *json, uint64_t now) { + probe_tick(&connection_test, json, now); } -static inline const char *test_stage_name(void){return probe_stage_name(&connection_test);} -static inline bool test_busy(void){return probe_busy(&connection_test);} -static inline void test_close(void){probe_close(&connection_test);} -static inline bool test_begin(const TargetConfig *target,uint64_t now){return probe_begin(&connection_test,target,now);} -static inline void test_tick(PlaybackJson *json,uint64_t now){probe_tick(&connection_test,json,now);} #endif diff --git a/native/handover.h b/native/handover.h index 17abc0d..99a06fb 100644 --- a/native/handover.h +++ b/native/handover.h @@ -5,22 +5,32 @@ #define CALIBRATION_ACTIVE false #endif #ifndef PICCAP_BINARY -#define PICCAP_BINARY "/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/hyperion-webos" +#define PICCAP_BINARY \ + "/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/hyperion-webos" #endif -#define HANDOVER_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/handover.json" +#define HANDOVER_PREF \ + "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/" \ + "handover.json" static PlaybackJson playback_json; -static bool handover_enabled,piccap_owned,piccap_busy,media_busy,piccap_connected; +static bool handover_enabled, piccap_owned, piccap_busy, media_busy, piccap_connected; static PlaybackProtection protection; -static uint64_t protection_at,next_media_probe,next_piccap_probe,handover_cooldown,piccap_action_at; -typedef enum {SOURCE_HISTOGRAM,SOURCE_PICCAP_STARTING,SOURCE_PICCAP,SOURCE_PICCAP_STOPPING} CaptureSource; +static uint64_t protection_at, next_media_probe, next_piccap_probe, handover_cooldown, + piccap_action_at; +typedef enum { + SOURCE_HISTOGRAM, + SOURCE_PICCAP_STARTING, + SOURCE_PICCAP, + SOURCE_PICCAP_STOPPING +} CaptureSource; static CaptureSource capture_source; static void *service_handle; -static bool (*call_once)(void*,const char*,const char*,Callback,void*,unsigned long*,LSError*); -static bool (*cancel_call)(void*,unsigned long,LSError*); -static unsigned long piccap_token,media_token; -static uint64_t piccap_deadline,media_deadline; -static unsigned clear_samples,piccap_fallbacks; -static uint64_t clear_since,unknown_since,piccap_disconnected_since; +static bool (*call_once)(void *, const char *, const char *, Callback, void *, unsigned long *, + LSError *); +static bool (*cancel_call)(void *, unsigned long, LSError *); +static unsigned long piccap_token, media_token; +static uint64_t piccap_deadline, media_deadline; +static unsigned clear_samples, piccap_fallbacks; +static uint64_t clear_since, unknown_since, piccap_disconnected_since; #define CLEAR_STABLE_MS 2000 #define UNKNOWN_GRACE_MS 1500 #define PICCAP_LOSS_MS 5000 @@ -28,105 +38,245 @@ static uint64_t clear_since,unknown_since,piccap_disconnected_since; static TargetConfig piccap_target; static char piccap_error[128]; static bool piccap_request(int command); -static inline bool piccap_installed(void){return access(PICCAP_BINARY,X_OK)==0;} -static bool handover_save(bool enabled){ - const char *data=enabled?"{\"piccapHandover\":true}\n":"{\"piccapHandover\":false}\n"; - int fd=open(HANDOVER_PREF ".tmp",O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false; - ssize_t n=write(fd,data,strlen(data));bool ok=n==(ssize_t)strlen(data) && fsync(fd)==0; - if(close(fd)!=0)ok=false; - if(ok)ok=rename(HANDOVER_PREF ".tmp",HANDOVER_PREF)==0; - if(!ok)unlink(HANDOVER_PREF ".tmp"); - return ok; +static inline bool piccap_installed(void) { + return access(PICCAP_BINARY, X_OK) == 0; } -static void handover_load(void){ - int fd=open(HANDOVER_PREF,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return; - char text[128];ssize_t n=read(fd,text,sizeof(text)-1);close(fd); - if(n>0){text[n]=0;void *root=playback_json.parse(text);handover_enabled=playback_true(&playback_json,root,"piccapHandover");if(root)playback_json.put(root);} +static bool handover_save(bool enabled) { + const char *data = enabled ? "{\"piccapHandover\":true}\n" : "{\"piccapHandover\":false}\n"; + int fd = open(HANDOVER_PREF ".tmp", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); + if (fd < 0) + return false; + ssize_t n = write(fd, data, strlen(data)); + bool ok = n == (ssize_t)strlen(data) && fsync(fd) == 0; + if (close(fd) != 0) + ok = false; + if (ok) + ok = rename(HANDOVER_PREF ".tmp", HANDOVER_PREF) == 0; + if (!ok) + unlink(HANDOVER_PREF ".tmp"); + return ok; } -static bool media_reply(void *h,void *m,void *ctx){ - (void)h;(void)ctx;media_busy=false;media_token=0; - protection=playback_classify(&playback_json,payload(m));protection_at=milliseconds(); - if(protection==PLAYBACK_CLEAR){ - if(!clear_samples)clear_since=protection_at; - if(clear_samples<3)clear_samples++; - unknown_since=0; - }else {clear_samples=0;clear_since=0;if(protection==PLAYBACK_UNKNOWN && !unknown_since)unknown_since=protection_at;else if(protection==PLAYBACK_PROTECTED)unknown_since=0;} - return true; +static void handover_load(void) { + int fd = open(HANDOVER_PREF, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) + return; + char text[128]; + ssize_t n = read(fd, text, sizeof(text) - 1); + close(fd); + if (n > 0) { + text[n] = 0; + void *root = playback_json.parse(text); + handover_enabled = playback_true(&playback_json, root, "piccapHandover"); + if (root) + playback_json.put(root); + } } -/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. Confirm stopped status before VT reuse. */ -static bool piccap_reply(void *h,void *m,void *ctx){ - (void)h;int command=(int)(intptr_t)ctx;piccap_busy=false;piccap_token=0; - void *root=playback_json.parse(payload(m));bool ok=playback_true(&playback_json,root,"returnValue"); - if(command==0){ - void *running=playback_field(&playback_json,root,"isRunning"); - if(ok && running && playback_json.type(running)==1){ - bool active=playback_json.boolean(running); - piccap_connected=playback_true(&playback_json,root,"connected"); - if(active){ - if(capture_source==SOURCE_PICCAP_STARTING)capture_source=SOURCE_PICCAP; - if(piccap_connected)piccap_disconnected_since=0; - else if(!piccap_disconnected_since)piccap_disconnected_since=milliseconds(); - } - else if(capture_source!=SOURCE_PICCAP_STARTING || milliseconds()-piccap_action_at>=2000){ - if(capture_source!=SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000; - piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;piccap_disconnected_since=0; - if(handover_cooldown=3 && capture_source==SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000; - if(root)playback_json.put(root); - next_piccap_probe=milliseconds()+500; - if(command==2)piccap_request(0); - return true; +static bool media_reply(void *h, void *m, void *ctx) { + (void)h; + (void)ctx; + media_busy = false; + media_token = 0; + protection = playback_classify(&playback_json, payload(m)); + protection_at = milliseconds(); + if (protection == PLAYBACK_CLEAR) { + if (!clear_samples) + clear_since = protection_at; + if (clear_samples < 3) + clear_samples++; + unknown_since = 0; + } else { + clear_samples = 0; + clear_since = 0; + if (protection == PLAYBACK_UNKNOWN && !unknown_since) + unknown_since = protection_at; + else if (protection == PLAYBACK_PROTECTED) + unknown_since = 0; + } + return true; } -static bool piccap_request(int command){ - if(piccap_busy)return false; - const char *uri=command==3?"luna://org.webosbrew.piccap.service/getSettings":command==4?"luna://org.webosbrew.piccap.service/setSettings":command==1?"luna://org.webosbrew.piccap.service/start":command==2?"luna://org.webosbrew.piccap.service/stop":"luna://org.webosbrew.piccap.service/status"; - LSError e;error_init(&e); - char data[160];snprintf(data,sizeof(data),"{\"address\":\"%s\",\"port\":%u,\"unix-socket\":false}",target_piccap_host(&piccap_target),piccap_target.piccap_port); - piccap_busy=call_once(service_handle,uri,command==4?data:"{}",piccap_reply,(void*)(intptr_t)command,&piccap_token,&e); - piccap_deadline=milliseconds()+3000; - error_free(&e);if(command)piccap_action_at=milliseconds();return piccap_busy; +/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. Confirm stopped status before + * VT reuse. */ +static bool piccap_reply(void *h, void *m, void *ctx) { + (void)h; + int command = (int)(intptr_t)ctx; + piccap_busy = false; + piccap_token = 0; + void *root = playback_json.parse(payload(m)); + bool ok = playback_true(&playback_json, root, "returnValue"); + if (command == 0) { + void *running = playback_field(&playback_json, root, "isRunning"); + if (ok && running && playback_json.type(running) == 1) { + bool active = playback_json.boolean(running); + piccap_connected = playback_true(&playback_json, root, "connected"); + if (active) { + if (capture_source == SOURCE_PICCAP_STARTING) + capture_source = SOURCE_PICCAP; + if (piccap_connected) + piccap_disconnected_since = 0; + else if (!piccap_disconnected_since) + piccap_disconnected_since = milliseconds(); + } else if (capture_source != SOURCE_PICCAP_STARTING || + milliseconds() - piccap_action_at >= 2000) { + if (capture_source != SOURCE_PICCAP_STOPPING) + handover_cooldown = milliseconds() + 10000; + piccap_owned = false; + capture_source = SOURCE_HISTOGRAM; + piccap_connected = false; + piccap_disconnected_since = 0; + if (handover_cooldown < milliseconds() + 3000) + handover_cooldown = milliseconds() + 3000; + if (!retries.paused) + retries.due = milliseconds(); + } + } else if (!piccap_installed()) { + piccap_owned = false; + capture_source = SOURCE_HISTOGRAM; + piccap_connected = false; + } + } else if (command == 3 && capture_source == SOURCE_PICCAP_STARTING) { + void *port = playback_field(&playback_json, root, "port"), + *socket = playback_field(&playback_json, root, "unix-socket"); + bool same = + ok && + playback_is(&playback_json, root, "address", target_piccap_host(&piccap_target)) && + port && playback_json.type(port) == 3 && + strtoul(playback_json.string(port), NULL, 10) == piccap_target.piccap_port && socket && + playback_json.type(socket) == 1 && !playback_json.boolean(socket); + if (ok) { + if (!piccap_request(same ? 1 : 4)) { + capture_source = SOURCE_PICCAP_STOPPING; + snprintf(piccap_error, sizeof(piccap_error), "PicCap endpoint request failed"); + } + } else { + capture_source = SOURCE_PICCAP_STOPPING; + snprintf(piccap_error, sizeof(piccap_error), "Cannot read PicCap settings"); + } + } else if (command == 4) { + if (!ok) { + capture_source = SOURCE_PICCAP_STOPPING; + snprintf(piccap_error, sizeof(piccap_error), "PicCap rejected receiver settings"); + } + /* setSettings restarts PicCap itself; status confirms ownership. */ + } else if (command == 1 && !ok) { + capture_source = SOURCE_PICCAP_STOPPING; + handover_cooldown = milliseconds() + 10000; + } + if (command >= 3 && capture_source == SOURCE_PICCAP_STOPPING) + handover_cooldown = milliseconds() + 10000; + if (root) + playback_json.put(root); + next_piccap_probe = milliseconds() + 500; + if (command == 2) + piccap_request(0); + return true; +} +static bool piccap_request(int command) { + if (piccap_busy) + return false; + const char *uri = command == 3 ? "luna://org.webosbrew.piccap.service/getSettings" + : command == 4 ? "luna://org.webosbrew.piccap.service/setSettings" + : command == 1 ? "luna://org.webosbrew.piccap.service/start" + : command == 2 ? "luna://org.webosbrew.piccap.service/stop" + : "luna://org.webosbrew.piccap.service/status"; + LSError e; + error_init(&e); + char data[160]; + snprintf(data, sizeof(data), "{\"address\":\"%s\",\"port\":%u,\"unix-socket\":false}", + target_piccap_host(&piccap_target), piccap_target.piccap_port); + piccap_busy = call_once(service_handle, uri, command == 4 ? data : "{}", piccap_reply, + (void *)(intptr_t)command, &piccap_token, &e); + piccap_deadline = milliseconds() + 3000; + error_free(&e); + if (command) + piccap_action_at = milliseconds(); + return piccap_busy; } /* Return true while PicCap owns capture or a stop confirmation is pending. */ -static bool handover_tick(void){ - uint64_t now=milliseconds(); - if(media_busy && now>=media_deadline){LSError e;error_init(&e);cancel_call(service_handle,media_token,&e);error_free(&e);media_busy=false;media_token=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=0;if(!unknown_since)unknown_since=now;} - if(piccap_busy && now>=piccap_deadline){snprintf(piccap_error,sizeof(piccap_error),"PicCap request timed out");LSError e;error_init(&e);cancel_call(service_handle,piccap_token,&e);error_free(&e);piccap_busy=false;piccap_token=0;capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;} - if(handover_enabled && piccap_installed() && !retries.paused && playback_json.lib && !media_busy && now>=next_media_probe){ - LSError e;error_init(&e); - media_busy=call_once(service_handle,"luna://com.webos.media/getActivePipelines","{}",media_reply,NULL,&media_token,&e); - media_deadline=now+3000; - error_free(&e);next_media_probe=now+1000; - } - if(protection==PLAYBACK_CLEAR && now-protection_at>=3000){protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=0;if(!unknown_since)unknown_since=now;} - bool session=!CALIBRATION_ACTIVE && handover_enabled && target_piccap_compatible(&target) && piccap_installed() && !retries.paused && !retries.blocked; - bool clear=session && protection==PLAYBACK_CLEAR && now-protection_at<3000; - bool stable=clear && clear_samples>=3 && now-clear_since>=CLEAR_STABLE_MS && now>=handover_cooldown; - if(capture_source==SOURCE_HISTOGRAM && stable){ - piccap_target=target;piccap_error[0]=0;piccap_disconnected_since=now;release_capture(false);retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING; - if(!piccap_request(3)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+PICCAP_FAILURE_COOLDOWN_MS;} - } - bool grace=protection==PLAYBACK_UNKNOWN && unknown_since && now-unknown_since=PICCAP_LOSS_MS; - if(lost){ - snprintf(piccap_error,sizeof(piccap_error),"PicCap receiver disconnected; histogram fallback");piccap_fallbacks++;handover_cooldown=now+PICCAP_FAILURE_COOLDOWN_MS; - } - if(piccap_owned && (!session || (!clear && !grace) || lost || strcmp(piccap_target.host,target.host) || piccap_target.piccap_port!=target.piccap_port) && capture_source!=SOURCE_PICCAP_STOPPING){capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;} - if(piccap_owned && !piccap_busy && now>=next_piccap_probe){ - if(capture_source==SOURCE_PICCAP_STOPPING)piccap_request(2);else piccap_request(0); - next_piccap_probe=now+1000; - } - return piccap_owned; +static bool handover_tick(void) { + uint64_t now = milliseconds(); + if (media_busy && now >= media_deadline) { + LSError e; + error_init(&e); + cancel_call(service_handle, media_token, &e); + error_free(&e); + media_busy = false; + media_token = 0; + protection = PLAYBACK_UNKNOWN; + clear_samples = 0; + clear_since = 0; + if (!unknown_since) + unknown_since = now; + } + if (piccap_busy && now >= piccap_deadline) { + snprintf(piccap_error, sizeof(piccap_error), "PicCap request timed out"); + LSError e; + error_init(&e); + cancel_call(service_handle, piccap_token, &e); + error_free(&e); + piccap_busy = false; + piccap_token = 0; + capture_source = SOURCE_PICCAP_STOPPING; + next_piccap_probe = 0; + } + if (handover_enabled && piccap_installed() && !retries.paused && playback_json.lib && + !media_busy && now >= next_media_probe) { + LSError e; + error_init(&e); + media_busy = call_once(service_handle, "luna://com.webos.media/getActivePipelines", "{}", + media_reply, NULL, &media_token, &e); + media_deadline = now + 3000; + error_free(&e); + next_media_probe = now + 1000; + } + if (protection == PLAYBACK_CLEAR && now - protection_at >= 3000) { + protection = PLAYBACK_UNKNOWN; + clear_samples = 0; + clear_since = 0; + if (!unknown_since) + unknown_since = now; + } + bool session = !CALIBRATION_ACTIVE && handover_enabled && target_piccap_compatible(&target) && + piccap_installed() && !retries.paused && !retries.blocked; + bool clear = session && protection == PLAYBACK_CLEAR && now - protection_at < 3000; + bool stable = clear && clear_samples >= 3 && now - clear_since >= CLEAR_STABLE_MS && + now >= handover_cooldown; + if (capture_source == SOURCE_HISTOGRAM && stable) { + piccap_target = target; + piccap_error[0] = 0; + piccap_disconnected_since = now; + release_capture(false); + retries.due = 0; + piccap_owned = true; + capture_source = SOURCE_PICCAP_STARTING; + if (!piccap_request(3)) { + capture_source = SOURCE_PICCAP_STOPPING; + handover_cooldown = now + PICCAP_FAILURE_COOLDOWN_MS; + } + } + bool grace = protection == PLAYBACK_UNKNOWN && unknown_since && + now - unknown_since < UNKNOWN_GRACE_MS && capture_source == SOURCE_PICCAP; + bool lost = piccap_owned && capture_source == SOURCE_PICCAP && piccap_disconnected_since && + now - piccap_disconnected_since >= PICCAP_LOSS_MS; + if (lost) { + snprintf(piccap_error, sizeof(piccap_error), + "PicCap receiver disconnected; histogram fallback"); + piccap_fallbacks++; + handover_cooldown = now + PICCAP_FAILURE_COOLDOWN_MS; + } + if (piccap_owned && + (!session || (!clear && !grace) || lost || strcmp(piccap_target.host, target.host) || + piccap_target.piccap_port != target.piccap_port) && + capture_source != SOURCE_PICCAP_STOPPING) { + capture_source = SOURCE_PICCAP_STOPPING; + next_piccap_probe = 0; + } + if (piccap_owned && !piccap_busy && now >= next_piccap_probe) { + if (capture_source == SOURCE_PICCAP_STOPPING) + piccap_request(2); + else + piccap_request(0); + next_piccap_probe = now + 1000; + } + return piccap_owned; } #endif diff --git a/native/histogram.h b/native/histogram.h index a3a3d16..02d9ecc 100644 --- a/native/histogram.h +++ b/native/histogram.h @@ -4,12 +4,18 @@ #include /* LG ABI: region ID followed by three planar channels, 256 uint16 bins * per channel. Bin index is the 8-bit intensity (0..255). */ -typedef struct { uint32_t id; uint16_t bins[3][256]; } Histogram; -_Static_assert(sizeof(Histogram)==0x604,"LG ABI record size"); -_Static_assert(offsetof(Histogram,bins)==4,"LG ABI channel offset"); +typedef struct { + uint32_t id; + uint16_t bins[3][256]; +} Histogram; +_Static_assert(sizeof(Histogram) == 0x604, "LG ABI record size"); +_Static_assert(offsetof(Histogram, bins) == 4, "LG ABI channel offset"); static inline unsigned char histogram_mean(const uint16_t bins[256]) { - uint64_t count=0,weight=0; - for(unsigned b=0;b<256;b++){count+=bins[b];weight+=(uint64_t)bins[b]*b;} - return count?(unsigned char)((weight+count/2)/count):0; + uint64_t count = 0, weight = 0; + for (unsigned b = 0; b < 256; b++) { + count += bins[b]; + weight += (uint64_t)bins[b] * b; + } + return count ? (unsigned char)((weight + count / 2) / count) : 0; } #endif diff --git a/native/interpolation.h b/native/interpolation.h index c454107..3b4f5c1 100644 --- a/native/interpolation.h +++ b/native/interpolation.h @@ -1,6 +1,10 @@ #ifndef SPATIAL_INTERPOLATION_H #define SPATIAL_INTERPOLATION_H -static inline void blend_color(const unsigned char a[3],const unsigned char b[3],unsigned weight,unsigned char out[3]){ - for(unsigned c=0;c<3;c++)out[c]=(unsigned char)(((unsigned)a[c]*(65535-weight)+(unsigned)b[c]*weight+32767)/65535); +static inline void blend_color(const unsigned char a[3], const unsigned char b[3], unsigned weight, + unsigned char out[3]) { + for (unsigned c = 0; c < 3; c++) + out[c] = + (unsigned char)(((unsigned)a[c] * (65535 - weight) + (unsigned)b[c] * weight + 32767) / + 65535); } #endif diff --git a/native/layout.h b/native/layout.h index 1f9b369..53b893a 100644 --- a/native/layout.h +++ b/native/layout.h @@ -5,54 +5,92 @@ #include #include #include -typedef struct {unsigned char a,b;unsigned weight;} LedBlend; +typedef struct { + unsigned char a, b; + unsigned weight; +} LedBlend; #include #include #include #include "calibration.h" -static double screen_perimeter(double x,double y){ - double distances[]={y,1-x,1-y,x},positions[]={x,1+y,3-x,4-y};unsigned edge=0; - for(unsigned i=1;i<4;i++)if(distances[i]=4?positions[edge]-4:positions[edge]; +static double screen_perimeter(double x, double y) { + double distances[] = {y, 1 - x, 1 - y, x}, positions[] = {x, 1 + y, 3 - x, 4 - y}; + unsigned edge = 0; + for (unsigned i = 1; i < 4; i++) + if (distances[i] < distances[edge]) + edge = i; + return positions[edge] >= 4 ? positions[edge] - 4 : positions[edge]; } -static LedBlend layout_blend(double x,double y){ - const RimPoint *rim=calibration_effective()==CAL_CUSTOM?calibration.points:g4_rim; - double t=screen_perimeter(x,y);unsigned n=calibration_effective()==CAL_CUSTOM?calibration.count:sizeof(g4_rim)/sizeof(*g4_rim),right=0; - while(right0 && n<=16384; - LedBlend *mapping=valid?calloc(n,sizeof(*mapping)):NULL;valid=valid && mapping; - const char *keys[]={"hmin","hmax","vmin","vmax"}; - for(size_t i=0;valid && i=0 && coordinates[c]<=1;} - } - if(valid)valid=coordinates[0]<=coordinates[1] && coordinates[2]<=coordinates[3]; - if(valid)mapping[i]=layout_blend((coordinates[0]+coordinates[1])/2,(coordinates[2]+coordinates[3])/2); - } - if(root)put(root); - dlclose(lib); - if(!valid){free(mapping);return false;} - *result=mapping;*count=(unsigned)n;return true; +static bool layout_from_json(const char *text, LedBlend **result, unsigned *count) { + void *lib = dlopen("libjson-c.so.5", RTLD_NOW); + if (!lib) + return false; + void *(*parse)(const char *) = dlsym(lib, "json_tokener_parse"); + int (*get)(const void *, const char *, void **) = dlsym(lib, "json_object_object_get_ex"); + int (*type)(const void *) = dlsym(lib, "json_object_get_type"); + size_t (*length)(const void *) = dlsym(lib, "json_object_array_length"); + void *(*at)(const void *, size_t) = dlsym(lib, "json_object_array_get_idx"); + double (*number)(const void *) = dlsym(lib, "json_object_get_double"); + int (*put)(void *) = dlsym(lib, "json_object_put"); + if (!parse || !get || !type || !length || !at || !number || !put) { + dlclose(lib); + return false; + } + void *root = parse(text), *info = NULL, *leds = NULL; + bool valid = root && type(root) == 4 && get(root, "info", &info) && type(info) == 4 && + get(info, "leds", &leds) && type(leds) == 5; + size_t n = valid ? length(leds) : 0; + valid = valid && n > 0 && n <= 16384; + LedBlend *mapping = valid ? calloc(n, sizeof(*mapping)) : NULL; + valid = valid && mapping; + const char *keys[] = {"hmin", "hmax", "vmin", "vmax"}; + for (size_t i = 0; valid && i < n; i++) { + void *led = at(leds, i); + double coordinates[4]; + valid = led && type(led) == 4; + for (unsigned c = 0; valid && c < 4; c++) { + void *v = NULL; + valid = get(led, keys[c], &v) && (type(v) == 2 || type(v) == 3); + if (valid) { + coordinates[c] = number(v); + valid = isfinite(coordinates[c]) && coordinates[c] >= 0 && coordinates[c] <= 1; + } + } + if (valid) + valid = coordinates[0] <= coordinates[1] && coordinates[2] <= coordinates[3]; + if (valid) + mapping[i] = layout_blend((coordinates[0] + coordinates[1]) / 2, + (coordinates[2] + coordinates[3]) / 2); + } + if (root) + put(root); + dlclose(lib); + if (!valid) { + free(mapping); + return false; + } + *result = mapping; + *count = (unsigned)n; + return true; } #endif diff --git a/native/layout_refresh.h b/native/layout_refresh.h index 9e1a985..19a65ff 100644 --- a/native/layout_refresh.h +++ b/native/layout_refresh.h @@ -1,34 +1,53 @@ #ifndef HYPERDRM_LAYOUT_REFRESH_H #define HYPERDRM_LAYOUT_REFRESH_H #define LAYOUT_REFRESH_MS 5000 -static ReceiverProbe layout_probe={.fd=-1}; -static uint64_t next_layout_check,last_layout_check; -static unsigned layout_updates,layout_probe_generation; +static ReceiverProbe layout_probe = {.fd = -1}; +static uint64_t next_layout_check, last_layout_check; +static unsigned layout_updates, layout_probe_generation; static char layout_error[128]; -static void layout_refresh_reset(uint64_t due){ - probe_close(&layout_probe);layout_probe.stage=TEST_IDLE;next_layout_check=due; +static void layout_refresh_reset(uint64_t due) { + probe_close(&layout_probe); + layout_probe.stage = TEST_IDLE; + next_layout_check = due; } -static bool layout_mapping_changed(const LedBlend *next,unsigned count){ - if(count!=led_count || !led_blends)return true; - for(unsigned i=0;i=next_layout_check){ - layout_probe_generation=calibration_generation;probe_begin(&layout_probe,&target,now);next_layout_check=now+LAYOUT_REFRESH_MS; - } +static void layout_refresh_tick(PlaybackJson *json, uint64_t now, bool running) { + if (!running) { + layout_refresh_reset(now + LAYOUT_REFRESH_MS); + return; + } + probe_tick(&layout_probe, json, now); + if (layout_probe.stage == TEST_DONE) { + if (layout_probe_generation == calibration_generation) { + if (layout_mapping_changed(layout_probe.mapping, layout_probe.leds)) { + free(led_blends); + led_blends = layout_probe.mapping; + layout_probe.mapping = NULL; + led_count = layout_probe.leds; + layout_updates++; + } + last_layout_check = now; + layout_error[0] = 0; + } else + next_layout_check = now; + probe_close(&layout_probe); + layout_probe.stage = TEST_IDLE; + } else if (layout_probe.stage == TEST_FAILED) { + snprintf(layout_error, sizeof(layout_error), "%s", layout_probe.error); + layout_probe.stage = TEST_IDLE; + } + if (!probe_busy(&layout_probe) && now >= next_layout_check) { + layout_probe_generation = calibration_generation; + probe_begin(&layout_probe, &target, now); + next_layout_check = now + LAYOUT_REFRESH_MS; + } } #endif diff --git a/native/playback.h b/native/playback.h index b00f708..5218b9a 100644 --- a/native/playback.h +++ b/native/playback.h @@ -5,43 +5,89 @@ #include #include typedef struct { - void *lib;void *(*parse)(const char*);int (*get)(const void*,const char*,void**); - int (*type)(const void*);size_t (*length)(const void*);void *(*at)(const void*,size_t); - const char *(*string)(const void*);int (*boolean)(const void*);int (*put)(void*); + void *lib; + void *(*parse)(const char *); + int (*get)(const void *, const char *, void **); + int (*type)(const void *); + size_t (*length)(const void *); + void *(*at)(const void *, size_t); + const char *(*string)(const void *); + int (*boolean)(const void *); + int (*put)(void *); } PlaybackJson; -static inline bool playback_json_init(PlaybackJson *j){ - j->lib=dlopen("libjson-c.so.5",RTLD_NOW);if(!j->lib)return false; - j->parse=dlsym(j->lib,"json_tokener_parse");j->get=dlsym(j->lib,"json_object_object_get_ex"); - j->type=dlsym(j->lib,"json_object_get_type");j->length=dlsym(j->lib,"json_object_array_length"); - j->at=dlsym(j->lib,"json_object_array_get_idx");j->string=dlsym(j->lib,"json_object_get_string"); - j->boolean=dlsym(j->lib,"json_object_get_boolean");j->put=dlsym(j->lib,"json_object_put"); - if(!j->parse||!j->get||!j->type||!j->length||!j->at||!j->string||!j->boolean||!j->put){dlclose(j->lib);j->lib=NULL;return false;}return true; +static inline bool playback_json_init(PlaybackJson *j) { + j->lib = dlopen("libjson-c.so.5", RTLD_NOW); + if (!j->lib) + return false; + j->parse = dlsym(j->lib, "json_tokener_parse"); + j->get = dlsym(j->lib, "json_object_object_get_ex"); + j->type = dlsym(j->lib, "json_object_get_type"); + j->length = dlsym(j->lib, "json_object_array_length"); + j->at = dlsym(j->lib, "json_object_array_get_idx"); + j->string = dlsym(j->lib, "json_object_get_string"); + j->boolean = dlsym(j->lib, "json_object_get_boolean"); + j->put = dlsym(j->lib, "json_object_put"); + if (!j->parse || !j->get || !j->type || !j->length || !j->at || !j->string || !j->boolean || + !j->put) { + dlclose(j->lib); + j->lib = NULL; + return false; + } + return true; } -static inline void *playback_field(PlaybackJson *j,void *o,const char *key){void *v=NULL;return o && j->type(o)==4 && j->get(o,key,&v)?v:NULL;} -static inline bool playback_true(PlaybackJson *j,void *o,const char *key){void *v=playback_field(j,o,key);return v && j->type(v)==1 && j->boolean(v);} -static inline bool playback_is(PlaybackJson *j,void *o,const char *key,const char *value){void *v=playback_field(j,o,key);return v && j->type(v)==6 && strcmp(j->string(v),value)==0;} -typedef enum {PLAYBACK_UNKNOWN,PLAYBACK_CLEAR,PLAYBACK_PROTECTED} PlaybackProtection; +static inline void *playback_field(PlaybackJson *j, void *o, const char *key) { + void *v = NULL; + return o && j->type(o) == 4 && j->get(o, key, &v) ? v : NULL; +} +static inline bool playback_true(PlaybackJson *j, void *o, const char *key) { + void *v = playback_field(j, o, key); + return v && j->type(v) == 1 && j->boolean(v); +} +static inline bool playback_is(PlaybackJson *j, void *o, const char *key, const char *value) { + void *v = playback_field(j, o, key); + return v && j->type(v) == 6 && strcmp(j->string(v), value) == 0; +} +typedef enum { PLAYBACK_UNKNOWN, PLAYBACK_CLEAR, PLAYBACK_PROTECTED } PlaybackProtection; /* G4 resource-state classification, not black-frame or app-name guessing. * Any active Secure Video Path wins. Only a foreground media decoder without * a secure resource is eligible; HDMI, idle and malformed replies are unknown. */ -static inline PlaybackProtection playback_classify(PlaybackJson *j,const char *text){ - void *root=j->parse(text);if(!root)return PLAYBACK_UNKNOWN; - bool clear=false,invalid=j->type(root)!=5,secure=false; - size_t n=invalid?0:j->length(root); - for(size_t i=0;iat(root,i);if(!p || j->type(p)!=4){invalid=true;continue;} - void *resources=playback_field(j,p,"resource");if(!resources)resources=playback_field(j,p,"resources"); - if(!resources || j->type(resources)!=5){invalid=true;continue;} - bool decoder=false; - for(size_t k=0;klength(resources);k++){ - void *resource=j->at(resources,k);void *name=playback_field(j,resource,"resource"); - if(!name || j->type(name)!=6){invalid=true;continue;} - const char *s=j->string(name); - if(strcmp(s,"SVP_CPB")==0)secure=true; - if(strcmp(s,"VDEC")==0)decoder=true; - } - if(decoder && playback_true(j,p,"is_foreground") && playback_is(j,p,"type","media"))clear=true; - } - j->put(root);return secure?PLAYBACK_PROTECTED:clear&&!invalid?PLAYBACK_CLEAR:PLAYBACK_UNKNOWN; +static inline PlaybackProtection playback_classify(PlaybackJson *j, const char *text) { + void *root = j->parse(text); + if (!root) + return PLAYBACK_UNKNOWN; + bool clear = false, invalid = j->type(root) != 5, secure = false; + size_t n = invalid ? 0 : j->length(root); + for (size_t i = 0; i < n; i++) { + void *p = j->at(root, i); + if (!p || j->type(p) != 4) { + invalid = true; + continue; + } + void *resources = playback_field(j, p, "resource"); + if (!resources) + resources = playback_field(j, p, "resources"); + if (!resources || j->type(resources) != 5) { + invalid = true; + continue; + } + bool decoder = false; + for (size_t k = 0; k < j->length(resources); k++) { + void *resource = j->at(resources, k); + void *name = playback_field(j, resource, "resource"); + if (!name || j->type(name) != 6) { + invalid = true; + continue; + } + const char *s = j->string(name); + if (strcmp(s, "SVP_CPB") == 0) + secure = true; + if (strcmp(s, "VDEC") == 0) + decoder = true; + } + if (decoder && playback_true(j, p, "is_foreground") && playback_is(j, p, "type", "media")) + clear = true; + } + j->put(root); + return secure ? PLAYBACK_PROTECTED : clear && !invalid ? PLAYBACK_CLEAR : PLAYBACK_UNKNOWN; } #endif diff --git a/native/receiver_stream.h b/native/receiver_stream.h index d2dd6ef..312495c 100644 --- a/native/receiver_stream.h +++ b/native/receiver_stream.h @@ -5,67 +5,223 @@ #endif /* Independent raw LED subscription: bounded, nonblocking, before receiver * color correction. Old result and newer data envelopes are both accepted. */ -typedef struct {int fd,stage;uint64_t deadline,retry_at,updated;unsigned sequence,count,revision,layout_revision;LedBlend *mapping;char *data,*request;size_t used,sent,length;unsigned char colors[16][3];bool valid;} ReceiverStream; -static ReceiverStream stream={.fd=-1}; -static void stream_close(uint64_t now){if(stream.fd>=0)close(stream.fd);free(stream.mapping);free(stream.data);free(stream.request);stream=(ReceiverStream){.fd=-1,.retry_at=now+5000};} -static bool stream_queue(const char *line){ - if(stream.fd<0 || stream.request)return false; - stream.request=strdup(line);if(!stream.request)return false;stream.length=strlen(line);stream.sent=0;return true; +#include "runtime_states.h" +typedef struct { + int fd; + StreamStage stage; + uint64_t deadline, retry_at, updated; + unsigned sequence, count, revision, layout_revision; + LedBlend *mapping; + char *data, *request; + size_t used, sent, length; + unsigned char colors[16][3]; + bool valid; +} ReceiverStream; +static ReceiverStream stream = {.fd = -1}; +static void stream_close(uint64_t now) { + if (stream.fd >= 0) + close(stream.fd); + free(stream.mapping); + free(stream.data); + free(stream.request); + stream = (ReceiverStream){.fd = -1, .retry_at = now + 5000}; } -static void stream_start(uint64_t now){ - stream.fd=target_open(&target);if(stream.fd<0){stream.retry_at=now+5000;return;} - stream.data=malloc(262145);if(!stream.data){stream_close(now);return;}stream.stage=1;stream.deadline=now+3000;stream.revision=calibration_revision();stream.layout_revision=RECEIVER_LAYOUT_REVISION; +static bool stream_queue(const char *line) { + if (stream.fd < 0 || stream.request) + return false; + stream.request = strdup(line); + if (!stream.request) + return false; + stream.length = strlen(line); + stream.sent = 0; + return true; } -static bool stream_colors(PlaybackJson *j,void *root,uint64_t now){ - void *body=playback_field(j,root,"result");if(!body)body=playback_field(j,root,"data"); - void *leds=playback_field(j,body,"leds"); - if(!leds || j->type(leds)!=5)return false; - if(j->length(leds)!=(size_t)stream.count*3){stream.revision=0;return false;} - uint64_t sums[16][3]={{0}},weights[16]={0}; - for(unsigned i=0;iat(leds,i*3+c);if(!v || j->type(v)!=3)return false;char *end;long n=strtol(j->string(v),&end,10);if(*end || n<0 || n>255)return false;values[c]=(unsigned)n;} - LedBlend b=stream.mapping[i];unsigned w[2]={65535-b.weight,b.weight},r[2]={b.a,b.b}; - for(unsigned k=0;k<2;k++){weights[r[k]]+=w[k];for(unsigned c=0;c<3;c++)sums[r[k]][c]+=(uint64_t)values[c]*w[k];} - } - unsigned char mean[3]={0};uint64_t total=0,all[3]={0}; - for(unsigned r=0;r<16;r++){total+=weights[r];for(unsigned c=0;c<3;c++)all[c]+=sums[r][c];} - if(!total)return false; - for(unsigned c=0;c<3;c++)mean[c]=(unsigned char)(all[c]/total); - for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)stream.colors[r][c]=weights[r]?(unsigned char)(sums[r][c]/weights[r]):mean[c]; - stream.valid=true;stream.updated=now;stream.sequence++;return true; +static void stream_start(uint64_t now) { + stream.fd = target_open(&target); + if (stream.fd < 0) { + stream.retry_at = now + 5000; + return; + } + stream.data = malloc(262145); + if (!stream.data) { + stream_close(now); + return; + } + stream.stage = STREAM_CONNECTING; + stream.deadline = now + 3000; + stream.revision = calibration_revision(); + stream.layout_revision = RECEIVER_LAYOUT_REVISION; } -static void stream_tick(PlaybackJson *j,uint64_t now,bool enabled){ - if(!enabled){if(stream.fd>=0)stream_close(now);return;} - if(stream.fd>=0 && (stream.revision!=calibration_revision() || stream.layout_revision!=RECEIVER_LAYOUT_REVISION)){stream_close(now);stream.retry_at=now;} - if(stream.fd<0){if(now>=stream.retry_at)stream_start(now);return;} - if(stream.stage<4 && now>=stream.deadline){stream_close(now);return;} - if(stream.stage==1){ - struct pollfd p={stream.fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);if(poll(&p,1,0)<=0)return; - if(getsockopt(stream.fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){stream_close(now);return;} - char line[384];if(target.token[0]){snprintf(line,sizeof(line),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",target.token);stream.stage=2;} - else {snprintf(line,sizeof(line),"{\"command\":\"serverinfo\"}\n");stream.stage=3;} - if(!stream_queue(line)){stream_close(now);return;} - } - if(stream.request){ - ssize_t n=send(stream.fd,stream.request+stream.sent,stream.length-stream.sent,MSG_NOSIGNAL); - if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return; - if(n<=0){stream_close(now);return;}stream.sent+=(size_t)n; - if(stream.sent==stream.length){free(stream.request);stream.request=NULL;} - } - if(stream.used==262144){stream_close(now);return;} - size_t available=262144-stream.used;if(available>65536)available=65536; - ssize_t n=recv(stream.fd,stream.data+stream.used,available,0); - if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return; - if(n<=0){stream_close(now);return;}stream.used+=(size_t)n;stream.data[stream.used]=0; - for(unsigned messages=0;messages<8;messages++){ - char *end=strchr(stream.data,'\n');if(!end)break;*end=0;void *root=j->parse(stream.data); - if(stream.stage==2){if(!playback_true(j,root,"success")){if(root)j->put(root);stream_close(now);return;}stream.stage=3;stream_queue("{\"command\":\"serverinfo\"}\n");stream.deadline=now+3000;} - else if(stream.stage==3){ - LedBlend *mapping=NULL;unsigned count=0;if(!layout_from_json(stream.data,&mapping,&count)){if(root)j->put(root);stream_close(now);return;} - stream.mapping=mapping;stream.count=count;stream.stage=4;stream_queue("{\"command\":\"ledcolors\",\"subcommand\":\"ledstream-start\",\"interval\":100}\n"); - }else if(playback_is(j,root,"command","ledcolors-ledstream-update"))stream_colors(j,root,now); - if(root)j->put(root); - size_t consumed=(size_t)(end-stream.data)+1;memmove(stream.data,stream.data+consumed,stream.used-consumed);stream.used-=consumed;stream.data[stream.used]=0; - } +static bool stream_colors(PlaybackJson *j, void *root, uint64_t now) { + void *body = playback_field(j, root, "result"); + if (!body) + body = playback_field(j, root, "data"); + void *leds = playback_field(j, body, "leds"); + if (!leds || j->type(leds) != 5) + return false; + if (j->length(leds) != (size_t)stream.count * 3) { + stream.revision = 0; + return false; + } + uint64_t sums[16][3] = {{0}}, weights[16] = {0}; + for (unsigned i = 0; i < stream.count; i++) { + unsigned values[3]; + for (unsigned c = 0; c < 3; c++) { + void *v = j->at(leds, i * 3 + c); + if (!v || j->type(v) != 3) + return false; + char *end; + long n = strtol(j->string(v), &end, 10); + if (*end || n < 0 || n > 255) + return false; + values[c] = (unsigned)n; + } + LedBlend b = stream.mapping[i]; + unsigned w[2] = {65535 - b.weight, b.weight}, r[2] = {b.a, b.b}; + for (unsigned k = 0; k < 2; k++) { + weights[r[k]] += w[k]; + for (unsigned c = 0; c < 3; c++) + sums[r[k]][c] += (uint64_t)values[c] * w[k]; + } + } + unsigned char mean[3] = {0}; + uint64_t total = 0, all[3] = {0}; + for (unsigned r = 0; r < 16; r++) { + total += weights[r]; + for (unsigned c = 0; c < 3; c++) + all[c] += sums[r][c]; + } + if (!total) + return false; + for (unsigned c = 0; c < 3; c++) + mean[c] = (unsigned char)(all[c] / total); + for (unsigned r = 0; r < 16; r++) + for (unsigned c = 0; c < 3; c++) + stream.colors[r][c] = weights[r] ? (unsigned char)(sums[r][c] / weights[r]) : mean[c]; + stream.valid = true; + stream.updated = now; + stream.sequence++; + return true; +} +static void stream_tick(PlaybackJson *j, uint64_t now, bool enabled) { + if (!enabled) { + if (stream.fd >= 0) + stream_close(now); + return; + } + if (stream.fd >= 0 && (stream.revision != calibration_revision() || + stream.layout_revision != RECEIVER_LAYOUT_REVISION)) { + stream_close(now); + stream.retry_at = now; + } + if (stream.fd < 0) { + if (now >= stream.retry_at) + stream_start(now); + return; + } + if (stream.stage < STREAM_SUBSCRIBED && now >= stream.deadline) { + stream_close(now); + return; + } + if (stream.stage == STREAM_CONNECTING) { + struct pollfd p = {stream.fd, POLLOUT, 0}; + int error = 0; + socklen_t size = sizeof(error); + if (poll(&p, 1, 0) <= 0) + return; + if (getsockopt(stream.fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 || error) { + stream_close(now); + return; + } + char line[384]; + if (target.token[0]) { + snprintf(line, sizeof(line), + "{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n", + target.token); + stream.stage = STREAM_AUTHENTICATING; + } else { + snprintf(line, sizeof(line), "{\"command\":\"serverinfo\"}\n"); + stream.stage = STREAM_LAYOUT; + } + if (!stream_queue(line)) { + stream_close(now); + return; + } + } + if (stream.request) { + ssize_t n = send(stream.fd, stream.request + stream.sent, stream.length - stream.sent, + MSG_NOSIGNAL); + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) + return; + if (n <= 0) { + stream_close(now); + return; + } + stream.sent += (size_t)n; + if (stream.sent == stream.length) { + free(stream.request); + stream.request = NULL; + } + } + /* Drain complete buffered replies even when recv would return EAGAIN. + * The per-tick budget applies independently of incoming network traffic. */ + if (stream.used == 262144 && !strchr(stream.data, '\n')) { + stream_close(now); + return; + } + size_t available = 262144 - stream.used; + if (available > 65536) + available = 65536; + ssize_t n = recv(stream.fd, stream.data + stream.used, available, 0); + if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { + stream_close(now); + return; + } + if (n == 0 && available) { + stream_close(now); + return; + } + if (n > 0) + stream.used += (size_t)n; + stream.data[stream.used] = 0; + for (unsigned messages = 0; messages < 8; messages++) { + char *end = strchr(stream.data, '\n'); + if (!end) + break; + *end = 0; + void *root = j->parse(stream.data); + if (stream.stage == STREAM_AUTHENTICATING) { + if (!playback_true(j, root, "success")) { + if (root) + j->put(root); + stream_close(now); + return; + } + stream.stage = STREAM_LAYOUT; + stream_queue("{\"command\":\"serverinfo\"}\n"); + stream.deadline = now + 3000; + } else if (stream.stage == STREAM_LAYOUT) { + LedBlend *mapping = NULL; + unsigned count = 0; + if (!layout_from_json(stream.data, &mapping, &count)) { + if (root) + j->put(root); + stream_close(now); + return; + } + stream.mapping = mapping; + stream.count = count; + stream.stage = STREAM_SUBSCRIBED; + stream_queue("{\"command\":\"ledcolors\",\"subcommand\":\"ledstream-start\"," + "\"interval\":100}\n"); + } else if (playback_is(j, root, "command", "ledcolors-ledstream-update")) + stream_colors(j, root, now); + if (root) + j->put(root); + size_t consumed = (size_t)(end - stream.data) + 1; + memmove(stream.data, stream.data + consumed, stream.used - consumed); + stream.used -= consumed; + stream.data[stream.used] = 0; + } } #endif diff --git a/native/retry.h b/native/retry.h index 6bf3204..abe2352 100644 --- a/native/retry.h +++ b/native/retry.h @@ -5,26 +5,50 @@ #include #define RETRY_DELAY_MS 5000 /* Continuous autostart retries and bounded manual source recovery. */ -typedef struct {uint64_t due;unsigned attempts,quick_retries;bool automatic,paused,blocked;} RetryState; -static void retry_reset(RetryState *state){*state=(RetryState){0};} -static void retry_pause(RetryState *state){retry_reset(state);state->paused=true;} -static void retry_update(RetryState *state,bool enabled,bool running,uint64_t now){ - bool previous=state->automatic; - state->automatic=enabled && !state->paused && !state->blocked; - if(previous && !state->automatic){state->due=0;state->attempts=0;} - if(state->automatic && !running && !state->due)state->due=now+RETRY_DELAY_MS; +typedef struct { + uint64_t due; + unsigned attempts, quick_retries; + bool automatic, paused, blocked; +} RetryState; +static void retry_reset(RetryState *state) { + *state = (RetryState){0}; } -static bool retry_take(RetryState *state,uint64_t now){ - if(!state->due || nowdue || state->paused || state->blocked)return false; - state->due=0;if(state->attemptsattempts++;return true; +static void retry_pause(RetryState *state) { + retry_reset(state); + state->paused = true; } -static void retry_failure(RetryState *state,uint64_t now){ - if(!state->paused && !state->blocked && (state->automatic || state->attempts<6)){ - if(state->quick_retries){state->quick_retries--;state->due=now+400;} - else state->due=now+RETRY_DELAY_MS; - } +static void retry_update(RetryState *state, bool enabled, bool running, uint64_t now) { + bool previous = state->automatic; + state->automatic = enabled && !state->paused && !state->blocked; + if (previous && !state->automatic) { + state->due = 0; + state->attempts = 0; + } + if (state->automatic && !running && !state->due) + state->due = now + RETRY_DELAY_MS; } -static inline void retry_source_transition(RetryState *state,uint64_t now){ - if(!state->paused && !state->blocked){state->attempts=0;state->quick_retries=2;state->due=now+200;} +static bool retry_take(RetryState *state, uint64_t now) { + if (!state->due || now < state->due || state->paused || state->blocked) + return false; + state->due = 0; + if (state->attempts < UINT_MAX) + state->attempts++; + return true; +} +static void retry_failure(RetryState *state, uint64_t now) { + if (!state->paused && !state->blocked && (state->automatic || state->attempts < 6)) { + if (state->quick_retries) { + state->quick_retries--; + state->due = now + 400; + } else + state->due = now + RETRY_DELAY_MS; + } +} +static inline void retry_source_transition(RetryState *state, uint64_t now) { + if (!state->paused && !state->blocked) { + state->attempts = 0; + state->quick_retries = 2; + state->due = now + 200; + } } #endif diff --git a/native/runtime_states.h b/native/runtime_states.h new file mode 100644 index 0000000..be02371 --- /dev/null +++ b/native/runtime_states.h @@ -0,0 +1,12 @@ +#ifndef HYPERDRM_RUNTIME_STATES_H +#define HYPERDRM_RUNTIME_STATES_H +/* Connection and source transitions are independent state machines. */ +typedef enum { + STREAM_DISCONNECTED, + STREAM_CONNECTING, + STREAM_AUTHENTICATING, + STREAM_LAYOUT, + STREAM_SUBSCRIBED +} StreamStage; +typedef enum { BRIDGE_IDLE, BRIDGE_HOLDING, BRIDGE_WAITING_FRAME, BRIDGE_FADING } BridgePhase; +#endif diff --git a/native/service.c b/native/service.c index eefee4e..b0f86d7 100644 --- a/native/service.c +++ b/native/service.c @@ -17,6 +17,7 @@ #include #include #include +#include "version.h" #include "histogram.h" #include "blackbars.h" #include "color_boost.h" @@ -30,379 +31,383 @@ #include "continuity.h" #define SERVICE "org.webosbrew.histogramhyperion.service" #define PRIORITY 149 -typedef struct {int code;char *message;const char *file;int line;const char *func;void *padding;unsigned long magic;} LSError; -typedef bool (*Callback)(void*,void*,void*); -typedef struct {const char *name;Callback function;int flags;} Method; -static bool (*reply)(void*,void*,const char*,LSError*); -static const char *(*payload)(void*); -static bool (*error_init)(LSError*); -static void (*error_free)(LSError*); -static void *(*vt_init)(bool(*)(void)); -static int (*vt_preset)(void*,int),(*vt_get)(void*,Histogram*),(*vt_finish)(void*); +typedef struct { + int code; + char *message; + const char *file; + int line; + const char *func; + void *padding; + unsigned long magic; +} LSError; +typedef bool (*Callback)(void *, void *, void *); +typedef struct { + const char *name; + Callback function; + int flags; +} Method; +static bool (*reply)(void *, void *, const char *, LSError *); +static const char *(*payload)(void *); +static bool (*error_init)(LSError *); +static void (*error_free)(LSError *); +static void *(*vt_init)(bool (*)(void)); +static int (*vt_preset)(void *, int), (*vt_get)(void *, Histogram *), (*vt_finish)(void *); static void *capture; static char lg_diagnostic[256]; /* Process-local LG diagnostics; no changes to the loaded LG library. */ -void printLog(const char *context,int level,const char *message_id, - const char *file,const char *function,int line,const char *format,...) { - (void)context;(void)message_id;(void)file; - if(capture && level>3)return; - if(format){va_list args;va_start(args,format);vsnprintf(lg_diagnostic,sizeof(lg_diagnostic),format,args);va_end(args);} - fprintf(stderr,"LG %s:%d: ",function?function:"?",line); - if(format){va_list args;va_start(args,format);vfprintf(stderr,format,args);va_end(args);} - fputc('\n',stderr); +void printLog(const char *context, int level, const char *message_id, const char *file, + const char *function, int line, const char *format, ...) { + (void)context; + (void)message_id; + (void)file; + if (capture && level > 3) + return; + if (format) { + va_list args; + va_start(args, format); + vsnprintf(lg_diagnostic, sizeof(lg_diagnostic), format, args); + va_end(args); + } + fprintf(stderr, "LG %s:%d: ", function ? function : "?", line); + if (format) { + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + } + fputc('\n', stderr); } -static int output=-1; +static int output = -1; static unsigned long frames; static unsigned char rgb[16][3]; static unsigned char sent_rgb[3]; static unsigned black_percent[16]; static unsigned capture_preset; -static unsigned letterbox_mode,settling; -static int pending_letterbox=-1; -static uint64_t candidate_since,next_border_probe; +static unsigned letterbox_mode, settling; +static int pending_letterbox = -1; +static uint64_t candidate_since, next_border_probe; static bool color_valid; static Continuity continuity; -static uint64_t sample_at,calibration_until; +static uint64_t sample_at, calibration_until; static unsigned sample_sequence; -#define CALIBRATION_ACTIVE (calibration_until>milliseconds()) +#define CALIBRATION_ACTIVE (calibration_until > milliseconds()) static unsigned led_count; static LedBlend *led_blends; static bool interpolation_enabled; static RetryState retries; static unsigned capture_recoveries; -static uint64_t recovery_started,last_recovery_ms,last_output_at; -static uint64_t milliseconds(void){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (uint64_t)t.tv_sec*1000+t.tv_nsec/1000000;} +static uint64_t recovery_started, last_recovery_ms, last_output_at; +static uint64_t milliseconds(void) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000; +} static char last_error[256]; -static volatile sig_atomic_t revoked,quitting; -static int guard_pipe=-1; -static pid_t guard_pid=-1; +static volatile sig_atomic_t revoked, quitting; +static int guard_pipe = -1; +static pid_t guard_pid = -1; #define DEV_MODE "/var/luna/preferences/vt_devmode" /* LG's developer gate is enabled only during initialization. A separate * guardian removes our own marker even if this process aborts or is killed. * An existing marker is preserved, as are files replaced by another owner. */ -static bool enter_developer_mode(void){ - int marker=open(DEV_MODE,O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC,0600); - if(marker<0)return errno==EEXIST; - struct stat owned; - if(fstat(marker,&owned)!=0){close(marker);unlink(DEV_MODE);return false;} - close(marker);int p[2]; - if(pipe(p)!=0){unlink(DEV_MODE);return false;} - pid_t child=fork(); - if(child<0){close(p[0]);close(p[1]);unlink(DEV_MODE);return false;} - if(child==0){ - close(p[1]);struct pollfd fd={p[0],POLLIN|POLLHUP,0}; - /* Bounded cleanup also covers an indefinitely blocked initialization. */ - poll(&fd,1,10000);close(p[0]);struct stat current; - int rc=0; - if(lstat(DEV_MODE,¤t)==0 && current.st_dev==owned.st_dev && current.st_ino==owned.st_ino)rc=unlink(DEV_MODE); - _exit(rc==0?0:1); - } - close(p[0]);guard_pipe=p[1];guard_pid=child;return true; +static bool enter_developer_mode(void) { + int marker = open(DEV_MODE, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600); + if (marker < 0) + return errno == EEXIST; + struct stat owned; + if (fstat(marker, &owned) != 0) { + close(marker); + unlink(DEV_MODE); + return false; + } + close(marker); + int p[2]; + if (pipe(p) != 0) { + unlink(DEV_MODE); + return false; + } + pid_t child = fork(); + if (child < 0) { + close(p[0]); + close(p[1]); + unlink(DEV_MODE); + return false; + } + if (child == 0) { + close(p[1]); + struct pollfd fd = {p[0], POLLIN | POLLHUP, 0}; + /* Bounded cleanup also covers an indefinitely blocked initialization. */ + poll(&fd, 1, 10000); + close(p[0]); + struct stat current; + int rc = 0; + if (lstat(DEV_MODE, ¤t) == 0 && current.st_dev == owned.st_dev && + current.st_ino == owned.st_ino) + rc = unlink(DEV_MODE); + _exit(rc == 0 ? 0 : 1); + } + close(p[0]); + guard_pipe = p[1]; + guard_pid = child; + return true; } -static bool leave_developer_mode(void){ - if(guard_pipe<0)return true; - close(guard_pipe);guard_pipe=-1;int status; - while(waitpid(guard_pid,&status,0)<0){if(errno!=EINTR)return false;} - guard_pid=-1;return WIFEXITED(status)&&WEXITSTATUS(status)==0; +static bool leave_developer_mode(void) { + if (guard_pipe < 0) + return true; + close(guard_pipe); + guard_pipe = -1; + int status; + while (waitpid(guard_pid, &status, 0) < 0) { + if (errno != EINTR) + return false; + } + guard_pid = -1; + return WIFEXITED(status) && WEXITSTATUS(status) == 0; } -static void quit_signal(int s){(void)s;quitting=1;} -static bool policy(void){revoked=1;return true;} -static void set_error(const char *s){snprintf(last_error,sizeof(last_error),"%s",s);fprintf(stderr,"%s\n",s);} -static int send_line(const char *line){ - size_t left=strlen(line);const char *p=line; - while(left){ssize_t n=send(output,p,left,MSG_NOSIGNAL);if(n<=0)return -1;p+=n;left-=n;} - return 0; +static void quit_signal(int s) { + (void)s; + quitting = 1; } -static bool receiver_ack(void){ - char answer[4096];size_t used=0;bool complete=false; - while(used+1=0){if(clear_output && frames){char line[80];snprintf(line,sizeof(line),"{\"command\":\"clear\",\"priority\":%d}\n",PRIORITY);send_line(line);}close(output);output=-1;} - if(capture){vt_finish(capture);capture=NULL;} +static void release_capture(bool clear_output) { + layout_refresh_reset(milliseconds() + 5000); + free(led_blends); + led_blends = NULL; + led_count = 0; + interpolation_enabled = false; + color_valid = false; + memset(rgb, 0, sizeof(rgb)); + memset(sent_rgb, 0, sizeof(sent_rgb)); + if (output >= 0) { + if (clear_output && frames) { + char line[80]; + snprintf(line, sizeof(line), "{\"command\":\"clear\",\"priority\":%d}\n", PRIORITY); + send_line(line); + } + close(output); + output = -1; + } + if (capture) { + vt_finish(capture); + capture = NULL; + } +} +static void stop_capture(void) { + release_capture(true); +} +static bool respond(void *h, void *m, const char *s) { + if (!h || !m) + return true; + LSError e; + error_init(&e); + bool ok = reply(h, m, s, &e); + error_free(&e); + return ok; } -static void stop_capture(void){release_capture(true);} -static bool respond(void *h,void *m,const char *s){if(!h||!m)return true;LSError e;error_init(&e);bool ok=reply(h,m,s,&e);error_free(&e);return ok;} #include "handover.h" #include "layout_refresh.h" #define RECEIVER_LAYOUT_REVISION layout_updates #include "receiver_stream.h" #include "source_bridge.h" -static bool calibration_session(void *h,void *m,void *ctx){ - (void)ctx;const char *p=payload(m); - if(!strcmp(p,"{\"enabled\":true}")){ - calibration_until=milliseconds()+110000; - if(capture && vt_preset(capture,0)!=0){calibration_until=0;return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot select full-screen calibration capture\"}");} - capture_preset=letterbox_mode=0;settling=2;pending_letterbox=-1; - }else if(!strcmp(p,"{\"enabled\":false}"))calibration_until=0; - else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}"); - return respond(h,m,"{\"returnValue\":true}"); +#include "service_api.h" +#include "capture_loop.h" +static void *symbol(void *lib, const char *name) { + void *s = dlsym(lib, name); + if (!s) { + fprintf(stderr, "Missing symbol %s\n", name); + exit(2); + } + return s; } -static Calibration calibration_draft; -static char calibration_message[256]; -static bool report_calibration(void *h,void *m,void *ctx){ - if(!playback_json.lib)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"JSON-C is unavailable\"}"); - (void)ctx;void *root=playback_json.parse(payload(m)),*message=playback_field(&playback_json,root,"message"); - bool valid=message && playback_json.type(message)==6 && strlen(playback_json.string(message))=model_deadline){LSError e;error_init(&e);cancel_call(service_handle,model_token,&e);error_free(&e);model_busy=false;model_token=0;} - if(!tv_model[0] && !model_busy && now>=next_model_probe){ - LSError e;error_init(&e);model_busy=call_once(service_handle,"luna://com.webos.service.tv.systemproperty/getSystemInfo","{\"keys\":[\"modelName\"]}",model_reply,NULL,&model_token,&e);error_free(&e);model_deadline=now+3000;next_model_probe=now+60000; - } -} -static bool set_calibration(void *h,void *m,void *ctx){ - (void)ctx;Calibration next=calibration; - if(!playback_json.lib || !calibration_parse(&playback_json,payload(m),&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Select a valid profile; custom profiles need a model and 2-16 sorted rim points (position 0-4, region 0-15)\"}"); - if(!calibration_save(CALIBRATION_PREF,&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot save calibration profile\"}"); - calibration=next;calibration_generation++;layout_refresh_reset(milliseconds()); - return respond(h,m,"{\"returnValue\":true}"); -} -static bool start_failure(void *h,void *m,const char *message){ - retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds()); - char json[512];snprintf(json,sizeof(json),"{\"returnValue\":false,\"errorText\":\"%s\",\"retryScheduled\":%s,\"retryBlocked\":%s}",message,retries.due?"true":"false",retries.blocked?"true":"false"); - return respond(h,m,json); -} -static bool start(void *h,void *m,void *ctx){ - (void)ctx;const char *p=m?payload(m):NULL; - if(p && strcmp(p,"{}")!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Start expects an empty object; use settings to configure the receiver\"}"); - if(m)retry_reset(&retries); - if(piccap_owned)return respond(h,m,"{\"returnValue\":true,\"running\":true,\"activeSource\":\"PicCap\"}"); - if(capture)return respond(h,m,"{\"returnValue\":true,\"running\":true}"); - frames=0;revoked=0;last_error[0]=0;lg_diagnostic[0]=0; - output=target_connect(&target); - if(output<0){stop_capture();set_error("Cannot connect to configured receiver IP and JSON port");return start_failure(h,m,"Configured receiver is unavailable");} - if(!target_login()){stop_capture();set_error("Receiver token login failed; check the saved API token");return start_failure(h,m,"Receiver authentication failed");} - if(!read_layout()){stop_capture();set_error("Cannot read receiver LED layout; check JSON port and API authorization");return start_failure(h,m,"Receiver layout unavailable; check API authorization");} - if(!enter_developer_mode()){stop_capture();set_error("Developer initialization needs root. Elevate this own service through Homebrew Channel.");return start_failure(h,m,"Root access is required for temporary LG developer initialization");} - capture=vt_init(policy); - if(!leave_developer_mode()){stop_capture();retries.blocked=true;set_error("Developer marker cleanup failed; manual intervention required");return start_failure(h,m,"Developer marker cleanup failed");} - if(!capture){stop_capture();set_error("LG refused histogram initialization");return start_failure(h,m,"LG refused histogram initialization. Another capture source may hold VT_CAPTURE.");} - if(vt_preset(capture,0)!=0){stop_capture();set_error("LG histogram preset initialization failed");return start_failure(h,m,"Histogram preset failed");} - capture_preset=letterbox_mode=settling=0;pending_letterbox=-1;next_border_probe=0; - retries.due=0;retries.attempts=0;retries.quick_retries=0; - if(recovery_started){last_recovery_ms=milliseconds()-recovery_started;recovery_started=0;} - return respond(h,m,"{\"returnValue\":true,\"running\":true}"); -} -static bool stop(void *h,void *m,void *ctx){ - (void)ctx;calibration_until=0;retry_pause(&retries); - bool bridging=bridge_phase!=0;stream_close(milliseconds());bridge_phase=0; - memset(&continuity,0,sizeof(continuity));stop_capture(); - if(bridging){ - output=target_connect(&target); - if(output>=0){if(target_login())send_line("{\"command\":\"clear\",\"priority\":149}\n");close(output);output=-1;} - } - return respond(h,m,"{\"returnValue\":true,\"running\":false}"); -} -static bool autostart(void *h,void *m,void *ctx){ - (void)ctx;const char *p=m?payload(m):NULL;bool enabled; - if(p && strcmp(p,"{\"enabled\":true}")==0)enabled=true; - else if(p && strcmp(p,"{\"enabled\":false}")==0)enabled=false; - else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}"); - if(geteuid()!=0)return respond(h,m,"{\"returnValue\":false,\"rootRequired\":true,\"errorText\":\"Root access is required to change autostart. Select Enable Root Access in Overview.\"}"); - if(!set_boot(enabled))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Cannot update the HyperDRM boot hook. Check the installed boot script and whether another file occupies the hook path.\"}"); - if(enabled)retry_reset(&retries); - retry_update(&retries,enabled,capture!=NULL,milliseconds()); - return respond(h,m,enabled?"{\"returnValue\":true,\"autostart\":true}":"{\"returnValue\":true,\"autostart\":false}"); -} -static bool restart_service(void *h,void *m,void *ctx){ - (void)ctx;if(strcmp(payload(m),"{}"))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Restart expects an empty object\"}"); - bool ok=respond(h,m,"{\"returnValue\":true,\"restarting\":true}");quitting=1;return ok; -} -static bool handover(void *h,void *m,void *ctx){ - (void)ctx;const char *p=payload(m);bool enabled; - if(strcmp(p,"{\"enabled\":true}")==0)enabled=true; - else if(strcmp(p,"{\"enabled\":false}")==0)enabled=false; - else return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}"); - if(!playback_json.lib || geteuid()!=0)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Root service and JSON-C are required\"}"); - if(enabled && !piccap_installed())return respond(h,m,"{\"returnValue\":false,\"errorText\":\"PicCap is not installed\"}"); - if(!handover_save(enabled))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Could not save PicCap handover preference\"}"); - handover_enabled=enabled;protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=unknown_since=0;next_media_probe=0; - return respond(h,m,"{\"returnValue\":true}"); -} -static bool settings(void *h,void *m,void *ctx){ - (void)ctx;TargetConfig next=target; - if(!playback_json.lib || !target_parse(&playback_json,payload(m),&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Enter a valid IPv4/IPv6 address, integer port (1-65535), receiver and optional API token\"}"); - if(!target_save(TARGET_PREF,&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Could not save receiver settings\"}"); - bool active=capture || piccap_owned || retries.due; - stop_capture();stream_close(milliseconds());bridge_phase=0;memset(&continuity,0,sizeof(continuity));test_close();connection_test.stage=TEST_IDLE;target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;clear_since=unknown_since=0; - if(active && !retries.paused && !retries.blocked)retries.due=milliseconds(); - return respond(h,m,"{\"returnValue\":true}"); -} -static bool test_connection(void *h,void *m,void *ctx){ - (void)ctx; - if(strcmp(payload(m),"{}") || !playback_json.lib)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Test requires an empty object and JSON-C\"}"); - if(!test_begin(&target,milliseconds()))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"A connection test is already running\"}"); - return respond(h,m,"{\"returnValue\":true,\"testing\":true}"); -} -static bool status(void *h,void *m,void *ctx){ - (void)ctx;char json[8192],safe[512],endpoint[80];size_t n=0; - snprintf(endpoint,sizeof(endpoint),strchr(target.host,':')?"[%s]:%u":"%s:%u",target.host,target.port); - for(size_t i=0;last_error[i] && n+2=32)safe[n++]=c;}safe[n]=0; - uint64_t now=milliseconds(); - char report[512];size_t k=0;for(size_t i=0;calibration_message[i] && k+2=32)report[k++]=c;}report[k]=0; - int used=snprintf(json,sizeof(json),"{\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\",\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[",calibration_key(calibration.kind),calibration_key(calibration_effective()),tv_model,calibration.model); - for(unsigned i=0;inow?handover_cooldown-now:0)); - used+=snprintf(json+used,sizeof(json)-used,"\"calibrationMessage\":\"%s\",",report); - used+=snprintf(json+used,sizeof(json)-used,"\"calibrationDraft\":{\"model\":\"%s\",\"points\":[",calibration_draft.model); - for(unsigned i=0;inow?retries.due-now:0),capture_source==SOURCE_PICCAP_STARTING?"Starting PicCap":capture_source==SOURCE_PICCAP_STOPPING?"Stopping PicCap":piccap_owned?"PicCap":capture?"Histogram":"Stopped",piccap_error); - used+=snprintf(json+used,sizeof(json)-used,"\"version\":\"0.11.0\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,capture_recoveries,(unsigned long long)last_recovery_ms,endpoint,target.host,target.port,target.receiver,target.token[0]?"true":"false",target_piccap_compatible(&target)?"true":"false",PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe); - for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s[%u,%u,%u]",i?",":"",rgb[i][0],rgb[i][1],rgb[i][2]); - used+=snprintf(json+used,sizeof(json)-used,"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",led_count,led_count?"true":"false"); - for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s%u",i?",":"",black_percent[i]); - snprintf(json+used,sizeof(json)-used,"],\"preset\":%u,\"saturationGain\":1.0,\"brightnessGain\":1.0,\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s,\"retryScheduled\":%s,\"retryPaused\":%s,\"retryBlocked\":%s,\"retryAttempts\":%u,\"piccapInstalled\":%s,\"piccapHandover\":%s,\"piccapConnected\":%s,\"activeSource\":\"%s\",\"playbackProtection\":\"%s\",\"letterboxDetected\":%s}",capture_preset,interpolation_enabled?"true":"false",boot_enabled()?"true":"false",retries.due?"true":"false",retries.paused?"true":"false",retries.blocked?"true":"false",retries.attempts,piccap_installed()?"true":"false",handover_enabled?"true":"false",piccap_connected?"true":"false",piccap_owned?"PicCap":"HyperDRM",protection==PLAYBACK_PROTECTED?"protected":protection==PLAYBACK_CLEAR?"non-secure decoder":"unknown",capture&&letterbox_mode?"true":"false");return respond(h,m,json); -} -static int tick(void *unused){ - (void)unused; - uint64_t now=milliseconds(); - model_tick(now);test_tick(&playback_json,now);layout_refresh_tick(&playback_json,now,capture || capture_source==SOURCE_PICCAP); - stream_tick(&playback_json,now,handover_enabled && !retries.paused && !retries.blocked); - bool owned=handover_tick();bridge_tick(now); - if(owned)return 1; - if(!capture){ - retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds()); - if(retry_take(&retries,milliseconds())){ - start(NULL,NULL,NULL); - if(!capture)retry_failure(&retries,milliseconds()); - } - return 1; - } - if(revoked){set_error("LG revoked capture; recovering after source transition");capture_recoveries++;recovery_started=milliseconds();release_capture(false);retry_source_transition(&retries,milliseconds());return 1;} - if(settling){settling--;return 1;} - if(!CALIBRATION_ACTIVE && letterbox_mode && capture_preset==1 && milliseconds()>=next_border_probe){ - if(vt_preset(capture,0)!=0){set_error("Border probe preset failed");stop_capture();return 1;} - capture_preset=0;settling=2;return 1; - } - Histogram hist[16]={0};int rc=vt_get(capture,hist); - bool populated=!rc && histogram_populated(hist); - if(!populated){ - unsigned char held[16][3]; - color_valid=false; - if(!continuity_frame(&continuity,false,rgb,now,held)){set_error("Histogram data unavailable beyond hold window; recovering capture");release_capture(false);retry_source_transition(&retries,now);return 1;} - char *line=region_command(held,led_blends,led_count,(unsigned)(1500-(now-continuity.missing_at))); - if(!line || send_line(line)){free(line);set_error("Receiver connection lost while holding colors");stop_capture();return 1;}free(line); - if(!receiver_ack()){set_error("Receiver did not acknowledge held colors");stop_capture();return 1;} - last_output_at=now;return 1; - } - sample_at=now;sample_sequence++; - unsigned aggregate[3]={0}; - for(int i=0;i<16;i++){ - black_percent[i]=100; - for(int c=0;c<3;c++){ - uint64_t total=0,black=0; - for(int b=0;b<256;b++){total+=hist[i].bins[c][b];if(b<=12)black+=hist[i].bins[c][b];} - unsigned fraction=total?(unsigned)(100*black/total):0; - if(fraction=2000)letterbox_mode=(unsigned)candidate; - if(letterbox_mode){ - if(vt_preset(capture,1)!=0){set_error("Letterbox preset failed");stop_capture();return 1;} - capture_preset=1;settling=2;next_border_probe=now+2000;return 1; - } - } - for(int c=0;c<3;c++)sent_rgb[c]=(unsigned char)(aggregate[c]/16); - unsigned char boosted_mean[3];boost_color(sent_rgb,boosted_mean);memcpy(sent_rgb,boosted_mean,3); - unsigned char mapped_rgb[16][3],stable_rgb[16][3];calibration_colors(rgb,mapped_rgb); - continuity_frame(&continuity,true,mapped_rgb,now,stable_rgb); - interpolation_enabled=led_count && calibration_effective()!=CAL_AVERAGE; - char *line=region_command(stable_rgb,led_blends,led_count,1500); - if(!line){set_error("Color buffer allocation failed");stop_capture();return 1;} - int failed=send_line(line);free(line); - if(failed){set_error("Receiver connection lost");stop_capture();return 1;} - if(!receiver_ack()){set_error("Receiver did not acknowledge color data");stop_capture();return 1;} - last_error[0]=0; - color_valid=true;last_output_at=milliseconds();frames++;return 1; -} -static void *symbol(void *lib,const char *name){void *s=dlsym(lib,name);if(!s){fprintf(stderr,"Missing symbol %s\n",name);exit(2);}return s;} -int main(void){ - signal(SIGTERM,quit_signal);signal(SIGINT,quit_signal); - if(boot_enabled() && !set_boot(true))fprintf(stderr,"Own legacy boot hook migration failed\n"); - void *vt=dlopen("libvtcapture.so.1",RTLD_NOW),*ls=dlopen("libluna-service2.so.3",RTLD_NOW),*glib=dlopen("libglib-2.0.so.0",RTLD_NOW); - if(!vt||!ls||!glib){fprintf(stderr,"Missing LG library: %s\n",dlerror());return 2;} - vt_init=symbol(vt,"vtCapture_initHistogram");vt_preset=symbol(vt,"vtCapture_setPresetHistogram");vt_get=symbol(vt,"vtCapture_getHistogram");vt_finish=symbol(vt,"vtCapture_finalizeHistogram"); - call_once=symbol(ls,"LSCallOneReply"); - cancel_call=symbol(ls,"LSCallCancel"); - if(playback_json_init(&playback_json)){handover_load();target_load(&playback_json,TARGET_PREF,&target);calibration_load(&playback_json,CALIBRATION_PREF);} - reply=symbol(ls,"LSMessageReply");payload=symbol(ls,"LSMessageGetPayload");error_init=symbol(ls,"LSErrorInit");error_free=symbol(ls,"LSErrorFree"); - bool (*reg)(const char*,void**,LSError*)=symbol(ls,"LSRegister"); - bool (*category)(void*,const char*,Method*,void*,void*,LSError*)=symbol(ls,"LSRegisterCategory"); - bool (*attach)(void*,void*,LSError*)=symbol(ls,"LSGmainAttach"); - bool (*unreg)(void*,LSError*)=symbol(ls,"LSUnregister"); - void *(*loop_new)(void*,int)=symbol(glib,"g_main_loop_new"); - int (*iterate)(void*,int)=symbol(glib,"g_main_context_iteration"); - unsigned (*timer)(unsigned,int(*)(void*),void*)=symbol(glib,"g_timeout_add"); - void (*loop_unref)(void*)=symbol(glib,"g_main_loop_unref"); - void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e); - Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"restartService",restart_service,0},{"handover",handover,0},{"settings",settings,0},{"testConnection",test_connection,0},{"calibration",set_calibration,0},{"calibrationSession",calibration_session,0},{"calibrationDraft",draft_calibration,0},{"calibrationResult",report_calibration,0},{NULL,NULL,0}}; - if(!reg(SERVICE,&handle,&e)||!category(handle,"/",methods,NULL,NULL,&e)||!attach(handle,loop,&e)){fprintf(stderr,"Luna registration failed: %s\n",e.message?e.message:"unknown");error_free(&e);return 3;} - service_handle=handle; - if(!boot_enabled())retry_pause(&retries); - timer(100,tick,NULL); - while(!quitting){iterate(NULL,0);usleep(10000);} - stream_close(milliseconds());test_close();stop_capture(); - if(piccap_owned){ - handover_enabled=false;retry_pause(&retries); - uint64_t deadline=milliseconds()+4000; - while(piccap_owned && milliseconds()= 0) { + if (target_login()) + send_line("{\"command\":\"clear\",\"priority\":149}\n"); + close(output); + output = -1; + } + } + return respond(h, m, "{\"returnValue\":true,\"running\":false}"); +} +static bool autostart(void *h, void *m, void *ctx) { + (void)ctx; + const char *p = m ? payload(m) : NULL; + bool enabled; + if (p && strcmp(p, "{\"enabled\":true}") == 0) + enabled = true; + else if (p && strcmp(p, "{\"enabled\":false}") == 0) + enabled = false; + else + return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}"); + if (geteuid() != 0) + return respond( + h, m, + "{\"returnValue\":false,\"rootRequired\":true,\"errorText\":\"Root access is required " + "to change autostart. Select Enable Root Access in Overview.\"}"); + if (!set_boot(enabled)) + return respond( + h, m, + "{\"returnValue\":false,\"errorText\":\"Cannot update the HyperDRM boot hook. Check " + "the installed boot script and whether another file occupies the hook path.\"}"); + if (enabled) + retry_reset(&retries); + retry_update(&retries, enabled, capture != NULL, milliseconds()); + return respond(h, m, + enabled ? "{\"returnValue\":true,\"autostart\":true}" + : "{\"returnValue\":true,\"autostart\":false}"); +} +static bool restart_service(void *h, void *m, void *ctx) { + (void)ctx; + if (strcmp(payload(m), "{}")) + return respond(h, m, + "{\"returnValue\":false,\"errorText\":\"Restart expects an empty object\"}"); + bool ok = respond(h, m, "{\"returnValue\":true,\"restarting\":true}"); + quitting = 1; + return ok; +} +static bool handover(void *h, void *m, void *ctx) { + (void)ctx; + const char *p = payload(m); + bool enabled; + if (strcmp(p, "{\"enabled\":true}") == 0) + enabled = true; + else if (strcmp(p, "{\"enabled\":false}") == 0) + enabled = false; + else + return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}"); + if (!playback_json.lib || geteuid() != 0) + return respond( + h, m, "{\"returnValue\":false,\"errorText\":\"Root service and JSON-C are required\"}"); + if (enabled && !piccap_installed()) + return respond(h, m, "{\"returnValue\":false,\"errorText\":\"PicCap is not installed\"}"); + if (!handover_save(enabled)) + return respond( + h, m, + "{\"returnValue\":false,\"errorText\":\"Could not save PicCap handover preference\"}"); + handover_enabled = enabled; + protection = PLAYBACK_UNKNOWN; + clear_samples = 0; + clear_since = unknown_since = 0; + next_media_probe = 0; + return respond(h, m, "{\"returnValue\":true}"); +} +static bool settings(void *h, void *m, void *ctx) { + (void)ctx; + TargetConfig next = target; + if (!playback_json.lib || !target_parse(&playback_json, payload(m), &next)) + return respond(h, m, + "{\"returnValue\":false,\"errorText\":\"Enter a valid IPv4/IPv6 address, " + "integer port (1-65535), receiver and optional API token\"}"); + if (!target_save(TARGET_PREF, &next)) + return respond( + h, m, "{\"returnValue\":false,\"errorText\":\"Could not save receiver settings\"}"); + bool active = capture || piccap_owned || retries.due; + stop_capture(); + stream_close(milliseconds()); + bridge_phase = BRIDGE_IDLE; + memset(&continuity, 0, sizeof(continuity)); + test_close(); + connection_test.stage = TEST_IDLE; + target = next; + last_error[0] = 0; + protection = PLAYBACK_UNKNOWN; + clear_samples = 0; + clear_since = unknown_since = 0; + if (active && !retries.paused && !retries.blocked) + retries.due = milliseconds(); + return respond(h, m, "{\"returnValue\":true}"); +} +static bool test_connection(void *h, void *m, void *ctx) { + (void)ctx; + if (strcmp(payload(m), "{}") || !playback_json.lib) + return respond( + h, m, + "{\"returnValue\":false,\"errorText\":\"Test requires an empty object and JSON-C\"}"); + if (!test_begin(&target, milliseconds())) + return respond( + h, m, "{\"returnValue\":false,\"errorText\":\"A connection test is already running\"}"); + return respond(h, m, "{\"returnValue\":true,\"testing\":true}"); +} +#include "service_status.h" diff --git a/native/service_calibration.h b/native/service_calibration.h new file mode 100644 index 0000000..989c17f --- /dev/null +++ b/native/service_calibration.h @@ -0,0 +1,107 @@ +/* Calibration session, proposals, saved profiles and model discovery. */ +static bool calibration_session(void *h, void *m, void *ctx) { + (void)ctx; + const char *p = payload(m); + if (!strcmp(p, "{\"enabled\":true}")) { + calibration_until = milliseconds() + 110000; + if (capture && vt_preset(capture, 0) != 0) { + calibration_until = 0; + return respond(h, m, + "{\"returnValue\":false,\"errorText\":\"Cannot select full-screen " + "calibration capture\"}"); + } + capture_preset = letterbox_mode = 0; + settling = 2; + pending_letterbox = -1; + } else if (!strcmp(p, "{\"enabled\":false}")) + calibration_until = 0; + else + return respond(h, m, "{\"returnValue\":false,\"errorText\":\"Expected enabled boolean\"}"); + return respond(h, m, "{\"returnValue\":true}"); +} +static Calibration calibration_draft; +static char calibration_message[256]; +static bool report_calibration(void *h, void *m, void *ctx) { + if (!playback_json.lib) + return respond(h, m, "{\"returnValue\":false,\"errorText\":\"JSON-C is unavailable\"}"); + (void)ctx; + void *root = playback_json.parse(payload(m)), + *message = playback_field(&playback_json, root, "message"); + bool valid = message && playback_json.type(message) == 6 && + strlen(playback_json.string(message)) < sizeof(calibration_message); + if (valid) + snprintf(calibration_message, sizeof(calibration_message), "%s", + playback_json.string(message)); + if (root) + playback_json.put(root); + return respond( + h, m, + valid ? "{\"returnValue\":true}" + : "{\"returnValue\":false,\"errorText\":\"Invalid calibration result message\"}"); +} +static bool draft_calibration(void *h, void *m, void *ctx) { + (void)ctx; + Calibration next = {0}; + if (!playback_json.lib || !calibration_parse(&playback_json, payload(m), &next) || + next.kind != CAL_CUSTOM || next.count < 4 || strcmp(next.model, tv_model)) + return respond(h, m, + "{\"returnValue\":false,\"errorText\":\"Invalid measured model profile\"}"); + calibration_draft = next; + return respond(h, m, "{\"returnValue\":true}"); +} +static bool model_busy; +static unsigned long model_token; +static uint64_t model_deadline, next_model_probe; +static bool model_reply(void *h, void *m, void *ctx) { + (void)h; + (void)ctx; + model_busy = false; + model_token = 0; + void *root = playback_json.parse(payload(m)), + *model = playback_field(&playback_json, root, "modelName"); + if (playback_true(&playback_json, root, "returnValue") && model && + playback_json.type(model) == 6 && calibration_model_valid(playback_json.string(model))) { + snprintf(tv_model, sizeof(tv_model), "%s", playback_json.string(model)); + calibration_generation++; + layout_refresh_reset(milliseconds()); + } + if (root) + playback_json.put(root); + return true; +} +static void model_tick(uint64_t now) { + if (model_busy && now >= model_deadline) { + LSError e; + error_init(&e); + cancel_call(service_handle, model_token, &e); + error_free(&e); + model_busy = false; + model_token = 0; + } + if (!tv_model[0] && !model_busy && now >= next_model_probe) { + LSError e; + error_init(&e); + model_busy = + call_once(service_handle, "luna://com.webos.service.tv.systemproperty/getSystemInfo", + "{\"keys\":[\"modelName\"]}", model_reply, NULL, &model_token, &e); + error_free(&e); + model_deadline = now + 3000; + next_model_probe = now + 60000; + } +} +static bool set_calibration(void *h, void *m, void *ctx) { + (void)ctx; + Calibration next = calibration; + if (!playback_json.lib || !calibration_parse(&playback_json, payload(m), &next)) + return respond( + h, m, + "{\"returnValue\":false,\"errorText\":\"Select a valid profile; custom profiles need a " + "model and 2-16 sorted rim points (position 0-4, region 0-15)\"}"); + if (!calibration_save(CALIBRATION_PREF, &next)) + return respond(h, m, + "{\"returnValue\":false,\"errorText\":\"Cannot save calibration profile\"}"); + calibration = next; + calibration_generation++; + layout_refresh_reset(milliseconds()); + return respond(h, m, "{\"returnValue\":true}"); +} diff --git a/native/service_status.h b/native/service_status.h new file mode 100644 index 0000000..b462a3b --- /dev/null +++ b/native/service_status.h @@ -0,0 +1,111 @@ +/* Read-only runtime status serialization. */ +static bool status(void *h, void *m, void *ctx) { + (void)ctx; + char json[8192], safe[512], endpoint[80]; + size_t n = 0; + snprintf(endpoint, sizeof(endpoint), strchr(target.host, ':') ? "[%s]:%u" : "%s:%u", + target.host, target.port); + for (size_t i = 0; last_error[i] && n + 2 < sizeof(safe); i++) { + char c = last_error[i]; + if (c == '"' || c == '\\') + safe[n++] = '\\'; + if ((unsigned char)c >= 32) + safe[n++] = c; + } + safe[n] = 0; + uint64_t now = milliseconds(); + char report[512]; + size_t k = 0; + for (size_t i = 0; calibration_message[i] && k + 2 < sizeof(report); i++) { + char c = calibration_message[i]; + if (c == '"' || c == '\\') + report[k++] = '\\'; + if ((unsigned char)c >= 32) + report[k++] = c; + } + report[k] = 0; + int used = snprintf(json, sizeof(json), + "{\"calibrationProfile\":\"%s\",\"effectiveCalibration\":\"%s\"," + "\"tvModel\":\"%s\",\"customModel\":\"%s\",\"customPoints\":[", + calibration_key(calibration.kind), calibration_key(calibration_effective()), + tv_model, calibration.model); + for (unsigned i = 0; i < calibration.count; i++) + used += + snprintf(json + used, sizeof(json) - used, "%s{\"position\":%.12g,\"region\":%u}", + i ? "," : "", calibration.points[i].position, calibration.points[i].region); + used += snprintf(json + used, sizeof(json) - used, + "],\"layoutUpdates\":%u,\"lastLayoutCheckAgeMs\":%lld,\"layoutError\":\"%s\"," + "\"piccapFallbacks\":%u,\"piccapCooldownMs\":%llu,", + layout_updates, last_layout_check ? (long long)(now - last_layout_check) : -1, + layout_error, piccap_fallbacks, + (unsigned long long)(handover_cooldown > now ? handover_cooldown - now : 0)); + used += snprintf(json + used, sizeof(json) - used, "\"calibrationMessage\":\"%s\",", report); + used += + snprintf(json + used, sizeof(json) - used, + "\"calibrationDraft\":{\"model\":\"%s\",\"points\":[", calibration_draft.model); + for (unsigned i = 0; i < calibration_draft.count; i++) + used += snprintf(json + used, sizeof(json) - used, "%s{\"position\":%.12g,\"region\":%u}", + i ? "," : "", calibration_draft.points[i].position, + calibration_draft.points[i].region); + used += snprintf(json + used, sizeof(json) - used, "]},\"receiverStreamConnected\":%s,", + stream.stage == STREAM_SUBSCRIBED ? "true" : "false"); + used += snprintf(json + used, sizeof(json) - used, + "\"sampleSequence\":%u,\"sampleAgeMs\":%lld,\"holdingColors\":%s," + "\"sourceFade\":%s,\"calibrating\":%s,", + sample_sequence, sample_at ? (long long)(now - sample_at) : -1, + continuity.holding ? "true" : "false", + (continuity.fading || bridge_phase == BRIDGE_FADING) ? "true" : "false", + CALIBRATION_ACTIVE ? "true" : "false"); + used += snprintf(json + used, sizeof(json) - used, + "\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\"," + "\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%" + "lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",", + target.piccap_port, test_stage_name(), connection_test.leds, + (unsigned long long)connection_test.elapsed, connection_test.error, + last_output_at ? (long long)(now - last_output_at) : -1, + (unsigned long long)(retries.due > now ? retries.due - now : 0), + capture_source == SOURCE_PICCAP_STARTING ? "Starting PicCap" + : capture_source == SOURCE_PICCAP_STOPPING ? "Stopping PicCap" + : piccap_owned ? "PicCap" + : capture ? "Histogram" + : "Stopped", + piccap_error); + used += snprintf(json + used, sizeof(json) - used, + "\"version\":\"" HYPERDRM_VERSION + "\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u," + "\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u," + "\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s," + "\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false," + "\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[", + (capture || capture_source == SOURCE_PICCAP) ? "true" : "false", + color_valid ? "true" : "false", frames, capture_recoveries, + (unsigned long long)last_recovery_ms, endpoint, target.host, target.port, + target.receiver, target.token[0] ? "true" : "false", + target_piccap_compatible(&target) ? "true" : "false", PRIORITY, + (unsigned)geteuid(), sent_rgb[0], sent_rgb[1], sent_rgb[2], safe); + for (int i = 0; i < 16; i++) + used += snprintf(json + used, sizeof(json) - used, "%s[%u,%u,%u]", i ? "," : "", rgb[i][0], + rgb[i][1], rgb[i][2]); + used += snprintf(json + used, sizeof(json) - used, + "],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[", led_count, + led_count ? "true" : "false"); + for (int i = 0; i < 16; i++) + used += snprintf(json + used, sizeof(json) - used, "%s%u", i ? "," : "", black_percent[i]); + snprintf(json + used, sizeof(json) - used, + "],\"preset\":%u,\"saturationGain\":1.0,\"brightnessGain\":1.0," + "\"spatialInterpolation\":%s,\"blackBarDetection\":true,\"autostart\":%s," + "\"retryScheduled\":%s,\"retryPaused\":%s,\"retryBlocked\":%s,\"retryAttempts\":%u," + "\"piccapInstalled\":%s,\"piccapHandover\":%s,\"piccapConnected\":%s,\"activeSource\":" + "\"%s\",\"playbackProtection\":\"%s\",\"letterboxDetected\":%s}", + capture_preset, interpolation_enabled ? "true" : "false", + boot_enabled() ? "true" : "false", retries.due ? "true" : "false", + retries.paused ? "true" : "false", retries.blocked ? "true" : "false", + retries.attempts, piccap_installed() ? "true" : "false", + handover_enabled ? "true" : "false", piccap_connected ? "true" : "false", + piccap_owned ? "PicCap" : "HyperDRM", + protection == PLAYBACK_PROTECTED ? "protected" + : protection == PLAYBACK_CLEAR ? "non-secure decoder" + : "unknown", + capture && letterbox_mode ? "true" : "false"); + return respond(h, m, json); +} diff --git a/native/source_bridge.h b/native/source_bridge.h index 775558b..0ef778a 100644 --- a/native/source_bridge.h +++ b/native/source_bridge.h @@ -1,47 +1,112 @@ #ifndef HYPERDRM_SOURCE_BRIDGE_H #define HYPERDRM_SOURCE_BRIDGE_H -static unsigned bridge_phase,bridge_sequence; -static uint64_t bridge_started,bridge_clear_at,bridge_fade_at,bridge_last_sent; -static unsigned char bridge_from[16][3],bridge_to[16][3]; +#include "runtime_states.h" +static BridgePhase bridge_phase; +static unsigned bridge_sequence; +static uint64_t bridge_started, bridge_clear_at, bridge_fade_at, bridge_last_sent; +static unsigned char bridge_from[16][3], bridge_to[16][3]; static CaptureSource bridge_source; -static char *region_command(const unsigned char colors[16][3],const LedBlend *mapping,unsigned count,unsigned duration){ - if(!count || !mapping)return NULL; - if(!strcmp(target.receiver,"HyperHDR"))return spatial_image_command_duration(colors,PRIORITY,duration); - size_t capacity=256+(size_t)count*12;char *line=malloc(capacity);if(!line)return NULL; - size_t n=(size_t)snprintf(line,capacity,"{\"command\":\"color\",\"priority\":%d,\"duration\":%u,\"origin\":\"HyperDRM\",\"color\":[",PRIORITY,duration); - for(unsigned i=0;i=3000){if(bridge_clear())bridge_phase=0;return;} - if(bridge_phase==1 && capture_source==SOURCE_PICCAP && piccap_connected && stream.stage==4){ - if(bridge_clear()){bridge_sequence=stream.sequence;bridge_clear_at=now;bridge_phase=2;} - }else if(bridge_phase==1 && now-bridge_last_sent>=100 && stream.stage==4){ - if(bridge_send(bridge_from))bridge_last_sent=now; - }else if(bridge_phase==2 && stream.valid && stream.sequence>bridge_sequence && stream.updated>=bridge_clear_at+150){ - memcpy(bridge_to,stream.colors,sizeof(bridge_to));bridge_fade_at=now;bridge_phase=3; - bool black=true;for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)if(bridge_to[r][c]>3)black=false; - if(black)bridge_phase=0; - } - if(bridge_phase==3 && now-bridge_last_sent>=100){ - if(now-bridge_fade_at>=500){if(bridge_clear())bridge_phase=0;} - else {unsigned char colors[16][3];continuity_mix(bridge_from,bridge_to,(unsigned)(now-bridge_fade_at),colors);if(bridge_send(colors))bridge_last_sent=now;} - } +static bool bridge_send(const unsigned char colors[16][3]) { + if (stream.stage != STREAM_SUBSCRIBED) + return false; + char *line = region_command(colors, stream.mapping, stream.count, 1500); + if (!line) + return false; + bool ok = stream_queue(line); + free(line); + return ok; +} +static void bridge_tick(uint64_t now) { + if (retries.paused || retries.blocked) { + if (bridge_phase) + bridge_clear(); + bridge_phase = BRIDGE_IDLE; + bridge_source = capture_source; + return; + } + if (capture_source != bridge_source) { + if (capture_source == SOURCE_PICCAP_STARTING) { + bridge_phase = continuity.available && now - continuity.valid_at < 2000 ? BRIDGE_HOLDING + : BRIDGE_IDLE; + bridge_started = now; + memcpy(bridge_from, continuity.last, sizeof(bridge_from)); + } else if (capture_source == SOURCE_PICCAP_STOPPING || + (bridge_source == SOURCE_PICCAP && capture_source == SOURCE_HISTOGRAM)) { + bridge_phase = BRIDGE_IDLE; + if (stream.valid && now - stream.updated < 1000) { + continuity_seed(&continuity, stream.colors, now); + bridge_send(stream.colors); + } + } + bridge_source = capture_source; + } + if (bridge_phase && now - bridge_started >= 3000) { + if (bridge_clear()) + bridge_phase = BRIDGE_IDLE; + return; + } + if (bridge_phase == BRIDGE_HOLDING && capture_source == SOURCE_PICCAP && piccap_connected && + stream.stage == STREAM_SUBSCRIBED) { + if (bridge_clear()) { + bridge_sequence = stream.sequence; + bridge_clear_at = now; + bridge_phase = BRIDGE_WAITING_FRAME; + } + } else if (bridge_phase == BRIDGE_HOLDING && now - bridge_last_sent >= 100 && + stream.stage == STREAM_SUBSCRIBED) { + if (bridge_send(bridge_from)) + bridge_last_sent = now; + } else if (bridge_phase == BRIDGE_WAITING_FRAME && stream.valid && + stream.sequence > bridge_sequence && stream.updated >= bridge_clear_at + 150) { + memcpy(bridge_to, stream.colors, sizeof(bridge_to)); + bridge_fade_at = now; + bridge_phase = BRIDGE_FADING; + bool black = true; + for (unsigned r = 0; r < 16; r++) + for (unsigned c = 0; c < 3; c++) + if (bridge_to[r][c] > 3) + black = false; + if (black) + bridge_phase = BRIDGE_IDLE; + } + if (bridge_phase == BRIDGE_FADING && now - bridge_last_sent >= 100) { + if (now - bridge_fade_at >= 500) { + if (bridge_clear()) + bridge_phase = BRIDGE_IDLE; + } else { + unsigned char colors[16][3]; + continuity_mix(bridge_from, bridge_to, (unsigned)(now - bridge_fade_at), colors); + if (bridge_send(colors)) + bridge_last_sent = now; + } + } } #endif diff --git a/native/spatial_image.h b/native/spatial_image.h index d7cae1a..201de3e 100644 --- a/native/spatial_image.h +++ b/native/spatial_image.h @@ -5,26 +5,48 @@ #include "layout.h" #include "interpolation.h" #include "color_boost.h" -enum {SPATIAL_WIDTH=160,SPATIAL_HEIGHT=90,SPATIAL_PIXELS=SPATIAL_WIDTH*SPATIAL_HEIGHT}; +enum { SPATIAL_WIDTH = 160, SPATIAL_HEIGHT = 90, SPATIAL_PIXELS = SPATIAL_WIDTH * SPATIAL_HEIGHT }; /* Synthetic color plane from histogram samples, never captured video pixels. * HyperHDR 22 treats JSON color as a solid color; image input retains space. * The receiver samples this plane using its configured LED rectangles. */ -static char *spatial_image_command_duration(const unsigned char regions[16][3],unsigned priority,unsigned duration){ - static LedBlend blends[SPATIAL_PIXELS];static unsigned generation; - if(generation!=calibration_generation){ - for(unsigned y=0;y>2];line[used++]=alphabet[((color[0]&3)<<4)|(color[1]>>4)]; - line[used++]=alphabet[((color[1]&15)<<2)|(color[2]>>6)];line[used++]=alphabet[color[2]&63]; - } - line[used++]='"';line[used++]='}';line[used++]='\n';line[used]=0;return line; +static char *spatial_image_command_duration(const unsigned char regions[16][3], unsigned priority, + unsigned duration) { + static LedBlend blends[SPATIAL_PIXELS]; + static unsigned generation; + if (generation != calibration_generation) { + for (unsigned y = 0; y < SPATIAL_HEIGHT; y++) + for (unsigned x = 0; x < SPATIAL_WIDTH; x++) + blends[y * SPATIAL_WIDTH + x] = + layout_blend((x + 0.5) / SPATIAL_WIDTH, (y + 0.5) / SPATIAL_HEIGHT); + generation = calibration_generation; + } + const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t capacity = SPATIAL_PIXELS * 4 + 256; + char *line = malloc(capacity); + if (!line) + return NULL; + size_t used = snprintf(line, capacity, + "{\"command\":\"image\",\"priority\":%u,\"duration\":%u,\"origin\":" + "\"HyperDRM\",\"imagewidth\":%u,\"imageheight\":%u,\"format\":\"rgb\"," + "\"name\":\"Histogram colors\",\"imagedata\":\"", + priority, duration, SPATIAL_WIDTH, SPATIAL_HEIGHT); + for (unsigned i = 0; i < SPATIAL_PIXELS; i++) { + unsigned char rgb[3], color[3]; + LedBlend b = blends[i]; + blend_color(regions[b.a], regions[b.b], b.weight, rgb); + boost_color(rgb, color); + line[used++] = alphabet[color[0] >> 2]; + line[used++] = alphabet[((color[0] & 3) << 4) | (color[1] >> 4)]; + line[used++] = alphabet[((color[1] & 15) << 2) | (color[2] >> 6)]; + line[used++] = alphabet[color[2] & 63]; + } + line[used++] = '"'; + line[used++] = '}'; + line[used++] = '\n'; + line[used] = 0; + return line; +} +static inline char *spatial_image_command(const unsigned char regions[16][3], unsigned priority) { + return spatial_image_command_duration(regions, priority, 1500); } -static inline char *spatial_image_command(const unsigned char regions[16][3],unsigned priority){return spatial_image_command_duration(regions,priority,1500);} #endif diff --git a/native/target.h b/native/target.h index 717de34..dffc20d 100644 --- a/native/target.h +++ b/native/target.h @@ -10,80 +10,194 @@ #include #include #include "playback.h" -#define TARGET_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json" -typedef struct {char host[46],receiver[16],token[257];unsigned port,piccap_port;} TargetConfig; -static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444,.piccap_port=19400}; -static inline bool target_piccap_local(const TargetConfig *t){return t->port==19444 && (!strcmp(t->host,"127.0.0.1") || !strcmp(t->host,"::1"));} -static inline bool target_piccap_compatible(const TargetConfig *t){unsigned char address[4];return !strcmp(t->host,"::1") || inet_pton(AF_INET,t->host,address)==1;} -static inline const char *target_piccap_host(const TargetConfig *t){return !strcmp(t->host,"::1")?"127.0.0.1":t->host;} -static bool target_valid(const TargetConfig *t){ - unsigned char address[16]; - if(inet_pton(AF_INET,t->host,address)!=1 && inet_pton(AF_INET6,t->host,address)!=1)return false; - if(!t->piccap_port || t->piccap_port>65535)return false; - if(!t->port || t->port>65535 || (strcmp(t->receiver,"Hyperion") && strcmp(t->receiver,"HyperHDR")))return false; - for(const unsigned char *p=(const unsigned char*)t->token;*p;p++) - if(!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='-'||*p=='_'||*p=='.'))return false; - return true; +#define TARGET_PREF \ + "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json" +typedef struct { + char host[46], receiver[16], token[257]; + unsigned port, piccap_port; +} TargetConfig; +static TargetConfig target = { + .host = "127.0.0.1", .receiver = "Hyperion", .port = 19444, .piccap_port = 19400}; +static inline bool target_piccap_local(const TargetConfig *t) { + return t->port == 19444 && (!strcmp(t->host, "127.0.0.1") || !strcmp(t->host, "::1")); } -static bool target_parse(PlaybackJson *j,const char *text,TargetConfig *out){ - void *root=j->parse(text);if(!root)return false; - TargetConfig next=*out;bool valid=j->type(root)==4; - const char *names[]={"host","receiver","token"};char *fields[]={next.host,next.receiver,next.token}; - size_t sizes[]={sizeof(next.host),sizeof(next.receiver),sizeof(next.token)}; - for(unsigned i=0;valid && i<3;i++){ - void *v=playback_field(j,root,names[i]); - if(!v){if(i<2)valid=false;continue;} - if(j->type(v)!=6){valid=false;break;} - const char *s=j->string(v);if(strlen(s)>=sizes[i]){valid=false;break;} - strcpy(fields[i],s); - } - void *port=playback_field(j,root,"port"); - if(!port || j->type(port)!=3)valid=false; - else {char *end=NULL;errno=0;long n=strtol(j->string(port),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.port=(unsigned)n;} - void *flat=NULL; - if(j->type(root)==4 && j->get(root,"piccapPort",&flat)){ - if(j->type(flat)!=3)valid=false; - else {char *end=NULL;errno=0;long n=strtol(j->string(flat),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.piccap_port=(unsigned)n;} - } - void *clear=playback_field(j,root,"clearToken"); - if(clear){if(j->type(clear)!=1)valid=false;else if(j->boolean(clear))next.token[0]=0;} - valid=valid && target_valid(&next);j->put(root);if(valid)*out=next;return valid; +static inline bool target_piccap_compatible(const TargetConfig *t) { + unsigned char address[4]; + return !strcmp(t->host, "::1") || inet_pton(AF_INET, t->host, address) == 1; } -static bool target_save(const char *path,const TargetConfig *t){ - if(!target_valid(t))return false; - char tmp[512],data[512];if(snprintf(tmp,sizeof(tmp),"%s.tmp",path)>=(int)sizeof(tmp))return false; - int n=snprintf(data,sizeof(data),"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",t->host,t->port,t->piccap_port,t->receiver,t->token); - int fd=open(tmp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false; - bool ok=write(fd,data,n)==n && fsync(fd)==0;if(close(fd)!=0)ok=false; - if(ok)ok=rename(tmp,path)==0; - if(!ok)unlink(tmp); - return ok; +static inline const char *target_piccap_host(const TargetConfig *t) { + return !strcmp(t->host, "::1") ? "127.0.0.1" : t->host; } -static bool target_load(PlaybackJson *j,const char *path,TargetConfig *t){ - int fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC);if(fd<0)return false; - char data[1024];ssize_t n=read(fd,data,sizeof(data)-1);close(fd);if(n<=0 || n==(ssize_t)sizeof(data)-1)return false; - data[n]=0;return target_parse(j,data,t); +static bool target_valid(const TargetConfig *t) { + unsigned char address[16]; + if (inet_pton(AF_INET, t->host, address) != 1 && inet_pton(AF_INET6, t->host, address) != 1) + return false; + if (!t->piccap_port || t->piccap_port > 65535) + return false; + if (!t->port || t->port > 65535 || + (strcmp(t->receiver, "Hyperion") && strcmp(t->receiver, "HyperHDR"))) + return false; + for (const unsigned char *p = (const unsigned char *)t->token; *p; p++) + if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || + *p == '-' || *p == '_' || *p == '.')) + return false; + return true; } -static int target_open(const TargetConfig *t){ - if(!target_valid(t))return -1; - struct sockaddr_storage storage={0};socklen_t length;int family; - struct sockaddr_in *v4=(void*)&storage;struct sockaddr_in6 *v6=(void*)&storage; - if(inet_pton(AF_INET,t->host,&v4->sin_addr)==1){family=AF_INET;v4->sin_family=family;v4->sin_port=htons(t->port);length=sizeof(*v4);} - else {family=AF_INET6;v6->sin6_family=family;v6->sin6_port=htons(t->port);length=sizeof(*v6);} - int fd=socket(family,SOCK_STREAM,0);if(fd<0)return -1; - int flags=fcntl(fd,F_GETFL,0);if(flags<0 || fcntl(fd,F_SETFL,flags|O_NONBLOCK)<0){close(fd);return -1;} - if(connect(fd,(void*)&storage,length)!=0){ - if(errno!=EINPROGRESS){close(fd);return -1;} - } - return fd; +static bool target_parse(PlaybackJson *j, const char *text, TargetConfig *out) { + void *root = j->parse(text); + if (!root) + return false; + TargetConfig next = *out; + bool valid = j->type(root) == 4; + const char *names[] = {"host", "receiver", "token"}; + char *fields[] = {next.host, next.receiver, next.token}; + size_t sizes[] = {sizeof(next.host), sizeof(next.receiver), sizeof(next.token)}; + for (unsigned i = 0; valid && i < 3; i++) { + void *v = playback_field(j, root, names[i]); + if (!v) { + if (i < 2) + valid = false; + continue; + } + if (j->type(v) != 6) { + valid = false; + break; + } + const char *s = j->string(v); + if (strlen(s) >= sizes[i]) { + valid = false; + break; + } + strcpy(fields[i], s); + } + void *port = playback_field(j, root, "port"); + if (!port || j->type(port) != 3) + valid = false; + else { + char *end = NULL; + errno = 0; + long n = strtol(j->string(port), &end, 10); + if (errno || !end || *end || n < 1 || n > 65535) + valid = false; + else + next.port = (unsigned)n; + } + void *flat = NULL; + if (j->type(root) == 4 && j->get(root, "piccapPort", &flat)) { + if (j->type(flat) != 3) + valid = false; + else { + char *end = NULL; + errno = 0; + long n = strtol(j->string(flat), &end, 10); + if (errno || !end || *end || n < 1 || n > 65535) + valid = false; + else + next.piccap_port = (unsigned)n; + } + } + void *clear = playback_field(j, root, "clearToken"); + if (clear) { + if (j->type(clear) != 1) + valid = false; + else if (j->boolean(clear)) + next.token[0] = 0; + } + valid = valid && target_valid(&next); + j->put(root); + if (valid) + *out = next; + return valid; } -static int target_connect(const TargetConfig *t){ - int fd=target_open(t);if(fd<0)return -1; - struct pollfd p={.fd=fd,.events=POLLOUT};int error=0;socklen_t size=sizeof(error); - if(poll(&p,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){close(fd);return -1;} - int flags=fcntl(fd,F_GETFL,0); - if(flags<0 || fcntl(fd,F_SETFL,flags & ~O_NONBLOCK)<0){close(fd);return -1;} - struct timeval timeout={1,0};setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout)); - return fd; +static bool target_save(const char *path, const TargetConfig *t) { + if (!target_valid(t)) + return false; + char tmp[512], data[512]; + if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp)) + return false; + int n = snprintf( + data, sizeof(data), + "{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n", + t->host, t->port, t->piccap_port, t->receiver, t->token); + int fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); + if (fd < 0) + return false; + bool ok = write(fd, data, n) == n && fsync(fd) == 0; + if (close(fd) != 0) + ok = false; + if (ok) + ok = rename(tmp, path) == 0; + if (!ok) + unlink(tmp); + return ok; +} +static bool target_load(PlaybackJson *j, const char *path, TargetConfig *t) { + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) + return false; + char data[1024]; + ssize_t n = read(fd, data, sizeof(data) - 1); + close(fd); + if (n <= 0 || n == (ssize_t)sizeof(data) - 1) + return false; + data[n] = 0; + return target_parse(j, data, t); +} +static int target_open(const TargetConfig *t) { + if (!target_valid(t)) + return -1; + struct sockaddr_storage storage = {0}; + socklen_t length; + int family; + struct sockaddr_in *v4 = (void *)&storage; + struct sockaddr_in6 *v6 = (void *)&storage; + if (inet_pton(AF_INET, t->host, &v4->sin_addr) == 1) { + family = AF_INET; + v4->sin_family = family; + v4->sin_port = htons(t->port); + length = sizeof(*v4); + } else { + family = AF_INET6; + v6->sin6_family = family; + v6->sin6_port = htons(t->port); + length = sizeof(*v6); + } + int fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) + return -1; + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { + close(fd); + return -1; + } + if (connect(fd, (void *)&storage, length) != 0) { + if (errno != EINPROGRESS) { + close(fd); + return -1; + } + } + return fd; +} +static int target_connect(const TargetConfig *t) { + int fd = target_open(t); + if (fd < 0) + return -1; + struct pollfd p = {.fd = fd, .events = POLLOUT}; + int error = 0; + socklen_t size = sizeof(error); + if (poll(&p, 1, 1000) <= 0 || getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 || + error) { + close(fd); + return -1; + } + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) { + close(fd); + return -1; + } + struct timeval timeout = {1, 0}; + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + return fd; } #endif diff --git a/native/test_receiver_stream.c b/native/test_receiver_stream.c index f0343ff..99ba967 100644 --- a/native/test_receiver_stream.c +++ b/native/test_receiver_stream.c @@ -5,35 +5,110 @@ #include "target.h" #include "layout.h" #include "receiver_stream.h" -static void fixture(PlaybackJson *json,bool modern,bool login){ - int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0); - struct sockaddr_in address={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}}; - assert(!bind(server,(void*)&address,sizeof(address)) && !listen(server,1));socklen_t size=sizeof(address);assert(!getsockname(server,(void*)&address,&size)); - target.port=ntohs(address.sin_port);snprintf(target.token,sizeof(target.token),"%s",login?"fixture-token":""); - pid_t child=fork();assert(child>=0); - if(!child){ - int fd=accept(server,NULL,NULL);if(fd<0)_exit(1);FILE *input=fdopen(dup(fd),"r");char request[512]; - if(login){if(!fgets(request,sizeof(request),input) || !strstr(request,"authorize"))_exit(2);write(fd,"{\"success\":true}\n",17);} - if(!fgets(request,sizeof(request),input) || !strstr(request,"serverinfo"))_exit(3); - const char *layout="{\"success\":true,\"info\":{\"leds\":[{\"hmin\":0,\"hmax\":1,\"vmin\":0,\"vmax\":0.1}]}}\n";write(fd,layout,strlen(layout)); - if(!fgets(request,sizeof(request),input) || !strstr(request,"ledstream-start"))_exit(4); - const char *reply=modern?"{\"success\":true,\"command\":\"ledcolors-ledstream-update\",\"data\":{\"leds\":[20,30,40]}}\n":"{\"success\":true,\"command\":\"ledcolors-ledstream-update\",\"result\":{\"leds\":[20,30,40]}}\n"; - write(fd,reply,13);usleep(20000);write(fd,reply+13,strlen(reply)-13);usleep(500000);close(fd);fclose(input);close(server);_exit(0); - } - stream_close(0);stream.retry_at=0; - for(unsigned i=0;i<500 && !stream.valid;i++){stream_tick(json,1000+i*5,true);usleep(1000);} - assert(stream.valid && stream.sequence==1 && stream.count==1); - for(unsigned r=0;r<16;r++){assert(stream.colors[r][0]==20 && stream.colors[r][1]==30 && stream.colors[r][2]==40);} - void *bad=json->parse("{\"result\":{\"leds\":[0,0,999]}}");assert(!stream_colors(json,bad,2000));json->put(bad);assert(stream.colors[0][2]==40); - bad=json->parse("{\"data\":{\"leds\":[0,0,0]}}");assert(stream_colors(json,bad,2000));json->put(bad);assert(stream.colors[0][2]==0); - bad=json->parse("{\"data\":{\"leds\":[1,2,3,4,5,6]}}");assert(!stream_colors(json,bad,2000) && !stream.revision);json->put(bad); - stream_tick(json,2001,false);assert(stream.fd<0 && !stream.valid);int status;waitpid(child,&status,0);assert(WIFEXITED(status) && !WEXITSTATUS(status));close(server); +static void fixture(PlaybackJson *json, bool modern, bool login) { + int server = socket(AF_INET, SOCK_STREAM, 0); + assert(server >= 0); + struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr = {htonl(INADDR_LOOPBACK)}}; + assert(!bind(server, (void *)&address, sizeof(address)) && !listen(server, 1)); + socklen_t size = sizeof(address); + assert(!getsockname(server, (void *)&address, &size)); + target.port = ntohs(address.sin_port); + snprintf(target.token, sizeof(target.token), "%s", login ? "fixture-token" : ""); + pid_t child = fork(); + assert(child >= 0); + if (!child) { + int fd = accept(server, NULL, NULL); + if (fd < 0) + _exit(1); + FILE *input = fdopen(dup(fd), "r"); + char request[512]; + if (login) { + if (!fgets(request, sizeof(request), input) || !strstr(request, "authorize")) + _exit(2); + write(fd, "{\"success\":true}\n", 17); + } + if (!fgets(request, sizeof(request), input) || !strstr(request, "serverinfo")) + _exit(3); + const char *layout = "{\"success\":true,\"info\":{\"leds\":[{\"hmin\":0,\"hmax\":1," + "\"vmin\":0,\"vmax\":0.1}]}}\n"; + write(fd, layout, strlen(layout)); + if (!fgets(request, sizeof(request), input) || !strstr(request, "ledstream-start")) + _exit(4); + const char *reply = modern ? "{\"success\":true,\"command\":\"ledcolors-ledstream-update\"," + "\"data\":{\"leds\":[20,30,40]}}\n" + : "{\"success\":true,\"command\":\"ledcolors-ledstream-update\"," + "\"result\":{\"leds\":[20,30,40]}}\n"; + write(fd, reply, 13); + usleep(20000); + write(fd, reply + 13, strlen(reply) - 13); + usleep(500000); + close(fd); + fclose(input); + close(server); + _exit(0); + } + stream_close(0); + stream.retry_at = 0; + for (unsigned i = 0; i < 500 && !stream.valid; i++) { + stream_tick(json, 1000 + i * 5, true); + usleep(1000); + } + assert(stream.valid && stream.sequence == 1 && stream.count == 1); + for (unsigned r = 0; r < 16; r++) { + assert(stream.colors[r][0] == 20 && stream.colors[r][1] == 30 && stream.colors[r][2] == 40); + } + /* More than the eight-message budget, with no further socket writes. */ + const char *buffered = + "{\"command\":\"ledcolors-ledstream-update\",\"data\":{\"leds\":[50,60,70]}}\n"; + stream.used = 0; + for (unsigned i = 0; i < 17; i++) { + memcpy(stream.data + stream.used, buffered, strlen(buffered)); + stream.used += strlen(buffered); + } + stream.data[stream.used] = 0; + for (unsigned i = 0; i < 3; i++) + stream_tick(json, 1900 + i, true); + assert(stream.sequence == 18 && stream.used == 0 && stream.colors[0][0] == 50); + void *bad = json->parse("{\"result\":{\"leds\":[0,0,999]}}"); + assert(!stream_colors(json, bad, 2000)); + json->put(bad); + assert(stream.colors[0][2] == 70); + bad = json->parse("{\"data\":{\"leds\":[0,0,0]}}"); + assert(stream_colors(json, bad, 2000)); + json->put(bad); + assert(stream.colors[0][2] == 0); + bad = json->parse("{\"data\":{\"leds\":[1,2,3,4,5,6]}}"); + assert(!stream_colors(json, bad, 2000) && !stream.revision); + json->put(bad); + stream_tick(json, 2001, false); + assert(stream.fd < 0 && !stream.valid); + int status; + waitpid(child, &status, 0); + assert(WIFEXITED(status) && !WEXITSTATUS(status)); + close(server); } -int main(void){ - PlaybackJson json={0};assert(playback_json_init(&json));fixture(&json,false,false);fixture(&json,true,true); - stream_start(1000);stream_tick(&json,5000,true);assert(stream.fd<0);stream_close(0); - TargetConfig copy=target;assert(target_valid(©));assert(target_piccap_compatible(©));assert(!target_piccap_local(©));assert(target_piccap_host(©)); - assert(!target_parse(&json,"{}",©));assert(!target_save("/no/such/profile",©));assert(!target_load(&json,"/no/such/profile",©)); - int fd=target_connect(©);if(fd>=0)close(fd); - dlclose(json.lib);puts("PASS: fragmented raw LED subscription, token login, old/new envelopes, invalid samples, real black, timeout and cleanup");return 0; +int main(void) { + PlaybackJson json = {0}; + assert(playback_json_init(&json)); + fixture(&json, false, false); + fixture(&json, true, true); + stream_start(1000); + stream_tick(&json, 5000, true); + assert(stream.fd < 0); + stream_close(0); + TargetConfig copy = target; + assert(target_valid(©)); + assert(target_piccap_compatible(©)); + assert(!target_piccap_local(©)); + assert(target_piccap_host(©)); + assert(!target_parse(&json, "{}", ©)); + assert(!target_save("/no/such/profile", ©)); + assert(!target_load(&json, "/no/such/profile", ©)); + int fd = target_connect(©); + if (fd >= 0) + close(fd); + dlclose(json.lib); + puts("PASS: fragmented raw LED subscription, token login, old/new envelopes, invalid samples, " + "real black, timeout and cleanup"); + return 0; } diff --git a/native/version.h b/native/version.h new file mode 100644 index 0000000..1ccdd7c --- /dev/null +++ b/native/version.h @@ -0,0 +1,4 @@ +#ifndef HYPERDRM_VERSION_H +#define HYPERDRM_VERSION_H +#define HYPERDRM_VERSION "0.12.0" +#endif diff --git a/package/packageinfo.json b/package/packageinfo.json index 9294b78..3c7fd06 100644 --- a/package/packageinfo.json +++ b/package/packageinfo.json @@ -1 +1,10 @@ -{"id": "org.webosbrew.histogramhyperion", "version": "0.11.0", "vendor": "Local development", "app": "org.webosbrew.histogramhyperion", "services": ["org.webosbrew.histogramhyperion.service"], "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM"} +{ + "id": "org.webosbrew.histogramhyperion", + "version": "0.12.0", + "vendor": "Local development", + "app": "org.webosbrew.histogramhyperion", + "services": [ + "org.webosbrew.histogramhyperion.service" + ], + "sourceUrl": "https://gitea.adnau.de/matze19999/HyperDRM" +} diff --git a/release.py b/release.py new file mode 100644 index 0000000..3b5a1b0 --- /dev/null +++ b/release.py @@ -0,0 +1,99 @@ +"""Test, compile and verify a release locally. Never contacts the TV. + +Use --push to commit the reviewed tree and push main and its version tag. +Release attachment publication requires the separately authorized Gitea session. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys + +from sync_version import synchronize + +ROOT = Path(__file__).resolve().parent + +GIT = ['git', '-c', 'safe.directory=' + ROOT.as_posix()] + +def run(*args): + if args[0] == 'git': + args = (*GIT, *args[1:]) + subprocess.run(args, cwd=ROOT, check=True) + +def linux(*args): + if os.name == 'nt': + path = '/mnt/' + str(ROOT)[0].lower() + str(ROOT)[2:].replace('\\', '/') + run('wsl', '-d', 'Debian', '--cd', path, '--exec', *args) + else: + run(*args) + +def verify_package(version): + import io + import tarfile + package = ROOT / f'HyperDRM_{version}_arm.ipk' + data = package.read_bytes() + manifest = json.loads((ROOT / 'HyperDRM.manifest.json').read_text(encoding='utf-8')) + assert manifest['version'] == version + assert manifest['ipkSize'] == len(data) + assert manifest['ipkHash']['sha256'] == hashlib.sha256(data).hexdigest() + assert data.startswith(b'!\n') + offset = 8 + members = {} + while offset < len(data): + header = data[offset:offset + 60] + assert len(header) == 60 and header[58:60] == b'`\n' + size = int(header[48:58]) + offset += 60 + members[header[:16].decode().strip().rstrip('/')] = data[offset:offset + size] + offset += size + size % 2 + with tarfile.open(fileobj=io.BytesIO(members['data.tar.gz']), mode='r:gz') as archive: + for directory, filename in [('applications', 'appinfo.json'), ('packages', 'packageinfo.json')]: + entry = archive.extractfile(f'usr/palm/{directory}/org.webosbrew.histogramhyperion/{filename}') + assert json.load(entry)['version'] == version + binary = archive.extractfile('usr/palm/services/org.webosbrew.histogramhyperion.service/histogram-service').read() + assert binary[:4] == b'\x7fELF' and binary[4] == 1 and binary[18:20] == b'\x28\x00' + assert version.encode() in binary + print(f'Verified {package.name}: {len(data)} bytes, SHA256 {manifest["ipkHash"]["sha256"]}') + return package + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--push', action='store_true') + options = parser.parse_args() + version = synchronize() + run(sys.executable, '-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_*.py') + node = shutil.which('node') + if not node: + raise RuntimeError('Node.js is required for UI tests') + for test in sorted((ROOT / 'tests').glob('*.test.js')): + run(node, str(test)) + # Host tests exercise the same portable code without LG libraries or a TV. + for test in sorted((ROOT / 'native').glob('test_*.c')): + name = '/tmp/hyperdrm-' + test.stem + linux('gcc', '-Wall', '-Wextra', '-Werror', '-O2', str(test.relative_to(ROOT)).replace('\\', '/'), '-ldl', '-lm', '-o', name) + linux(name) + linux('arm-linux-gnueabi-gcc', '-Wall', '-Wextra', '-Werror', '-O2', '-rdynamic', + 'native/service.c', '-ldl', '-o', 'service/histogram-service') + run(sys.executable, 'build_package.py') + run(sys.executable, 'build_manifest.py') + verify_package(version) + run('git', 'diff', '--check') + if options.push: + branch = subprocess.check_output(GIT + ['branch', '--show-current'], cwd=ROOT, text=True).strip() + if branch != 'main': + raise RuntimeError('Release pushes require main') + # Fail before committing when this version already exists. + exists = subprocess.run(GIT + ['rev-parse', '--verify', 'refs/tags/v' + version], cwd=ROOT, capture_output=True) + if exists.returncode == 0: + raise RuntimeError('Version tag already exists; bump VERSION') + run('git', 'add', '--all') + run('git', 'commit', '-m', f'Release {version}') + run('git', 'tag', 'v' + version) + run('git', 'push', '--atomic', 'origin', 'main', 'v' + version) + print('Attach the verified IPK to the matching Gitea release through an authorized session.') + +if __name__ == '__main__': + main() diff --git a/service/histogram-service b/service/histogram-service index 02c027d34b175418f730351da9fb5f29da6c57a3..d956250884f8b49523f1c9dfad3ea1339f248328 100644 GIT binary patch delta 10871 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 delta 10960 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.uri.includes('hbchannel'));assert.deepEqual(grant const grantCount=requests.filter(r=>r.uri.includes('hbchannel')).length; elements.autostart.onclick();flush();assert(!enabled);assert.equal(requests.filter(r=>r.uri.includes('hbchannel')).length,grantCount); uid=1000;deny=true;elements.autostart.onclick();flush();assert(!enabled && !elements.autostart.disabled && !elements.grantRoot.disabled);assert(elements.error.textContent.includes('could not grant root')); +const retainedError=elements.error.textContent;c.call('status',c.display);assert.equal(elements.error.textContent,retainedError); +c.showActionError('Invalid receiver settings');c.call('status',c.display);assert.equal(elements.error.textContent,'Invalid receiver settings'); deny=false;delayRoot=true;elements.grantRoot.onclick();elements.autostart.onclick();assert(pendingRoot && c.rootWaiters.length===2); delayRoot=false;pendingRoot.onservicecallback(JSON.stringify({returnValue:true}));flush();assert(enabled && !elements.autostart.disabled && !c.rootWaiters.length); +assert.equal(c.actionError,''); console.log('PASS: root-required autostart repairs only own service, confirms root after restart, retries setting, avoids unnecessary elevation and reports denial'); diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..8657a32 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,50 @@ +"""Version drift must fail before an inconsistent package can be built.""" +import json +from pathlib import Path +import tempfile +import unittest + +import sync_version + + +class VersionTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.previous_root = sync_version.ROOT + sync_version.ROOT = Path(self.directory.name) + for name in ('app', 'package', 'native'): + (sync_version.ROOT / name).mkdir() + (sync_version.ROOT / 'VERSION').write_text('1.2.3\n') + for name in ('app/appinfo.json', 'package/packageinfo.json'): + (sync_version.ROOT / name).write_text(json.dumps({'version': '1.0.0', 'id': 'unchanged'})) + + def tearDown(self): + sync_version.ROOT = self.previous_root + self.directory.cleanup() + + def test_check_rejects_drift_without_writing(self): + with self.assertRaises(ValueError): + sync_version.synchronize(check=True) + self.assertFalse((sync_version.ROOT / 'native/version.h').exists()) + + def test_sync_preserves_metadata_and_produces_runtime_version(self): + self.assertEqual(sync_version.synchronize(), '1.2.3') + sync_version.synchronize(check=True) + app = json.loads((sync_version.ROOT / 'app/appinfo.json').read_text()) + self.assertEqual(app, {'version': '1.2.3', 'id': 'unchanged'}) + self.assertIn('"1.2.3"', (sync_version.ROOT / 'native/version.h').read_text()) + + def test_invalid_version_is_rejected(self): + (sync_version.ROOT / 'VERSION').write_text('1.2\n') + with self.assertRaises(ValueError): + sync_version.synchronize() + + def test_native_drift_is_rejected(self): + sync_version.synchronize() + (sync_version.ROOT / 'native/version.h').write_text('old version') + with self.assertRaises(ValueError): + sync_version.synchronize(check=True) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/wizard.test.js b/tests/wizard.test.js index 373004d..a8b361a 100644 --- a/tests/wizard.test.js +++ b/tests/wizard.test.js @@ -1,11 +1,11 @@ 'use strict'; const assert=require('assert'),fs=require('fs'),vm=require('vm'); -const elements={},events={},requests=[];let active=false,sequence=0,globalResponse=false; +const elements={},events={},requests=[];let active=false,sequence=0,globalResponse=false,holdStatus=false,heldStatus=null; ['calibrationVideo','cancelCalibration','calibrate','validateCalibration','wizardStatus','customProfile'].forEach(id=>elements[id]={style:{},disabled:false,currentTime:0,textContent:'',value:'',events:{},focus(){},pause(){},play(){return Promise.resolve();},addEventListener(k,f){this.events[k]=f;}}); const c={profile:'auto',profileDirty:false,targetDirty:false,showProfile(){},display(){},setInterval(){return 1;},clearInterval(){},setTimeout(){},Date,Array,JSON, window:{addEventListener(k,f){events[k]=f;}},document:{getElementById(id){return elements[id];},addEventListener(k,f){events[k]=f;}}, call(method,done,data){ - requests.push({method,data});if(method==='calibrationSession')active=data.enabled; + requests.push({method,data});if(method==='status' && holdStatus){heldStatus=done;return;}if(method==='calibrationSession')active=data.enabled; const t=elements.calibrationVideo.currentTime,step=t<4?-1:Math.floor((t-4)/2); const regions=Array.from({length:16},(_,r)=>{ const center=r*2+.5,d=Math.min(Math.abs(step-center),32-Math.abs(step-center)); @@ -29,4 +29,6 @@ c.profileDirty=false;globalResponse=true;measure();assert(elements.wizardStatus. globalResponse=false;elements.calibrate.onclick();elements.cancelCalibration.onclick();assert(!active && !c.wizard); elements.validateCalibration.onclick();assert.equal(elements.calibrationVideo.currentTime,68);elements.calibrationVideo.events.ended();assert(!active && elements.wizardStatus.textContent.includes('Top-left red')); elements.calibrate.onclick();let prevented=false;events.keydown({keyCode:461,preventDefault(){prevented=true;}});assert(prevented && !active); +elements.calibrate.onclick();holdStatus=true;elements.calibrationVideo.currentTime=1;c.wizardSample();assert(heldStatus);const oldReply=heldStatus; +elements.cancelCalibration.onclick();holdStatus=false;elements.calibrate.onclick();const newSession=c.wizard;oldReply({returnValue:false,errorText:'Old failure'});assert.strictEqual(c.wizard,newSession);assert(!c.wizard.busy);elements.cancelCalibration.onclick(); console.log('PASS: guided measurement, proposal without save, invalid-response preservation, validation, cancel/Back and scoped remote navigation');