From f8685aec7bd92b52f7a3570e244be3d14d0e12ee Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:58:51 -0400 Subject: [PATCH 1/4] test(scroll-cards): refresh the goldens #409 made stale scripts/test_scroll_card_renders.py has been red on main since #409, which made the safety check fail on every open PR regardless of what that PR touched -- #428 fails it while changing only one plugin's README and manifest. The signal was useless: a real render regression would have looked identical to this noise. #409 deliberately changed which font several text elements draw in, so the goldens committed on 2026-08-02 describe the old, wrong rendering. Measured: at the parent of #409 the guard reports 3 problems, on main it reports 42. These 39 are that difference. Checked rather than assumed. Every regenerated card was compared against its predecessor pixel by pixel, and the four sampled visually: the text content is identical and only the glyph shapes change, which is what a font swap should look like. --update also rewrote 24 cards whose pixels had not changed at all, presumably encoder metadata; those are reverted so the diff is only the 39 that actually differ. Baseball's three upcoming cards are deliberately NOT refreshed. They are the 3 that were already failing before #409, and they fail for an unrelated reason: game_renderer.py formats the date with %-m/%-d and %-I:%M%p, which are glibc extensions. Off glibc they raise, the except falls back to a raw ISO string, and the card loses its start time and shows 2026-09-19 instead of Sep 19. The committed goldens are correct; regenerating them anywhere but glibc would bake the broken output in. Filed separately. Co-Authored-By: Claude Opus 5 --- .../test/golden-cards/128x64/live.png | Bin 236 -> 202 bytes .../test/golden-cards/128x64/recent.png | Bin 236 -> 202 bytes .../test/golden-cards/128x64/upcoming.png | Bin 236 -> 202 bytes .../test/golden-cards/256x128/live.png | Bin 312 -> 278 bytes .../test/golden-cards/256x128/recent.png | Bin 312 -> 278 bytes .../test/golden-cards/256x128/upcoming.png | Bin 312 -> 278 bytes .../test/golden-cards/64x32/live.png | Bin 199 -> 184 bytes .../test/golden-cards/64x32/recent.png | Bin 199 -> 184 bytes .../test/golden-cards/64x32/upcoming.png | Bin 199 -> 184 bytes .../test/golden-cards/128x64/live.png | Bin 238 -> 193 bytes .../test/golden-cards/128x64/recent.png | Bin 238 -> 193 bytes .../test/golden-cards/128x64/upcoming.png | Bin 238 -> 193 bytes .../test/golden-cards/256x128/live.png | Bin 307 -> 267 bytes .../test/golden-cards/256x128/recent.png | Bin 307 -> 267 bytes .../test/golden-cards/256x128/upcoming.png | Bin 307 -> 267 bytes .../test/golden-cards/64x32/live.png | Bin 207 -> 173 bytes .../test/golden-cards/64x32/recent.png | Bin 207 -> 173 bytes .../test/golden-cards/64x32/upcoming.png | Bin 207 -> 173 bytes .../test/golden-cards/128x64/upcoming.png | Bin 6657 -> 6628 bytes .../test/golden-cards/256x128/upcoming.png | Bin 23814 -> 23671 bytes .../test/golden-cards/64x32/upcoming.png | Bin 761 -> 742 bytes .../test/golden-cards/128x64/live.png | Bin 247 -> 204 bytes .../test/golden-cards/128x64/recent.png | Bin 247 -> 204 bytes .../test/golden-cards/128x64/upcoming.png | Bin 247 -> 204 bytes .../test/golden-cards/256x128/live.png | Bin 319 -> 280 bytes .../test/golden-cards/256x128/recent.png | Bin 319 -> 280 bytes .../test/golden-cards/256x128/upcoming.png | Bin 319 -> 280 bytes .../test/golden-cards/64x32/live.png | Bin 219 -> 183 bytes .../test/golden-cards/64x32/recent.png | Bin 219 -> 183 bytes .../test/golden-cards/64x32/upcoming.png | Bin 219 -> 183 bytes .../test/golden-cards/128x64/live.png | Bin 267 -> 187 bytes .../test/golden-cards/128x64/recent.png | Bin 267 -> 187 bytes .../test/golden-cards/128x64/upcoming.png | Bin 267 -> 187 bytes .../test/golden-cards/256x128/live.png | Bin 341 -> 263 bytes .../test/golden-cards/256x128/recent.png | Bin 341 -> 263 bytes .../test/golden-cards/256x128/upcoming.png | Bin 341 -> 263 bytes .../test/golden-cards/64x32/live.png | Bin 237 -> 168 bytes .../test/golden-cards/64x32/recent.png | Bin 237 -> 168 bytes .../test/golden-cards/64x32/upcoming.png | Bin 237 -> 168 bytes 39 files changed, 0 insertions(+), 0 deletions(-) diff --git a/plugins/afl-scoreboard/test/golden-cards/128x64/live.png b/plugins/afl-scoreboard/test/golden-cards/128x64/live.png index 586c1f833a01edc9f178c5b066c70362022d8777..8389e8721dc0f2fef016a6aaa7d5e4f90ec8599c 100644 GIT binary patch delta 174 zcmaFEc#3g?O8rDn7srr_TW_x(?!wio&B z1-|F+Z@CiIyl}-o%XHa-pE+r1=hx`mui=~@v!vFEfB&AAt2_)0>I~>0Bkn?C;d4!n Q{}(_qp00i_>zopr06E%CjsO4v delta 193 zcmV;y06zc90qg;gB!9C>L_t(|+U?ds4uc>N1yF6g|CQ;&gc^`h7KCcv+x6Q@n9(WA z0su_0o~9|K-#6B^4(mLBEN;@u>-(HuDw4}QSoc3|t?K8^q?Gc8k44|}-u3EVLv zYoVu9xuXF^6$b4vKfjl+(d%u~W?y5B1nr4O)?!wio&B z1-|F+Z@CiIyl}-o%XHa-pE+r1=hx`mui=~@v!vFEfB&AAt2_)0>I~>0Bkn?C;d4!n Q{}(_qp00i_>zopr06E%CjsO4v delta 193 zcmV;y06zc90qg;gB!9C>L_t(|+U?ds4uc>N1yF6g|CQ;&gc^`h7KCcv+x6Q@n9(WA z0su_0o~9|K-#6B^4(mLBEN;@u>-(HuDw4}QSoc3|t?K8^q?Gc8k44|}-u3EVLv zYoVu9xuXF^6$b4vKfjl+(d%u~W?y5B1nr4O)?!wio&B z1-|F+Z@CiIyl}-o%XHa-pE+r1=hx`mui=~@v!vFEfB&AAt2_)0>I~>0Bkn?C;d4!n Q{}(_qp00i_>zopr06E%CjsO4v delta 193 zcmV;y06zc90qg;gB!9C>L_t(|+U?ds4uc>N1yF6g|CQ;&gc^`h7KCcv+x6Q@n9(WA z0su_0o~9|K-#6B^4(mLBEN;@u>-(HuDw4}QSoc3|t?K8^q?Gc8k44|}-u3EVLv zYoVu9xuXF^6$b4vKfjl+(d%u~W?y5B1nr4O)J0MK^X|NnA#FmVLprpB;kd+%fs@X}I= z0001hKat~5RYgQ*j$u`;<74=C?enXdW7us4AF$@-@~{MdhD|@9hAm&*sc*Gh3GPQN1yE@E|G!KZCe(n8niP`8b9RmoHg{w+ zB>(^bc!+#hEv0;~$hj}}^?KMmWK{2;b9kz$oYsSNq4yr8zlN!lQUkXS{Z_xbUVm4u z1i>9GB<1|L0h$y}N?3h=pKc?tdIwlEdkw~zpgrP|N%Qbe4Jfc>_u`0e1Y(j`@Euua z{TZ+Sy?$L-G>ODRNV@zNDCnZNKIt!zjRKLePLaR}7oOn*bGv&XVKdO|00000NkvXX Hu0mjfqUv7Z diff --git a/plugins/afl-scoreboard/test/golden-cards/256x128/recent.png b/plugins/afl-scoreboard/test/golden-cards/256x128/recent.png index 80cc206a1e2c2d0168c9b4cce7bb5d060eef8084..da7a8cc00208fc0b63cf39b15875791cb2c96384 100644 GIT binary patch delta 170 zcmV;b09F6E0+s@hB!AsWL_t(|+U?iD4uc>J0MK^X|NnA#FmVLprpB;kd+%fs@X}I= z0001hKat~5RYgQ*j$u`;<74=C?enXdW7us4AF$@-@~{MdhD|@9hAm&*sc*Gh3GPQN1yE@E|G!KZCe(n8niP`8b9RmoHg{w+ zB>(^bc!+#hEv0;~$hj}}^?KMmWK{2;b9kz$oYsSNq4yr8zlN!lQUkXS{Z_xbUVm4u z1i>9GB<1|L0h$y}N?3h=pKc?tdIwlEdkw~zpgrP|N%Qbe4Jfc>_u`0e1Y(j`@Euua z{TZ+Sy?$L-G>ODRNV@zNDCnZNKIt!zjRKLePLaR}7oOn*bGv&XVKdO|00000NkvXX Hu0mjfqUv7Z diff --git a/plugins/afl-scoreboard/test/golden-cards/256x128/upcoming.png b/plugins/afl-scoreboard/test/golden-cards/256x128/upcoming.png index 80cc206a1e2c2d0168c9b4cce7bb5d060eef8084..da7a8cc00208fc0b63cf39b15875791cb2c96384 100644 GIT binary patch delta 170 zcmV;b09F6E0+s@hB!AsWL_t(|+U?iD4uc>J0MK^X|NnA#FmVLprpB;kd+%fs@X}I= z0001hKat~5RYgQ*j$u`;<74=C?enXdW7us4AF$@-@~{MdhD|@9hAm&*sc*Gh3GPQN1yE@E|G!KZCe(n8niP`8b9RmoHg{w+ zB>(^bc!+#hEv0;~$hj}}^?KMmWK{2;b9kz$oYsSNq4yr8zlN!lQUkXS{Z_xbUVm4u z1i>9GB<1|L0h$y}N?3h=pKc?tdIwlEdkw~zpgrP|N%Qbe4Jfc>_u`0e1Y(j`@Euua z{TZ+Sy?$L-G>ODRNV@zNDCnZNKIt!zjRKLePLaR}7oOn*bGv&XVKdO|00000NkvXX Hu0mjfqUv7Z diff --git a/plugins/afl-scoreboard/test/golden-cards/64x32/live.png b/plugins/afl-scoreboard/test/golden-cards/64x32/live.png index 2cabb02964b04c9f77060b6fc857bada2e466d6c..8c1d7c0dd00f8a2e1c1c5e106c6c9a548b32b3dc 100644 GIT binary patch delta 156 zcmV;N0Av5h0k{E>B!7QNL_t(|+U?WP4uBvCKv8J@|1Z-+n=zPWj#_Q^JcX$lf(U?L z(>!EmBBH7}G_y9I!^^eDS5m0001AuoVHt_HpN>`WE^C0000< KMNUMnLSTX&r%6`; delta 171 zcmdnNc${&9N`0TFi(^Q|t+!Jg`5F{>SdM=9ufM)sEklR(ibT?!ot9i%N)K;hII!F^ zR6Fy(lB)h*{r=^?O7Fj~+-D{EzBpv>TW!yv$O4^Pf37zE3_a??>wc*wum7!EXLByk zzhiopQDw~)Cbw3*R9*Kz%Cn X6{b>&ZGXDff&@HW{an^LB{Ts5F0D(J diff --git a/plugins/afl-scoreboard/test/golden-cards/64x32/recent.png b/plugins/afl-scoreboard/test/golden-cards/64x32/recent.png index 2cabb02964b04c9f77060b6fc857bada2e466d6c..8c1d7c0dd00f8a2e1c1c5e106c6c9a548b32b3dc 100644 GIT binary patch delta 156 zcmV;N0Av5h0k{E>B!7QNL_t(|+U?WP4uBvCKv8J@|1Z-+n=zPWj#_Q^JcX$lf(U?L z(>!EmBBH7}G_y9I!^^eDS5m0001AuoVHt_HpN>`WE^C0000< KMNUMnLSTX&r%6`; delta 171 zcmdnNc${&9N`0TFi(^Q|t+!Jg`5F{>SdM=9ufM)sEklR(ibT?!ot9i%N)K;hII!F^ zR6Fy(lB)h*{r=^?O7Fj~+-D{EzBpv>TW!yv$O4^Pf37zE3_a??>wc*wum7!EXLByk zzhiopQDw~)Cbw3*R9*Kz%Cn X6{b>&ZGXDff&@HW{an^LB{Ts5F0D(J diff --git a/plugins/afl-scoreboard/test/golden-cards/64x32/upcoming.png b/plugins/afl-scoreboard/test/golden-cards/64x32/upcoming.png index 2cabb02964b04c9f77060b6fc857bada2e466d6c..8c1d7c0dd00f8a2e1c1c5e106c6c9a548b32b3dc 100644 GIT binary patch delta 156 zcmV;N0Av5h0k{E>B!7QNL_t(|+U?WP4uBvCKv8J@|1Z-+n=zPWj#_Q^JcX$lf(U?L z(>!EmBBH7}G_y9I!^^eDS5m0001AuoVHt_HpN>`WE^C0000< KMNUMnLSTX&r%6`; delta 171 zcmdnNc${&9N`0TFi(^Q|t+!Jg`5F{>SdM=9ufM)sEklR(ibT?!ot9i%N)K;hII!F^ zR6Fy(lB)h*{r=^?O7Fj~+-D{EzBpv>TW!yv$O4^Pf37zE3_a??>wc*wum7!EXLByk zzhiopQDw~)Cbw3*R9*Kz%Cn X6{b>&ZGXDff&@HW{an^LB{Ts5F0D(J diff --git a/plugins/football-scoreboard/test/golden-cards/128x64/live.png b/plugins/football-scoreboard/test/golden-cards/128x64/live.png index 17c3d90e645160657dcb0f0de4ebcde5a81f374e..b7ba85448e1d46b094c07a1f1c0f438c05c6594d 100644 GIT binary patch delta 165 zcmaFIc#v^|N_~f?i(^Q|t+!VV`3@@xusGyT`CmU%WSZb3p=9^2DM`x?-3i^^u$`CT z2lv|x_Qze<*1eju>JCT3tRE+9vY&fO=i9D#-?V1iwMjm_rR}Z^zkPl`_}1F|N4bC3 z)F;0-%FdpAWsXX@b;Hx<%j-{k@s~4x5$Z3%fCS{A>gTe~ HDWM4fG8;#9 delta 210 zcmX@e_>OUcO8r((7srr_TW_yk?Fk(h*DOt#Zu4AImHXX`9XbpL z{HK_#+xpDhtMlhuOWxz&>+02YJ5QEJ|4zNB8k8-YUN|Ku>grrck*{C&=j$B^PIBKK z*w=qZ<@W#AOk3_g+jD&CrFC9~=W}d%tK@^XN5-#={QlQ-)|s`|-Vff1)_<65Ar{!2 ztJd~eW+MB$hQIIn9^5)N`I5|)gl*Xjn4rP_E2Cg{g;b+JCT3tRE+9vY&fO=i9D#-?V1iwMjm_rR}Z^zkPl`_}1F|N4bC3 z)F;0-%FdpAWsXX@b;Hx<%j-{k@s~4x5$Z3%fCS{A>gTe~ HDWM4fG8;#9 delta 210 zcmX@e_>OUcO8r((7srr_TW_yk?Fk(h*DOt#Zu4AImHXX`9XbpL z{HK_#+xpDhtMlhuOWxz&>+02YJ5QEJ|4zNB8k8-YUN|Ku>grrck*{C&=j$B^PIBKK z*w=qZ<@W#AOk3_g+jD&CrFC9~=W}d%tK@^XN5-#={QlQ-)|s`|-Vff1)_<65Ar{!2 ztJd~eW+MB$hQIIn9^5)N`I5|)gl*Xjn4rP_E2Cg{g;b+JCT3tRE+9vY&fO=i9D#-?V1iwMjm_rR}Z^zkPl`_}1F|N4bC3 z)F;0-%FdpAWsXX@b;Hx<%j-{k@s~4x5$Z3%fCS{A>gTe~ HDWM4fG8;#9 delta 210 zcmX@e_>OUcO8r((7srr_TW_yk?Fk(h*DOt#Zu4AImHXX`9XbpL z{HK_#+xpDhtMlhuOWxz&>+02YJ5QEJ|4zNB8k8-YUN|Ku>grrck*{C&=j$B^PIBKK z*w=qZ<@W#AOk3_g+jD&CrFC9~=W}d%tK@^XN5-#={QlQ-)|s`|-Vff1)_<65Ar{!2 ztJd~eW+MB$hQIIn9^5)N`I5|)gl*Xjn4rP_E2Cg{g;b+N1yFzE{jW?Hri3u5x@n^Jy%jREoDaAx z0002+r`kvV92d3vt#hivZ2~Y&{LXyW&Yd>6p!Iwt4y&!TBvV7(flCKqo9RBd+rmN4 zBfaWYlJm#**qo&1L~^)U01h8K{fe0WiN_4|swWTt000000Fl>4k>Cs${Ky+oL3g|y S6Ps%Q0000`MeILhp_svDUU)$M=bsRHN1yFzE{jW?Hri3u5x@n^Jy%jREoDaAx z0002+r`kvV92d3vt#hivZ2~Y&{LXyW&Yd>6p!Iwt4y&!TBvV7(flCKqo9RBd+rmN4 zBfaWYlJm#**qo&1L~^)U01h8K{fe0WiN_4|swWTt000000Fl>4k>Cs${Ky+oL3g|y S6Ps%Q0000`MeILhp_svDUU)$M=bsRHN1yFzE{jW?Hri3u5x@n^Jy%jREoDaAx z0002+r`kvV92d3vt#hivZ2~Y&{LXyW&Yd>6p!Iwt4y&!TBvV7(flCKqo9RBd+rmN4 zBfaWYlJm#**qo&1L~^)U01h8K{fe0WiN_4|swWTt000000Fl>4k>Cs${Ky+oL3g|y S6Ps%Q0000`MeILhp_svDUU)$M=bsRHkQRL1B8hO^X1M#+alY1q|2(ANeT^lXL*1&lfSqvUN{VMYTlAG|JnLx zNjc~000000_^2I}W_P^fgSwdj015yANkvXXu0mjffWboX delta 179 zcmV;k08Ia_0nY)DB!8AkL_t(|+U=A}3cw%?1r2!rE2j$~NYt33o0gfC2J?SyDFEjp-( z>@_Mfdbx5>sM|vOG*hu=dY-;disa}UIQ>N|;@DcOhi0*Bek?`xqR{)qqjFp|K@WF_ h2mk;80002+QyxmEcpAR96PEx0002ovPDHLkV1l;^PT&9l diff --git a/plugins/football-scoreboard/test/golden-cards/64x32/recent.png b/plugins/football-scoreboard/test/golden-cards/64x32/recent.png index b5e994038edf09ec516667e78917de41df1516f3..7ca2f38987e0f6df3ed8654ff63f88927a1b8085 100644 GIT binary patch delta 145 zcmV;C0B--!0j&X$B!6^CL_t(|+U?WP3VkQRL1B8hO^X1M#+alY1q|2(ANeT^lXL*1&lfSqvUN{VMYTlAG|JnLx zNjc~000000_^2I}W_P^fgSwdj015yANkvXXu0mjffWboX delta 179 zcmV;k08Ia_0nY)DB!8AkL_t(|+U=A}3cw%?1r2!rE2j$~NYt33o0gfC2J?SyDFEjp-( z>@_Mfdbx5>sM|vOG*hu=dY-;disa}UIQ>N|;@DcOhi0*Bek?`xqR{)qqjFp|K@WF_ h2mk;80002+QyxmEcpAR96PEx0002ovPDHLkV1l;^PT&9l diff --git a/plugins/football-scoreboard/test/golden-cards/64x32/upcoming.png b/plugins/football-scoreboard/test/golden-cards/64x32/upcoming.png index b5e994038edf09ec516667e78917de41df1516f3..7ca2f38987e0f6df3ed8654ff63f88927a1b8085 100644 GIT binary patch delta 145 zcmV;C0B--!0j&X$B!6^CL_t(|+U?WP3VkQRL1B8hO^X1M#+alY1q|2(ANeT^lXL*1&lfSqvUN{VMYTlAG|JnLx zNjc~000000_^2I}W_P^fgSwdj015yANkvXXu0mjffWboX delta 179 zcmV;k08Ia_0nY)DB!8AkL_t(|+U=A}3cw%?1r2!rE2j$~NYt33o0gfC2J?SyDFEjp-( z>@_Mfdbx5>sM|vOG*hu=dY-;disa}UIQ>N|;@DcOhi0*Bek?`xqR{)qqjFp|K@WF_ h2mk;80002+QyxmEcpAR96PEx0002ovPDHLkV1l;^PT&9l diff --git a/plugins/hockey-scoreboard/test/golden-cards/128x64/upcoming.png b/plugins/hockey-scoreboard/test/golden-cards/128x64/upcoming.png index aea8c79c1d23b730af206e7ee7761b19ad976934..660a2bb9659d64fefb4e59197556327af9f5cc47 100644 GIT binary patch delta 6621 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 N002ovPDHLkV1jam%>)1d diff --git a/plugins/hockey-scoreboard/test/golden-cards/64x32/upcoming.png b/plugins/hockey-scoreboard/test/golden-cards/64x32/upcoming.png index 630d8c9aad1b5d0a45d5c8d29a52356d3ff0f7ab..23f08f786ebe6ecd8f0a3b87e71fc6250113349d 100644 GIT binary patch delta 718 zcmV;<0x|vh1?B~iB!8_*L_t(|+U;07Z_`i|KIhuXn>2};?43tNjK8mK_2lIE$lNh>$0V;=?!%koQ!okEcO$%~Kg zdER?{zIy@wbD;Kxh)hNx+M7_dbR(_gHWqYF6}hUedsVi=N3W2AFeQ=M8KlC#nqMj zG9izFB+7jZ&q^!+fLwTn=$M$FdaW_GCv9R1O&ZnCg}Ln7^Ea=bEf-6*a5$1b`)=*! z$0v^-@R+(IgCDxTkVKMY31X1G$FNV^Z%>XFI9WfbV1Gc|I5m}w#rm;$e7j!vAAikd z!aQ}pCFbv5yR{IH`iZE$S14{Z8=D^-*IQ{}rX9^)4oZhcb#o#z0ASGB7<7In;t^4l z+wH?p$mbI)&{=m}TH`T5#}S+{(^^9iSmB`4+~418HS1@xfgYdVX$S}a5Lvi-d#P}K zS{lgZ`hVf)_pd-$cS*>sXt`r@k&ffV3_X7Of;;+7fYI#r)s>}E4H1#U(=6U5C$F|~=1VO?fW11ilC}=>4o+?Ou zKnf6zX{l1CgMx|z8t7;cEd`>XOOQnj5y+7ppGsmUa^kFahhimbwd;LeKikMi9=X}^ zo8!%!8{j_&)PE3Bj}wS|7F{aCNh>^ugF&c}5x`*}uTH$%N z)X6ol;xJ)m7M$yHET6!?$YpI2^F%JOb zi_NWHh0>lPX@9z+YnH;d002)a)UFF>@t%d;P)Eg*QaprMb=TDvgu>Sh< zvnP)vjNVhh1l*_Ku@5#7N zqQ;N#;{6+UmO=p+5!Lt8+3jj&>yxm(wG!5|q78QktwLirI1w2DFlcOc8o$FKzbvcu z`jOY`bjk&2YzUjyJO&tg1hLMvRB5$X`mj+wIM}aM%jXjA6DhS@X(0fBf9d+&mGs3~ zrK9G{M}J>GzJb=pFdomDLWG-(SO_o2>FM)Vj^m#M7)ab)%dF%|h=>l1-1aAzJx=P# zN{6Jw-EUO;LZlAkMVxM4JPg9@>Xi#~aW4Sni_P42v9zZkAfm%V?s*^wIGmuguw5YK z7--_^z!WBbE5V;c$JExF!+IdOD9{>=V~iBeT3U7|vc49w+U@jZJLJM*U>CBoknoH3 zbs_N=Ffi38cp7Jt|b~pmz!9U=LwU`d%}+ZrO8q`h7srr_TW_xhay1+9Fa*f%{y%kd|E8dvvu`AFO5XpKOg`W?IhUDX zUy^-{`w_$B`|Tf#Rv+>9^Od$M3RCZYzFn+9e*4nOpB*cvr{~{$7Sp+B&HZDy`PP-~`hS)~(S_xwQ%Wv`Y`sz-|3!JlvbE+} z_wFzjbMJk9FlTPm!gxiu3)5;&Y(DGZ7I44Wtnz9o4+91`pkKux|Dc_JvF(OSAW2VG KKbLh*2~7ZvGhz$? diff --git a/plugins/nrl-scoreboard/test/golden-cards/128x64/recent.png b/plugins/nrl-scoreboard/test/golden-cards/128x64/recent.png index 060fa692b17fa6d2b1abae17716baf745aacdf55..dec9ac59e2e58d72cf1a1c0c34604a8e113a3751 100644 GIT binary patch delta 176 zcmey)c!qI;O8sO{7srr_TW_yg@*Pp&VGWq@>|b~pmz!9U=LwU`d%}+ZrO8q`h7srr_TW_xhay1+9Fa*f%{y%kd|E8dvvu`AFO5XpKOg`W?IhUDX zUy^-{`w_$B`|Tf#Rv+>9^Od$M3RCZYzFn+9e*4nOpB*cvr{~{$7Sp+B&HZDy`PP-~`hS)~(S_xwQ%Wv`Y`sz-|3!JlvbE+} z_wFzjbMJk9FlTPm!gxiu3)5;&Y(DGZ7I44Wtnz9o4+91`pkKux|Dc_JvF(OSAW2VG KKbLh*2~7ZvGhz$? diff --git a/plugins/nrl-scoreboard/test/golden-cards/128x64/upcoming.png b/plugins/nrl-scoreboard/test/golden-cards/128x64/upcoming.png index 060fa692b17fa6d2b1abae17716baf745aacdf55..dec9ac59e2e58d72cf1a1c0c34604a8e113a3751 100644 GIT binary patch delta 176 zcmey)c!qI;O8sO{7srr_TW_yg@*Pp&VGWq@>|b~pmz!9U=LwU`d%}+ZrO8q`h7srr_TW_xhay1+9Fa*f%{y%kd|E8dvvu`AFO5XpKOg`W?IhUDX zUy^-{`w_$B`|Tf#Rv+>9^Od$M3RCZYzFn+9e*4nOpB*cvr{~{$7Sp+B&HZDy`PP-~`hS)~(S_xwQ%Wv`Y`sz-|3!JlvbE+} z_wFzjbMJk9FlTPm!gxiu3)5;&Y(DGZ7I44Wtnz9o4+91`pkKux|Dc_JvF(OSAW2VG KKbLh*2~7ZvGhz$? diff --git a/plugins/nrl-scoreboard/test/golden-cards/256x128/live.png b/plugins/nrl-scoreboard/test/golden-cards/256x128/live.png index 144637aa917b1a48757d46cd87cb4a78c25ed2a3..d5445fcbc7e3a06134e78ec96da77df4b2e6a7b6 100644 GIT binary patch delta 173 zcmV;e08;CF5CzZ({r_K{E-Zs+OcN+Hl56{M(2Lh4dN0IOl b7yQH1)~BYyz~Nkl5@x2?P_CQ+K zE(-tv0Q`w{^BfV|h;{9rmsjPy#MH3vPxkVR&&RLeL-&4Lq~eQ+`nXqlIp)a8+x+=F zcZkyGT(`3lb+3qc@}c>q8{kNl=Re~z3ysI5BY6hB1mA0bK`^23E)%^%uc^BGKsy4} zFryBa*a7Ai>ZS)~^VAIO3IW#|fo3m+96T6-+8)TkR$-B`P?5j}7vR+ci|dCEK@V<1 P00000NkvXXu0mjfi_~as diff --git a/plugins/nrl-scoreboard/test/golden-cards/256x128/recent.png b/plugins/nrl-scoreboard/test/golden-cards/256x128/recent.png index 144637aa917b1a48757d46cd87cb4a78c25ed2a3..d5445fcbc7e3a06134e78ec96da77df4b2e6a7b6 100644 GIT binary patch delta 173 zcmV;e08;CF5CzZ({r_K{E-Zs+OcN+Hl56{M(2Lh4dN0IOl b7yQH1)~BYyz~Nkl5@x2?P_CQ+K zE(-tv0Q`w{^BfV|h;{9rmsjPy#MH3vPxkVR&&RLeL-&4Lq~eQ+`nXqlIp)a8+x+=F zcZkyGT(`3lb+3qc@}c>q8{kNl=Re~z3ysI5BY6hB1mA0bK`^23E)%^%uc^BGKsy4} zFryBa*a7Ai>ZS)~^VAIO3IW#|fo3m+96T6-+8)TkR$-B`P?5j}7vR+ci|dCEK@V<1 P00000NkvXXu0mjfi_~as diff --git a/plugins/nrl-scoreboard/test/golden-cards/256x128/upcoming.png b/plugins/nrl-scoreboard/test/golden-cards/256x128/upcoming.png index 144637aa917b1a48757d46cd87cb4a78c25ed2a3..d5445fcbc7e3a06134e78ec96da77df4b2e6a7b6 100644 GIT binary patch delta 173 zcmV;e08;CF5CzZ({r_K{E-Zs+OcN+Hl56{M(2Lh4dN0IOl b7yQH1)~BYyz~Nkl5@x2?P_CQ+K zE(-tv0Q`w{^BfV|h;{9rmsjPy#MH3vPxkVR&&RLeL-&4Lq~eQ+`nXqlIp)a8+x+=F zcZkyGT(`3lb+3qc@}c>q8{kNl=Re~z3ysI5BY6hB1mA0bK`^23E)%^%uc^BGKsy4} zFryBa*a7Ai>ZS)~^VAIO3IW#|fo3m+96T6-+8)TkR$-B`P?5j}7vR+ci|dCEK@V<1 P00000NkvXXu0mjfi_~as diff --git a/plugins/nrl-scoreboard/test/golden-cards/64x32/live.png b/plugins/nrl-scoreboard/test/golden-cards/64x32/live.png index 2eb340a973b80be7189549bdfacb454b6f781687..5a3df82fc239b1e3f200f9809af74812c30e9d7e 100644 GIT binary patch delta 155 zcmV;M0A&B$0k;8=B!7NML_t(|+U?Wb3VGB`(B)OEj@+KUJ&eo-0{{R3005tL02D06Yxkxc`#Jys002ov JPDHLkV1m;kMEn2% delta 191 zcmV;w06_n@0owtPB!8kwL_t(|+U=9e4nrXb1+lsR|I2h?N{HgAHn)qWGdp?7Q7{95 zpQa6GcON5KEA8FY?L8*K%9K?dZPzX{yJ}e}+1=xqGY8p&OIzO7&-ScvZs2j(SC(Jo zFTOWI*T-Gv$LO=9$84vLc#AUuqS56tRgFfeav$aqiZF4}mN8Ep t{g50iMrhWMgIB8z00000008iRJOSIahYxQ8CrSVS002ovPDHLkV1g|ISs?%b diff --git a/plugins/nrl-scoreboard/test/golden-cards/64x32/recent.png b/plugins/nrl-scoreboard/test/golden-cards/64x32/recent.png index 2eb340a973b80be7189549bdfacb454b6f781687..5a3df82fc239b1e3f200f9809af74812c30e9d7e 100644 GIT binary patch delta 155 zcmV;M0A&B$0k;8=B!7NML_t(|+U?Wb3VGB`(B)OEj@+KUJ&eo-0{{R3005tL02D06Yxkxc`#Jys002ov JPDHLkV1m;kMEn2% delta 191 zcmV;w06_n@0owtPB!8kwL_t(|+U=9e4nrXb1+lsR|I2h?N{HgAHn)qWGdp?7Q7{95 zpQa6GcON5KEA8FY?L8*K%9K?dZPzX{yJ}e}+1=xqGY8p&OIzO7&-ScvZs2j(SC(Jo zFTOWI*T-Gv$LO=9$84vLc#AUuqS56tRgFfeav$aqiZF4}mN8Ep t{g50iMrhWMgIB8z00000008iRJOSIahYxQ8CrSVS002ovPDHLkV1g|ISs?%b diff --git a/plugins/nrl-scoreboard/test/golden-cards/64x32/upcoming.png b/plugins/nrl-scoreboard/test/golden-cards/64x32/upcoming.png index 2eb340a973b80be7189549bdfacb454b6f781687..5a3df82fc239b1e3f200f9809af74812c30e9d7e 100644 GIT binary patch delta 155 zcmV;M0A&B$0k;8=B!7NML_t(|+U?Wb3VGB`(B)OEj@+KUJ&eo-0{{R3005tL02D06Yxkxc`#Jys002ov JPDHLkV1m;kMEn2% delta 191 zcmV;w06_n@0owtPB!8kwL_t(|+U=9e4nrXb1+lsR|I2h?N{HgAHn)qWGdp?7Q7{95 zpQa6GcON5KEA8FY?L8*K%9K?dZPzX{yJ}e}+1=xqGY8p&OIzO7&-ScvZs2j(SC(Jo zFTOWI*T-Gv$LO=9$84vLc#AUuqS56tRgFfeav$aqiZF4}mN8Ep t{g50iMrhWMgIB8z00000008iRJOSIahYxQ8CrSVS002ovPDHLkV1g|ISs?%b diff --git a/plugins/soccer-scoreboard/test/golden-cards/128x64/live.png b/plugins/soccer-scoreboard/test/golden-cards/128x64/live.png index b7b79e4caa518a898f73615cca0cbdd7ad88532a..940415f185e9969f558d8943a6d42f44a74bfde9 100644 GIT binary patch delta 143 zcmV;A0C4|{0=of_ByEC8L_t(|+U?X!3V<*S1wbp_|H^d{C{bJdpx|O=)yBBV3!Nqa z;1_DGyKBgw@*(%yOvL%uNZyHAd0QRy#Qj1^Gv(v x7DMajUI51lcGot~ceK>ebEOlJ;S(2NYX#UneZdbJ08lXg|1WnB%LwD5ahqA@&Uq8DCM{4> z0sy`u_d^kpQp!0G$DcmVxtUx)HuLL%f2^SZ0000yOvL%uNZyHAd0QRy#Qj1^Gv(v x7DMajUI51lcGot~ceK>ebEOlJ;S(2NYX#UneZdbJ08lXg|1WnB%LwD5ahqA@&Uq8DCM{4> z0sy`u_d^kpQp!0G$DcmVxtUx)HuLL%f2^SZ0000yOvL%uNZyHAd0QRy#Qj1^Gv(v x7DMajUI51lcGot~ceK>ebEOlJ;S(2NYX#UneZdbJ08lXg|1WnB%LwD5ahqA@&Uq8DCM{4> z0sy`u_d^kpQp!0G$DcmVxtUx)HuLL%f2^SZ0000wf$G4JKOoktgZJ?zE!WA1}@x6XaGje2#lc$qNJI0%^d%ViQHm-voX3~YOA4|-|z QEdYsoy85}Sb4q9e0M4XLXaE2J delta 235 zcmVgPp3u03{1p-Dx!h1VP88?5V zLn#0N0B{pEFGWNtrPexK|9H68VRL^u?C%S9X{Tp@QOV0zn(#3i=g%L6AX)CQl zL=4;d&@Z=ggXPL_+T*9y<8AtSnZ77?n^5|>Tmvj2o<{jZUPk*mtx0k(!C#L+Yk$c@ z`t!*r_r_X|r$vV0!4ZfHH9+43IsI!%-579N!yFnp;ADEYPat{xUCZ}$>fN28`++$B l(C*hHk)cnKum>05_Flmpp8En7^|b&1002ovPDHLkV1mL?Zo2>g diff --git a/plugins/soccer-scoreboard/test/golden-cards/256x128/recent.png b/plugins/soccer-scoreboard/test/golden-cards/256x128/recent.png index aa002f83bff29d5aa854bf0e120e23a6adb32b7a..a0d4c29e61b9ddb56b8225e4221c3154599b19d1 100644 GIT binary patch delta 164 zcmcc0)Xp?Pxt`&ir;B4q#jUs39J!7t@UUKpdiF1TTX#c?Wb3vq3(iz)`(BQ%E4awf$G4JKOoktgZJ?zE!WA1}@x6XaGje2#lc$qNJI0%^d%ViQHm-voX3~YOA4|-|z QEdYsoy85}Sb4q9e0M4XLXaE2J delta 235 zcmVgPp3u03{1p-Dx!h1VP88?5V zLn#0N0B{pEFGWNtrPexK|9H68VRL^u?C%S9X{Tp@QOV0zn(#3i=g%L6AX)CQl zL=4;d&@Z=ggXPL_+T*9y<8AtSnZ77?n^5|>Tmvj2o<{jZUPk*mtx0k(!C#L+Yk$c@ z`t!*r_r_X|r$vV0!4ZfHH9+43IsI!%-579N!yFnp;ADEYPat{xUCZ}$>fN28`++$B l(C*hHk)cnKum>05_Flmpp8En7^|b&1002ovPDHLkV1mL?Zo2>g diff --git a/plugins/soccer-scoreboard/test/golden-cards/256x128/upcoming.png b/plugins/soccer-scoreboard/test/golden-cards/256x128/upcoming.png index aa002f83bff29d5aa854bf0e120e23a6adb32b7a..a0d4c29e61b9ddb56b8225e4221c3154599b19d1 100644 GIT binary patch delta 164 zcmcc0)Xp?Pxt`&ir;B4q#jUs39J!7t@UUKpdiF1TTX#c?Wb3vq3(iz)`(BQ%E4awf$G4JKOoktgZJ?zE!WA1}@x6XaGje2#lc$qNJI0%^d%ViQHm-voX3~YOA4|-|z QEdYsoy85}Sb4q9e0M4XLXaE2J delta 235 zcmVgPp3u03{1p-Dx!h1VP88?5V zLn#0N0B{pEFGWNtrPexK|9H68VRL^u?C%S9X{Tp@QOV0zn(#3i=g%L6AX)CQl zL=4;d&@Z=ggXPL_+T*9y<8AtSnZ77?n^5|>Tmvj2o<{jZUPk*mtx0k(!C#L+Yk$c@ z`t!*r_r_X|r$vV0!4ZfHH9+43IsI!%-579N!yFnp;ADEYPat{xUCZ}$>fN28`++$B l(C*hHk)cnKum>05_Flmpp8En7^|b&1002ovPDHLkV1mL?Zo2>g diff --git a/plugins/soccer-scoreboard/test/golden-cards/64x32/live.png b/plugins/soccer-scoreboard/test/golden-cards/64x32/live.png index 2eff7ee21286605921402fc74714dd0a9f506eca..821870baafc18a249aa8f0e2fb0690587a5c3136 100644 GIT binary patch delta 140 zcmV;70CWHC0jL3xB!6#7L_t(|+U?TO3cw%;h2b2%|0}l(K@P*l7}>@4e^+S+DzygS zQ^pvV(CL*Pv|dAfILqu9VAl8RaJggoz$r5;T7NJ7e%^Y_;K|7B9>BM#J*DRuBkD`_ un?*(k>Dsiv*;2jcNhJUP0002^n-iO?eZfbL+28;G002ovP6b4+LSTX+*+ZBB delta 209 zcmV;?051Qi0qp^hB!9F?L_t(|+U=Ch4umiWgu(d!ugo4cHgyK5ChlS5d^cN)F#MPa zA^sZYqlhpwcTdkxkKMs|{sjBk;kbf_seR+e%=R0(PGNC+oGsXlli?vEz-$isd&L65 zlkKPVjf1qjX`z-Pf-c)rfmWd`YgKuJva=SqTd0x*qUX-x+)66dQztMy&_eqId*H}{ zqp##|26QBRapb(OAdhdeSW@l#9WW0x|7f|*gb+dqA%qY@h!^t#<`ACye|4l200000 LNkvXXu0mjft6XMA diff --git a/plugins/soccer-scoreboard/test/golden-cards/64x32/recent.png b/plugins/soccer-scoreboard/test/golden-cards/64x32/recent.png index 2eff7ee21286605921402fc74714dd0a9f506eca..821870baafc18a249aa8f0e2fb0690587a5c3136 100644 GIT binary patch delta 140 zcmV;70CWHC0jL3xB!6#7L_t(|+U?TO3cw%;h2b2%|0}l(K@P*l7}>@4e^+S+DzygS zQ^pvV(CL*Pv|dAfILqu9VAl8RaJggoz$r5;T7NJ7e%^Y_;K|7B9>BM#J*DRuBkD`_ un?*(k>Dsiv*;2jcNhJUP0002^n-iO?eZfbL+28;G002ovP6b4+LSTX+*+ZBB delta 209 zcmV;?051Qi0qp^hB!9F?L_t(|+U=Ch4umiWgu(d!ugo4cHgyK5ChlS5d^cN)F#MPa zA^sZYqlhpwcTdkxkKMs|{sjBk;kbf_seR+e%=R0(PGNC+oGsXlli?vEz-$isd&L65 zlkKPVjf1qjX`z-Pf-c)rfmWd`YgKuJva=SqTd0x*qUX-x+)66dQztMy&_eqId*H}{ zqp##|26QBRapb(OAdhdeSW@l#9WW0x|7f|*gb+dqA%qY@h!^t#<`ACye|4l200000 LNkvXXu0mjft6XMA diff --git a/plugins/soccer-scoreboard/test/golden-cards/64x32/upcoming.png b/plugins/soccer-scoreboard/test/golden-cards/64x32/upcoming.png index 2eff7ee21286605921402fc74714dd0a9f506eca..821870baafc18a249aa8f0e2fb0690587a5c3136 100644 GIT binary patch delta 140 zcmV;70CWHC0jL3xB!6#7L_t(|+U?TO3cw%;h2b2%|0}l(K@P*l7}>@4e^+S+DzygS zQ^pvV(CL*Pv|dAfILqu9VAl8RaJggoz$r5;T7NJ7e%^Y_;K|7B9>BM#J*DRuBkD`_ un?*(k>Dsiv*;2jcNhJUP0002^n-iO?eZfbL+28;G002ovP6b4+LSTX+*+ZBB delta 209 zcmV;?051Qi0qp^hB!9F?L_t(|+U=Ch4umiWgu(d!ugo4cHgyK5ChlS5d^cN)F#MPa zA^sZYqlhpwcTdkxkKMs|{sjBk;kbf_seR+e%=R0(PGNC+oGsXlli?vEz-$isd&L65 zlkKPVjf1qjX`z-Pf-c)rfmWd`YgKuJva=SqTd0x*qUX-x+)66dQztMy&_eqId*H}{ zqp##|26QBRapb(OAdhdeSW@l#9WW0x|7f|*gb+dqA%qY@h!^t#<`ACye|4l200000 LNkvXXu0mjft6XMA From 78f68b23f2a0cdb920faa4fa50b9822ca4c540ed Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:26:33 -0400 Subject: [PATCH 2/4] refactor(scroll): route every scrolling plugin through the shared resolver Eleven plugins each hand-rolled scroll configuration and disagreed with one another about what identical config meant. odds-ticker ranked the deprecated scroll_pixels_per_second ABOVE the documented scroll_speed/scroll_delay pair, and because that key carries a schema default the documented settings were dead for every user (#408). ledmatrix-leaderboard read the same key only as a fallback, so the same config produced different speeds in the two plugins. stock-news derived px/frame through its own arithmetic. Nobody was wrong locally; they were just eleven answers to one question. All of them now call src.common.scroll_config.configure(), which resolves every supported config shape in one place, snaps the speed to one the panel can render in whole pixels, and reports the frame hold needed to keep slow speeds crisp. Identical config now means an identical speed everywhere. Each plugin keeps its original logic as a fallback behind try: from src.common import scroll_config as _scroll_config except ImportError: _scroll_config = None because plugins update independently of the core and must keep working against one that predates the helper. Where the legacy block sits inline in __init__ the shared call runs after it and wins, rather than re-indenting logic other config shapes still depend on; those plugins log a line saying so, because two scroll speeds in the journal with no indication which took effect is exactly the confusion this change exists to remove. Plugins pass settings.frame_hold to set_scrolling_state(True, ...) when they begin scrolling. The hold cannot be applied once at construction: plugins share one display manager, and it is reset whenever any other plugin finishes its scroll. Verified on hardware (Pi 4, 2x128x64, 100Hz). Four enabled plugins resolve through the helper with no tracebacks, and ledmatrix-stocks at 50 px/s measures 50.0 fps, median 20.00ms -- exactly two refreshes per frame, with the panel still refreshing at 100Hz so flicker is unchanged. Requires core support: ChuckBuilds/LEDMatrix#523. Without it every plugin takes the fallback path and behaves exactly as before. Co-Authored-By: Claude Opus 5 --- plugins.json | 22 +++--- plugins/f1-scoreboard/manifest.json | 2 +- plugins/f1-scoreboard/scroll_display.py | 31 +++++++++ plugins/ledmatrix-elections/manager.py | 46 ++++++++++++- plugins/ledmatrix-elections/manifest.json | 2 +- plugins/ledmatrix-leaderboard/manager.py | 38 ++++++++++- plugins/ledmatrix-leaderboard/manifest.json | 2 +- plugins/ledmatrix-stocks/manager.py | 76 ++++++++++++++++----- plugins/ledmatrix-stocks/manifest.json | 2 +- plugins/march-madness/manager.py | 30 ++++++++ plugins/march-madness/manifest.json | 2 +- plugins/news/manager.py | 34 ++++++++- plugins/news/manifest.json | 2 +- plugins/nfl-draft/manager.py | 30 ++++++++ plugins/nfl-draft/manifest.json | 2 +- plugins/odds-ticker/manager.py | 38 ++++++++++- plugins/odds-ticker/manifest.json | 2 +- plugins/stock-news/manager.py | 41 ++++++++++- plugins/stock-news/manifest.json | 2 +- plugins/text-display/manager.py | 38 ++++++++++- plugins/text-display/manifest.json | 2 +- plugins/ufc-scoreboard/manifest.json | 2 +- plugins/ufc-scoreboard/scroll_display.py | 26 +++++++ 23 files changed, 427 insertions(+), 45 deletions(-) diff --git a/plugins.json b/plugins.json index 5258f2df..e4778296 100644 --- a/plugins.json +++ b/plugins.json @@ -215,7 +215,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.8.7" + "latest_version": "1.8.8" }, { "id": "football-scoreboard", @@ -412,7 +412,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.3.3" + "latest_version": "1.3.4" }, { "id": "ledmatrix-flights", @@ -463,7 +463,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.1.0" + "latest_version": "1.1.1" }, { "id": "masters-tournament", @@ -557,7 +557,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.4.0" + "latest_version": "1.4.1" }, { "id": "on-air", @@ -605,7 +605,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.4.3", + "latest_version": "1.4.4", "icon": "fas fa-football-ball" }, { @@ -633,7 +633,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.4.0" + "latest_version": "1.4.1" }, { "id": "of-the-day", @@ -809,7 +809,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "2.6.1" + "latest_version": "2.6.2" }, { "id": "stocks", @@ -832,7 +832,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "2.9.0", + "latest_version": "2.9.1", "icon": "fas fa-chart-line" }, { @@ -856,7 +856,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.1.6" + "latest_version": "1.1.7" }, { "id": "tide-display", @@ -905,7 +905,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.8.2", + "latest_version": "1.8.3", "icon": "fas fa-fist-raised" }, { @@ -1001,7 +1001,7 @@ "last_updated": "2026-07-31", "verified": false, "screenshot": "", - "latest_version": "1.2.2" + "latest_version": "1.2.3" }, { "id": "ledmatrix-dresden-departures", diff --git a/plugins/f1-scoreboard/manifest.json b/plugins/f1-scoreboard/manifest.json index e7ab883d..ba31eae8 100644 --- a/plugins/f1-scoreboard/manifest.json +++ b/plugins/f1-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "f1-scoreboard", "name": "F1 Scoreboard", - "version": "1.8.7", + "version": "1.8.8", "author": "ChuckBuilds", "class_name": "F1ScoreboardPlugin", "entry_point": "manager.py", diff --git a/plugins/f1-scoreboard/scroll_display.py b/plugins/f1-scoreboard/scroll_display.py index 04cca24b..d22bfbb7 100644 --- a/plugins/f1-scoreboard/scroll_display.py +++ b/plugins/f1-scoreboard/scroll_display.py @@ -18,6 +18,14 @@ "ScrollHelper not available, scrolling disabled: %s", _scroll_import_err) +try: + # Shared scroll pacing: one resolver for every plugin, so identical config + # means the same speed everywhere, and slow speeds get the frame hold that + # keeps them crisp. + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + logger = logging.getLogger(__name__) @@ -29,6 +37,11 @@ class ScrollDisplay: and manages scroll state. """ + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings.""" + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def __init__(self, display_manager, config: Optional[Dict[str, Any]] = None, custom_logger: Optional[logging.Logger] = None, global_config: Optional[Dict[str, Any]] = None): @@ -69,6 +82,19 @@ def __init__(self, display_manager, config: Optional[Dict[str, Any]] = None, buffer=self.display_width ) + # Shared resolver wins over the setup above, which stays as the + # fallback for cores that predate it. + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.global_config, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + else: + self._scroll_settings = None + # Honor the global smooth-scrolling FPS target (older cores lack the setter) target_fps = self.global_config.get('target_fps') or self.global_config.get('scroll_target_fps') try: @@ -188,6 +214,11 @@ class ScrollDisplayManager: Manages multiple ScrollDisplay instances, one per display mode. """ + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings.""" + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def __init__(self, display_manager, config: Optional[Dict[str, Any]] = None, custom_logger: Optional[logging.Logger] = None, global_config: Optional[Dict[str, Any]] = None): diff --git a/plugins/ledmatrix-elections/manager.py b/plugins/ledmatrix-elections/manager.py index 0df83631..79de6f17 100644 --- a/plugins/ledmatrix-elections/manager.py +++ b/plugins/ledmatrix-elections/manager.py @@ -17,6 +17,14 @@ from PIL import Image from src.plugin_system.base_plugin import BasePlugin +try: + # Shared scroll pacing: resolves speed from any supported config shape, + # snaps it to a speed the panel can show in whole pixels, and reports the + # frame hold that keeps slow speeds crisp. Core docs: SCROLL_PERFORMANCE.md + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + from src.common.scroll_helper import ScrollHelper # Plugin-local imports (the plugin directory is on sys.path at load time, but be @@ -92,6 +100,19 @@ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_man self.scroll_helper.set_dynamic_duration_settings( enabled=True, min_duration=int(self.display_duration), max_duration=300 ) + + # Shared resolver wins over the setup above, which stays as the + # fallback for cores that predate it. + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.config.get('global', {}) or {}, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + else: + self._scroll_settings = None # Honor the global smooth-scrolling FPS target (older cores lack the setter). # The convention (news, leaderboard) is a `global` section in the plugin config. self.global_config = config.get('global', {}) or {} @@ -174,6 +195,19 @@ def on_config_change(self, new_config: dict) -> None: self.scroll_helper.set_dynamic_duration_settings( enabled=True, min_duration=int(self.display_duration), max_duration=300 ) + + # Shared resolver wins over the setup above, which stays as the + # fallback for cores that predate it. + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.config.get('global', {}) or {}, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + else: + self._scroll_settings = None # Refresh the FPS target from the (possibly edited) global section. self.global_config = config.get('global', {}) or {} target_fps = self.global_config.get('target_fps') or self.global_config.get('scroll_target_fps', 100) @@ -199,6 +233,15 @@ def on_config_change(self, new_config: dict) -> None: # -- Update loop -------------------------------------------------------- + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings. + + 1 without the shared helper, which is the old behaviour: a new frame + every panel refresh. + """ + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def update(self) -> None: now = time.time() if self._last_update and (now - self._last_update) < self.update_interval: @@ -485,7 +528,8 @@ def _display_ticker(self, force_clear: bool) -> bool: self.logger.debug("reset_scroll failed: %s", e) try: - self.display_manager.set_scrolling_state(True) + self.display_manager.set_scrolling_state( + True, frame_hold=self._scroll_frame_hold()) except Exception as e: self.logger.debug("set_scrolling_state failed: %s", e) diff --git a/plugins/ledmatrix-elections/manifest.json b/plugins/ledmatrix-elections/manifest.json index 911e6da6..94a2a707 100644 --- a/plugins/ledmatrix-elections/manifest.json +++ b/plugins/ledmatrix-elections/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-elections", "name": "Election Results", - "version": "1.2.2", + "version": "1.2.3", "author": "rpierce99", "description": "Live election results: a scrolling ticker of important races plus a full-screen interrupt when a race is newly called. Auto-activates for your state's primary + general (dormant otherwise), drains stale calls from the ticker, and survives restarts. Filterable to your state. NYT baseline provider + optional California SoS county/city rollup.", "entry_point": "manager.py", diff --git a/plugins/ledmatrix-leaderboard/manager.py b/plugins/ledmatrix-leaderboard/manager.py index 29121b8e..27c8c32d 100644 --- a/plugins/ledmatrix-leaderboard/manager.py +++ b/plugins/ledmatrix-leaderboard/manager.py @@ -22,6 +22,14 @@ from PIL import Image from src.plugin_system.base_plugin import BasePlugin +try: + # Shared scroll pacing: resolves speed from any supported config shape, + # snaps it to a speed the panel can show in whole pixels, and reports the + # frame hold that keeps slow speeds crisp. Core docs: SCROLL_PERFORMANCE.md + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + from src.common.scroll_helper import ScrollHelper from league_config import LeagueConfig @@ -140,6 +148,24 @@ def __init__(self, plugin_id: str, config: Dict[str, Any], self.scroll_helper.target_fps = max(30.0, min(200.0, target_fps)) self.scroll_helper.frame_time_target = 1.0 / self.scroll_helper.target_fps self.logger.debug(f"Target FPS set to: {self.scroll_helper.target_fps} FPS (using fallback method)") + + # The shared resolver takes precedence over the block above, which is + # kept as the fallback for cores that predate it. Running both costs a + # few microseconds once at construction and avoids re-indenting logic + # that other config shapes still depend on. + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.global_config, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + self.logger.info( + "Scroll pacing came from the shared resolver; any scroll speed " + "logged above this line by the legacy path was superseded") + else: + self._scroll_settings = None self.scroll_helper.set_dynamic_duration_settings( enabled=self.dynamic_duration_enabled, @@ -192,6 +218,15 @@ def __init__(self, plugin_id: str, config: Dict[str, Any], else: self.logger.warning("No leagues are enabled - leaderboard will not display any data") + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings. + + 1 without the shared helper, which is the old behaviour: a new frame + every panel refresh. + """ + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def update(self, force: bool = False) -> None: """ Update standings data for all enabled leagues. @@ -307,7 +342,8 @@ def display(self, force_clear: bool = False) -> None: return # Signal scrolling state - self.display_manager.set_scrolling_state(True) + self.display_manager.set_scrolling_state( + True, frame_hold=self._scroll_frame_hold()) self.display_manager.process_deferred_updates() # Update scroll position using the scroll helper diff --git a/plugins/ledmatrix-leaderboard/manifest.json b/plugins/ledmatrix-leaderboard/manifest.json index 5a4c03a1..f71f9b16 100644 --- a/plugins/ledmatrix-leaderboard/manifest.json +++ b/plugins/ledmatrix-leaderboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-leaderboard", "name": "Sports Leaderboard", - "version": "1.3.3", + "version": "1.3.4", "description": "Displays scrolling leaderboards and standings for multiple sports leagues including NFL, NBA, MLB, NCAA Football, NCAA Basketball, and more", "author": "ChuckBuilds", "entry_point": "manager.py", diff --git a/plugins/ledmatrix-stocks/manager.py b/plugins/ledmatrix-stocks/manager.py index 73ec12e2..0de58c05 100644 --- a/plugins/ledmatrix-stocks/manager.py +++ b/plugins/ledmatrix-stocks/manager.py @@ -11,6 +11,14 @@ from src.plugin_system.base_plugin import BasePlugin +try: + # Shared scroll pacing: resolves speed from config, snaps it to a speed the + # panel can show in whole pixels, and sets the frame hold that makes slow + # speeds crisp. See docs/SCROLL_PERFORMANCE.md in the core repo. + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + # Import our modular components from data_fetcher import StockDataFetcher from display_renderer import StockDisplayRenderer @@ -71,12 +79,6 @@ def __init__(self, plugin_id: str, config: Dict[str, Any], # Initialize scroll helper self.scroll_helper = self.display_renderer.get_scroll_helper() - # Convert pixels per frame to pixels per second for ScrollHelper - # scroll_speed is pixels per frame, scroll_delay is seconds per frame - # pixels per second = pixels per frame / seconds per frame - pixels_per_second = self.config_manager.scroll_speed / self.config_manager.scroll_delay if self.config_manager.scroll_delay > 0 else self.config_manager.scroll_speed * 100 - self.scroll_helper.set_scroll_speed(pixels_per_second) - self.scroll_helper.set_scroll_delay(self.config_manager.scroll_delay) # Configure dynamic duration settings self.scroll_helper.set_dynamic_duration_settings( @@ -86,21 +88,59 @@ def __init__(self, plugin_id: str, config: Dict[str, Any], buffer=self.config_manager.duration_buffer ) - # Honor the global smooth-scrolling FPS target (older cores lack the setter). - # The convention (news, leaderboard) is a `global` section in the plugin config. self.global_config = config.get('global', {}) or {} - global_config = self.global_config - target_fps = global_config.get('target_fps') or global_config.get('scroll_target_fps', 100) + self._configure_scroll(config) + + self.logger.info("Stock ticker plugin initialized - %dx%d", + self.display_width, self.display_height) + + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings. + + 1 on cores without the shared helper, which is the old behaviour: a new + frame every refresh. + """ + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + + def _configure_scroll(self, config): + """Apply scroll pacing via the shared helper, or the legacy path. + + The helper resolves every config shape in one place, snaps the speed to + one the panel can render in whole pixels, and sets the frame hold that + lets speeds below one pixel per refresh stay crisp. Passing + display_manager matters: without it the hold cannot be applied and slow + speeds fall back to fractional pixels. + """ + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=config, + global_config=self.global_config, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + return + + # Legacy path for cores without src.common.scroll_config. Kept because + # plugins update independently of the core and must not break on one + # that has not been upgraded yet. + self._scroll_settings = None + cfg = self.config_manager + pixels_per_second = (cfg.scroll_speed / cfg.scroll_delay + if cfg.scroll_delay > 0 else cfg.scroll_speed * 100) + self.scroll_helper.set_scroll_speed(pixels_per_second) + self.scroll_helper.set_scroll_delay(cfg.scroll_delay) + target_fps = (self.global_config.get('target_fps') + or self.global_config.get('scroll_target_fps', 100)) if hasattr(self.scroll_helper, 'set_target_fps'): self.scroll_helper.set_target_fps(target_fps) - self.logger.info(f"Target FPS set to: {target_fps}") else: self.scroll_helper.target_fps = max(30.0, min(200.0, target_fps)) self.scroll_helper.frame_time_target = 1.0 / self.scroll_helper.target_fps - - self.logger.info("Stock ticker plugin initialized - %dx%d", - self.display_width, self.display_height) - + self.logger.info( + "Scroll configured (legacy path): %.1f px/s", pixels_per_second) + def update(self) -> None: """Update stock and crypto data.""" current_time = time.time() @@ -147,7 +187,11 @@ def _display_scrolling(self, force_clear: bool = False) -> None: self.scroll_complete = False # Signal scrolling state - self.display_manager.set_scrolling_state(True) + # Pass the frame hold every time scrolling starts, not once at + # construction: plugins share one display manager, so a hold set at + # init is wiped as soon as any other plugin finishes its scroll. + self.display_manager.set_scrolling_state( + True, frame_hold=self._scroll_frame_hold()) # Guard for display managers that don't implement deferred updates # (e.g. the plugin-safety harness's bounds-checking display manager). if hasattr(self.display_manager, "process_deferred_updates"): diff --git a/plugins/ledmatrix-stocks/manifest.json b/plugins/ledmatrix-stocks/manifest.json index ad2cb2c2..88482fb9 100644 --- a/plugins/ledmatrix-stocks/manifest.json +++ b/plugins/ledmatrix-stocks/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-stocks", "name": "Stock & Crypto Ticker", - "version": "2.9.0", + "version": "2.9.1", "description": "Displays stock tickers with prices, changes, and optional charts for stocks and cryptocurrencies. Supports scroll and switch display modes.", "author": "LEDMatrix Team", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-stocks", diff --git a/plugins/march-madness/manager.py b/plugins/march-madness/manager.py index 91e3aebc..85379344 100644 --- a/plugins/march-madness/manager.py +++ b/plugins/march-madness/manager.py @@ -18,6 +18,14 @@ from urllib3.util.retry import Retry from src.plugin_system.base_plugin import BasePlugin +try: + # Shared scroll pacing: resolves speed from any supported config shape, + # snaps it to a speed the panel can show in whole pixels, and reports the + # frame hold that keeps slow speeds crisp. Core docs: SCROLL_PERFORMANCE.md + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + try: from src.common.scroll_helper import ScrollHelper @@ -156,6 +164,19 @@ def __init__( else: self.scroll_helper.target_fps = max(30.0, min(200.0, self.target_fps)) self.scroll_helper.frame_time_target = 1.0 / self.scroll_helper.target_fps + + # Shared resolver wins over the setup above, which stays as the + # fallback for cores that predate it. + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.config.get('global', {}) or {}, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + else: + self._scroll_settings = None self.scroll_helper.set_dynamic_duration_settings( enabled=self.dynamic_duration_enabled, min_duration=self.min_duration, @@ -746,6 +767,15 @@ def _create_ticker_image(self) -> None: # Plugin lifecycle # ------------------------------------------------------------------ + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings. + + 1 without the shared helper, which is the old behaviour: a new frame + every panel refresh. + """ + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def update(self) -> None: """Fetch and process tournament data.""" if not self.enabled: diff --git a/plugins/march-madness/manifest.json b/plugins/march-madness/manifest.json index 14b91e86..4559d6cc 100644 --- a/plugins/march-madness/manifest.json +++ b/plugins/march-madness/manifest.json @@ -1,7 +1,7 @@ { "id": "march-madness", "name": "March Madness", - "version": "1.1.0", + "version": "1.1.1", "description": "NCAA March Madness tournament bracket tracker with round branding, seeded matchups, live scores, and upset highlighting", "author": "ChuckBuilds", "category": "sports", diff --git a/plugins/news/manager.py b/plugins/news/manager.py index 12db5316..d9a74708 100644 --- a/plugins/news/manager.py +++ b/plugins/news/manager.py @@ -30,6 +30,14 @@ from PIL import Image, ImageDraw, ImageFont from src.plugin_system.base_plugin import BasePlugin +try: + # Shared scroll pacing: resolves speed from any supported config shape, + # snaps it to a speed the panel can show in whole pixels, and reports the + # frame hold that keeps slow speeds crisp. Core docs: SCROLL_PERFORMANCE.md + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + from src.common.scroll_helper import ScrollHelper from src.common.logo_helper import LogoHelper @@ -580,6 +588,15 @@ def _page_width_budget(self) -> Optional[float]: # would produce pages that can't hold a single headline. return max(float(self.display_width * 2), pixels_per_second * usable_seconds) + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings. + + 1 without the shared helper, which is the old behaviour: a new frame + every panel refresh. + """ + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def _configure_scroll_settings(self) -> None: """ Configure scroll helper with current settings. @@ -593,6 +610,20 @@ def _configure_scroll_settings(self) -> None: # Determine if we should use frame-based scrolling # Check if scroll_pixels_per_second is None (frame-based) or set (time-based) + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.global_config, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + return + + # Legacy path for cores without src.common.scroll_config. Plugins + # update independently of the core, so this must keep working. + self._scroll_settings = None + display_config = self.global_config.get('display', {}) use_frame_based = (self.scroll_pixels_per_second is None and display_config and @@ -1070,7 +1101,8 @@ def display(self, display_mode: str = None, force_clear: bool = False) -> None: self._cycle_complete = False # Signal scrolling state - self.display_manager.set_scrolling_state(True) + self.display_manager.set_scrolling_state( + True, frame_hold=self._scroll_frame_hold()) self.display_manager.process_deferred_updates() # Update scroll position using the scroll helper diff --git a/plugins/news/manifest.json b/plugins/news/manifest.json index c834f502..3c491f41 100644 --- a/plugins/news/manifest.json +++ b/plugins/news/manifest.json @@ -1,7 +1,7 @@ { "id": "news", "name": "News Ticker", - "version": "1.4.0", + "version": "1.4.1", "description": "Displays scrolling news headlines from RSS feeds including sports news from ESPN, NCAA updates, and custom RSS sources", "author": "ChuckBuilds", "category": "content", diff --git a/plugins/nfl-draft/manager.py b/plugins/nfl-draft/manager.py index 8001caae..33dc9066 100644 --- a/plugins/nfl-draft/manager.py +++ b/plugins/nfl-draft/manager.py @@ -28,6 +28,14 @@ from PIL import Image, ImageDraw, ImageFont from src.plugin_system.base_plugin import BasePlugin +try: + # Shared scroll pacing: resolves speed from any supported config shape, + # snaps it to a speed the panel can show in whole pixels, and reports the + # frame hold that keeps slow speeds crisp. Core docs: SCROLL_PERFORMANCE.md + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + from src.common.scroll_helper import ScrollHelper from src.common.logo_helper import LogoHelper from src.common.api_helper import APIHelper @@ -132,6 +140,19 @@ def _load_config(self) -> None: if hasattr(self.scroll_helper, 'set_scroll_delay'): self.scroll_helper.set_scroll_delay(self.scroll_delay) + # Shared resolver wins over the setup above, which stays as the + # fallback for cores that predate it. + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.config.get('global', {}) or {}, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + else: + self._scroll_settings = None + # Honor the global smooth-scrolling FPS target (older cores lack the setter). # The convention (news, leaderboard) is a `global` section in the plugin config. self.global_config = self.config.get('global', {}) or {} @@ -1124,6 +1145,15 @@ def _load_nfl_draft_logo(self) -> Optional[Image.Image]: self.logger.error(f"Error loading NFL Draft logo: {e}") return None + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings. + + 1 without the shared helper, which is the old behaviour: a new frame + every panel refresh. + """ + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def update(self) -> None: """ Fetch/update draft data from ESPN API. diff --git a/plugins/nfl-draft/manifest.json b/plugins/nfl-draft/manifest.json index b3b80fc3..8f7f06eb 100644 --- a/plugins/nfl-draft/manifest.json +++ b/plugins/nfl-draft/manifest.json @@ -1,7 +1,7 @@ { "id": "nfl-draft", "name": "NFL Draft", - "version": "1.4.3", + "version": "1.4.4", "author": "ChuckBuilds", "description": "Displays projected NFL draft picks from ESPN with live draft tracking support during the annual NFL Draft event. Includes simulate_live mode to replay a completed draft using real ESPN core API data. Shows team logos, player names, positions, and pick numbers in a scrolling display.", "entry_point": "manager.py", diff --git a/plugins/odds-ticker/manager.py b/plugins/odds-ticker/manager.py index 4ded99c4..0930dde8 100644 --- a/plugins/odds-ticker/manager.py +++ b/plugins/odds-ticker/manager.py @@ -66,6 +66,14 @@ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_man self.cache_manager = cache_manager self.plugin_manager = plugin_manager +try: + # Shared scroll pacing: resolves speed from any supported config + # shape, snaps it to a speed the panel can show in whole pixels, and + # reports the frame hold that keeps slow speeds crisp. + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + # Import BaseOddsManager from LEDMatrix core try: from src.base_odds_manager import BaseOddsManager @@ -366,6 +374,24 @@ def get_config(section, key, default, old_key=None): self.scroll_helper.target_fps = max(30.0, min(200.0, self.target_fps)) self.scroll_helper.frame_time_target = 1.0 / self.scroll_helper.target_fps self.logger.debug(f"Target FPS set to: {self.scroll_helper.target_fps} FPS (using fallback method)") + + # The shared resolver takes precedence over the block above, which is + # kept as the fallback for cores that predate it. Running both costs a + # few microseconds once at construction and avoids re-indenting logic + # that other config shapes still depend on. + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.global_config, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + self.logger.info( + "Scroll pacing came from the shared resolver; any scroll speed " + "logged above this line by the legacy path was superseded") + else: + self._scroll_settings = None self.scroll_helper.set_dynamic_duration_settings( enabled=self.dynamic_duration_enabled, min_duration=self.min_duration, @@ -2602,6 +2628,15 @@ def on_config_change(self, new_config: Dict[str, Any]) -> None: self.show_channel_logos = new_show_logos self.logger.info(f"Show channel logos updated to: {self.show_channel_logos}") + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings. + + 1 without the shared helper, which is the old behaviour: a new frame + every panel refresh. + """ + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def update(self): """Update odds ticker data.""" logger.debug("Entering update method") @@ -2947,7 +2982,8 @@ def create_image(): # Signal scrolling state if hasattr(self.display_manager, 'set_scrolling_state'): if self.loop or not self.scroll_helper.is_scroll_complete(): - self.display_manager.set_scrolling_state(True) + self.display_manager.set_scrolling_state( + True, frame_hold=self._scroll_frame_hold()) else: self.display_manager.set_scrolling_state(False) diff --git a/plugins/odds-ticker/manifest.json b/plugins/odds-ticker/manifest.json index 1adc2b2e..1ab7ffba 100644 --- a/plugins/odds-ticker/manifest.json +++ b/plugins/odds-ticker/manifest.json @@ -1,7 +1,7 @@ { "id": "odds-ticker", "name": "Odds Ticker", - "version": "1.4.0", + "version": "1.4.1", "description": "Displays scrolling odds and betting lines for upcoming games across multiple sports leagues including NFL, NBA, MLB, NCAA Football, and more", "author": "ChuckBuilds", "category": "sports", diff --git a/plugins/stock-news/manager.py b/plugins/stock-news/manager.py index 60b7888e..b86b0220 100644 --- a/plugins/stock-news/manager.py +++ b/plugins/stock-news/manager.py @@ -30,6 +30,14 @@ import re from datetime import datetime from pathlib import Path +try: + # Shared scroll pacing: resolves speed from any supported config shape, + # snaps it to a speed the panel can show in whole pixels, and reports the + # frame hold that keeps slow speeds crisp. Core docs: SCROLL_PERFORMANCE.md + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + from typing import Dict, Any, List, Optional, Tuple from PIL import Image, ImageDraw, ImageFont from requests.adapters import HTTPAdapter @@ -310,8 +318,36 @@ def _load_fonts(self) -> dict: return {'headline': main_font, 'symbol': main_font, 'publisher': publisher_font, 'age': age_font} + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings. + + 1 without the shared helper, which is the old behaviour: a new frame + every panel refresh. + """ + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def _configure_scroll_settings(self) -> None: - """Apply scroll speed / FPS to ScrollHelper using frame-based scrolling.""" + """Apply scroll speed / FPS to the ScrollHelper. + + Prefers the shared resolver so this plugin agrees with every other one + about what a given config means, and so slow speeds get the frame hold + that keeps them crisp. + """ + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.global_config, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + return + + # Legacy path for cores without src.common.scroll_config. Plugins + # update independently of the core, so this must keep working. + self._scroll_settings = None + if 'scroll_pixels_per_second' in self.global_config: pps = float(self.global_config['scroll_pixels_per_second']) elif self.scroll_delay and self.scroll_delay > 0: @@ -816,7 +852,8 @@ def display(self, display_mode: Optional[str] = None, force_clear: bool = False) self.scroll_helper.reset_scroll() self._cycle_complete = False - self.display_manager.set_scrolling_state(True) + self.display_manager.set_scrolling_state( + True, frame_hold=self._scroll_frame_hold()) self.display_manager.process_deferred_updates() self.scroll_helper.update_scroll_position() diff --git a/plugins/stock-news/manifest.json b/plugins/stock-news/manifest.json index 3105aa51..e20cfbdf 100644 --- a/plugins/stock-news/manifest.json +++ b/plugins/stock-news/manifest.json @@ -1,7 +1,7 @@ { "id": "stock-news", "name": "Stock News Ticker", - "version": "2.6.1", + "version": "2.6.2", "author": "ChuckBuilds", "description": "Live stock headlines via Yahoo Finance search API with RSS fallback, company logos, configurable display styles (logo+ticker, ticker only, logo only), Vegas scroll integration, and per-day/hour request budgeting", "entry_point": "manager.py", diff --git a/plugins/text-display/manager.py b/plugins/text-display/manager.py index 780e378a..6f5c122c 100644 --- a/plugins/text-display/manager.py +++ b/plugins/text-display/manager.py @@ -22,6 +22,14 @@ from pathlib import Path from src.plugin_system.base_plugin import BasePlugin +try: + # Shared scroll pacing: resolves speed from any supported config shape, + # snaps it to a speed the panel can show in whole pixels, and reports the + # frame hold that keeps slow speeds crisp. Core docs: SCROLL_PERFORMANCE.md + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + from src.common.scroll_helper import ScrollHelper try: @@ -177,6 +185,24 @@ def __init__(self, plugin_id: str, config: Dict[str, Any], # Calculate pixels per second for logging (even though we use frame-based mode) pixels_per_second = self.scroll_speed / self.scroll_delay if self.scroll_delay > 0 else self.scroll_speed * 100 self.logger.info(f"Scroll settings: {self.scroll_speed} px/frame, {self.scroll_delay}s delay = {pixels_per_second:.1f} px/s, target FPS: {self.target_fps}") + + # The shared resolver takes precedence over the block above, which is + # kept as the fallback for cores that predate it. Running both costs a + # few microseconds once at construction and avoids re-indenting logic + # that other config shapes still depend on. + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.global_config, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + self.logger.info( + "Scroll pacing came from the shared resolver; any scroll speed " + "logged above this line by the legacy path was superseded") + else: + self._scroll_settings = None self.scroll_helper.set_dynamic_duration_settings( enabled=True, # Honor the documented display_duration setting as the on-screen floor. @@ -418,6 +444,15 @@ def _create_text_cache(self): self.logger.error(f"Failed to create text cache: {e}", exc_info=True) self.text_image_cache = None + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings. + + 1 without the shared helper, which is the old behaviour: a new frame + every panel refresh. + """ + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def update(self) -> None: """Update scroll position if scrolling is enabled using ScrollHelper.""" if not self.scroll_enabled or self.text_width <= self.display_manager.matrix.width: @@ -504,7 +539,8 @@ def display(self, force_clear: bool = False) -> None: if hasattr(self.display_manager, 'set_scrolling_state'): # Only signal scrolling if not complete (or if looping and will reset) if not self.scroll_helper.is_scroll_complete() or (self.scroll_loop and self.scroll_helper.is_scroll_complete()): - self.display_manager.set_scrolling_state(True) + self.display_manager.set_scrolling_state( + True, frame_hold=self._scroll_frame_hold()) else: # One-shot mode and complete - stop scrolling self.display_manager.set_scrolling_state(False) diff --git a/plugins/text-display/manifest.json b/plugins/text-display/manifest.json index b1b52cb3..bc73793f 100644 --- a/plugins/text-display/manifest.json +++ b/plugins/text-display/manifest.json @@ -1,7 +1,7 @@ { "id": "text-display", "name": "Text Display", - "version": "1.1.6", + "version": "1.1.7", "author": "ChuckBuilds", "description": "Display custom scrolling or static text with configurable fonts, colors, and scroll speed. Perfect for announcements, messages, or custom displays.", "category": "display", diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index 32db04b6..8d9d5aa1 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ufc-scoreboard", "name": "UFC Scoreboard", - "version": "1.8.2", + "version": "1.8.3", "author": "LegoGuy1000", "contributors": [ { diff --git a/plugins/ufc-scoreboard/scroll_display.py b/plugins/ufc-scoreboard/scroll_display.py index 55cc0f83..78a609bd 100644 --- a/plugins/ufc-scoreboard/scroll_display.py +++ b/plugins/ufc-scoreboard/scroll_display.py @@ -22,6 +22,14 @@ except ImportError: ScrollHelper = None +try: + # Shared scroll pacing: one resolver for every plugin, so identical config + # means the same speed everywhere, and slow speeds get the frame hold that + # keeps them crisp. + from src.common import scroll_config as _scroll_config +except ImportError: # core predates the shared helper + _scroll_config = None + from fight_renderer import FightRenderer logger = logging.getLogger(__name__) @@ -42,6 +50,11 @@ class ScrollDisplayManager: # Path to UFC separator icon UFC_SEPARATOR_ICON = "assets/sports/ufc_logos/UFC.png" + def _scroll_frame_hold(self) -> int: + """Refreshes to hold each frame for, from the resolved scroll settings.""" + settings = getattr(self, "_scroll_settings", None) + return getattr(settings, "frame_hold", 1) if settings else 1 + def __init__( self, display_manager, @@ -144,6 +157,19 @@ def _configure_scroll_helper(self) -> None: pixels_per_frame = max(0.1, min(5.0, pixels_per_frame)) self.scroll_helper.set_scroll_speed(pixels_per_frame) + # Shared resolver wins over the setup above, which stays as the + # fallback for cores that predate it. + if _scroll_config is not None: + self._scroll_settings = _scroll_config.configure( + self.scroll_helper, + plugin_config=self.config, + global_config=self.global_config, + display_manager=self.display_manager, + plugin_logger=self.logger, + ) + else: + self._scroll_settings = None + effective_pps = pixels_per_frame / scroll_delay if scroll_delay > 0 else pixels_per_frame * 100 self.logger.info( From f1d47894a3439e4f963b4c747240051613751fdb Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:18:29 -0400 Subject: [PATCH 3/4] fix(odds-ticker): make the documented scroll settings reachable (#408) Two defects meant display_options.scroll_speed / scroll_delay -- the format the plugin documents and recommends -- could never take effect. The priority-1 branch read scroll_pixels_per_second into self rather than clearing it the way the display_config branch does. config_schema.json gives that deprecated key a default of 50.0 and schema defaults are merged into plugin config, so it was never None and use_frame_based() below was never True. Separately, use_frame_based() only ever inspected display_config, so even with that fixed the recommended shape still could not select frame-based mode. Both halves were needed. The visible symptom was the plugin logging its scroll configuration twice on startup, ~0.36s apart, the second line quietly overriding the first: Using display_options.scroll_speed=1.0 px/frame ... (frame-based mode) Using scroll_pixels_per_second: 50.0 px/s (time-based mode) with the second one being what actually ran. On a 100Hz panel that meant 0.5px per frame, so every second frame rendered identically, dirty tracking skipped the panel swap, and the ticker juddered. Editing scroll_speed had no effect, and deleting scroll_pixels_per_second only restored the schema default. Also takes the live-game check off the render path. display() called _get_current_update_interval() every frame, and its slow path reads the scoreboard cache from disk and parses JSON per enabled league -- producing a single ~15ms frame every few minutes, measurable as a stall mid-scroll. The interval is now memoised for 15s; the scoreboard re-check underneath is rate limited to 300s regardless, so live detection is unaffected. The neighbouring debug line used an f-string, so it called _has_live_games() on every skipped update even with debug logging off; it now uses %s args. Verified on hardware: odds-ticker selects frame-based mode with no time-based line following it, and resolves to 100.0 px/s at 1px per refresh. Co-Authored-By: Claude Opus 5 --- plugins.json | 2 +- plugins/odds-ticker/manager.py | 52 ++++++++++++++++++++++++++++--- plugins/odds-ticker/manifest.json | 2 +- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/plugins.json b/plugins.json index e4778296..82960ea7 100644 --- a/plugins.json +++ b/plugins.json @@ -633,7 +633,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.4.1" + "latest_version": "1.4.2" }, { "id": "of-the-day", diff --git a/plugins/odds-ticker/manager.py b/plugins/odds-ticker/manager.py index 0930dde8..fa78bacb 100644 --- a/plugins/odds-ticker/manager.py +++ b/plugins/odds-ticker/manager.py @@ -255,7 +255,14 @@ def get_config(section, key, default, old_key=None): # Priority 1: Current format - use display_options object self.scroll_speed = display_options.get('scroll_speed', 1.0) self.scroll_delay = display_options.get('scroll_delay', 0.02) - self.scroll_pixels_per_second = display_options.get('scroll_pixels_per_second') + # Must be None here, exactly as the display_config branch below + # does it. config_schema.json gives this deprecated key a default + # of 50.0, and schema defaults are merged into plugin config, so + # reading it on this path left it permanently non-None and + # use_frame_based below could never be True -- the documented + # scroll_speed/scroll_delay settings were unreachable for every + # user. See issue #408. + self.scroll_pixels_per_second = None self.logger.info(f"Using display_options.scroll_speed={self.scroll_speed} px/frame, display_options.scroll_delay={self.scroll_delay}s (frame-based mode)") elif display_config and ('scroll_speed' in display_config or 'scroll_delay' in display_config): # Old nested format: use display object for granular control @@ -336,9 +343,19 @@ def get_config(section, key, default, old_key=None): # Configure ScrollHelper with plugin settings # Check if we should use frame-based scrolling (new format) or time-based (old format) - use_frame_based = (self.scroll_pixels_per_second is None and - display_config and - ('scroll_speed' in display_config or 'scroll_delay' in display_config)) + # Accept either config shape. This previously consulted only + # display_config (the deprecated "display" block), so even with the + # fix above the recommended display_options format could never select + # frame-based mode. Both halves of #408 are needed. + use_frame_based = ( + self.scroll_pixels_per_second is None + and ( + (display_options and ('scroll_speed' in display_options + or 'scroll_delay' in display_options)) + or (display_config and ('scroll_speed' in display_config + or 'scroll_delay' in display_config)) + ) + ) if use_frame_based: # New format: use frame-based scrolling for finer control @@ -2730,7 +2747,25 @@ def _has_games_starting_soon(self) -> bool: return True return False + #: How long a computed update interval is reused for. display() asks every + #: frame, and the slow path below reads the scoreboard cache from disk and + #: parses JSON per enabled league -- on the render thread. That produced a + #: single ~15ms frame every few minutes, visible as a hitch mid-scroll. + #: 15s keeps live detection responsive; the scoreboard re-check underneath + #: is rate limited to _live_check_interval (300s) regardless. + _INTERVAL_CACHE_SECONDS = 15.0 + def _get_current_update_interval(self) -> int: + """The current update interval, memoised off the render path.""" + now = time.time() + cached = getattr(self, "_interval_cache", None) + if cached is not None and (now - cached[0]) < self._INTERVAL_CACHE_SECONDS: + return cached[1] + value = self._compute_update_interval() + self._interval_cache = (now, value) + return value + + def _compute_update_interval(self) -> int: """Get the current update interval based on game status. - Live games: use live_game_update_interval (default 60s) @@ -2755,7 +2790,14 @@ def _perform_update(self, preserve_scroll: bool = False): # Dynamically determine update interval based on live games current_interval = self._get_current_update_interval() if current_time - self.last_update < current_interval: - logger.debug(f"Odds ticker update interval not reached. Next update in {current_interval - (current_time - self.last_update)} seconds (interval: {current_interval}s, live games: {self._has_live_games()})") + # %s args, not an f-string: the f-string called _has_live_games() + # on every skipped update even with debug logging off, which is a + # scoreboard cache read for the sake of a message nobody sees. + logger.debug( + "Odds ticker update interval not reached. Next update in %.0f " + "seconds (interval: %ss)", + current_interval - (current_time - self.last_update), + current_interval) return # Use lock to prevent concurrent modifications during live updates diff --git a/plugins/odds-ticker/manifest.json b/plugins/odds-ticker/manifest.json index 1ab7ffba..9818fbf7 100644 --- a/plugins/odds-ticker/manifest.json +++ b/plugins/odds-ticker/manifest.json @@ -1,7 +1,7 @@ { "id": "odds-ticker", "name": "Odds Ticker", - "version": "1.4.1", + "version": "1.4.2", "description": "Displays scrolling odds and betting lines for upcoming games across multiple sports leagues including NFL, NBA, MLB, NCAA Football, and more", "author": "ChuckBuilds", "category": "sports", From 6d214259f266088dd195babf58f73171cac09cb5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 10:25:27 -0400 Subject: [PATCH 4/4] fix: make the frame-hold call safe for the test doubles, and update to main Three display-manager doubles still declared set_scrolling_state with a single argument, so the frame_hold this PR passes raised TypeError mid-render. display() swallows that in its own except Exception, leaving only "no frame reached the display" -- odds-ticker's cache-invalidation test failed for a reason that had nothing to do with cache invalidation. CI caught one of the three; the other two never reach the scrolling branch. scripts/test_scroll_state_doubles.py checks the invariant instead, and catches all three (including the *args one, which looks tolerant but cannot take a keyword). odds-ticker's double also needed _scroll_frame_hold itself: it borrows only the methods under test, and display() now calls that one. Fixes a continuation line in odds-ticker/manager.py that was indented back to column 12 inside the call -- legal, but not what anyone meant. Merges main. This branch was two commits behind, which made its diff look like it deleted versions[] entries main had gained. Six plugins then needed version bumps they did not need before, because main had bumped past them. ledmatrix-leaderboard and nfl-draft sit one patch above #466's numbers rather than duplicating them -- two PRs must never publish the same version, and a user could not tell which build they have. That pins the merge order these two already needed. Verified against a core with #523 and #534 merged: safety harness clean on all 11 changed plugins, 23 unit tests pass, version and collision gates pass. The one failing repo guard, test_odds_centre_collision.py, fails identically on main and is untouched here. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9 --- plugins.json | 14 ++-- plugins/ledmatrix-elections/manifest.json | 8 ++- plugins/ledmatrix-elections/test_calendar.py | 2 +- plugins/ledmatrix-leaderboard/manifest.json | 8 ++- plugins/march-madness/manifest.json | 8 ++- plugins/news/test_news_ticker.py | 2 +- plugins/nfl-draft/manifest.json | 8 ++- plugins/odds-ticker/manager.py | 2 +- .../test_scroll_cache_invalidation.py | 12 +++- plugins/text-display/manifest.json | 8 ++- plugins/ufc-scoreboard/manifest.json | 8 ++- scripts/test_scroll_state_doubles.py | 72 +++++++++++++++++++ 12 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 scripts/test_scroll_state_doubles.py diff --git a/plugins.json b/plugins.json index 88385c7a..977a3051 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-09-04", + "last_updated": "2026-09-07", "plugins": [ { "id": "cricket-scoreboard", @@ -412,7 +412,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.3.4" + "latest_version": "1.3.6" }, { "id": "ledmatrix-flights", @@ -463,7 +463,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.1.1" + "latest_version": "1.1.2" }, { "id": "masters-tournament", @@ -605,7 +605,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.4.4", + "latest_version": "1.4.6", "icon": "fas fa-football-ball" }, { @@ -856,7 +856,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.1.7" + "latest_version": "1.1.8" }, { "id": "tide-display", @@ -905,7 +905,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.8.3", + "latest_version": "1.8.4", "icon": "fas fa-fist-raised" }, { @@ -1001,7 +1001,7 @@ "last_updated": "2026-07-31", "verified": false, "screenshot": "", - "latest_version": "1.2.3" + "latest_version": "1.2.4" }, { "id": "ledmatrix-dresden-departures", diff --git a/plugins/ledmatrix-elections/manifest.json b/plugins/ledmatrix-elections/manifest.json index fea47ca8..2b206c9a 100644 --- a/plugins/ledmatrix-elections/manifest.json +++ b/plugins/ledmatrix-elections/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-elections", "name": "Election Results", - "version": "1.2.3", + "version": "1.2.4", "author": "rpierce99", "description": "Live election results: a scrolling ticker of important races plus a full-screen interrupt when a race is newly called. Auto-activates for your state's primary + general (dormant otherwise), drains stale calls from the ticker, and survives restarts. Filterable to your state. NYT baseline provider + optional California SoS county/city rollup.", "entry_point": "manager.py", @@ -22,6 +22,12 @@ "cache_manager" ], "versions": [ + { + "released": "2026-09-07", + "version": "1.2.4", + "changelog": "Resolve scroll speed through the shared resolver and apply the frame hold it reports, so slow speeds move whole pixels", + "ledmatrix_min_version": "2.0.0" + }, { "released": "2026-09-03", "version": "1.2.3", diff --git a/plugins/ledmatrix-elections/test_calendar.py b/plugins/ledmatrix-elections/test_calendar.py index 288837b1..43692dc1 100644 --- a/plugins/ledmatrix-elections/test_calendar.py +++ b/plugins/ledmatrix-elections/test_calendar.py @@ -245,7 +245,7 @@ def __init__(self): def update_display(self): pass - def set_scrolling_state(self, *_a): + def set_scrolling_state(self, *_a, **_kw): pass diff --git a/plugins/ledmatrix-leaderboard/manifest.json b/plugins/ledmatrix-leaderboard/manifest.json index 995cd700..0d0e7131 100644 --- a/plugins/ledmatrix-leaderboard/manifest.json +++ b/plugins/ledmatrix-leaderboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-leaderboard", "name": "Sports Leaderboard", - "version": "1.3.4", + "version": "1.3.6", "description": "Displays scrolling leaderboards and standings for multiple sports leagues including NFL, NBA, MLB, NCAA Football, NCAA Basketball, and more", "author": "ChuckBuilds", "entry_point": "manager.py", @@ -31,6 +31,12 @@ "requirements_file": "requirements.txt", "min_ledmatrix_version": "2.0.0", "versions": [ + { + "released": "2026-09-07", + "version": "1.3.6", + "changelog": "Resolve scroll speed through the shared resolver and apply the frame hold it reports, so slow speeds move whole pixels", + "ledmatrix_min_version": "2.0.0" + }, { "released": "2026-09-03", "version": "1.3.4", diff --git a/plugins/march-madness/manifest.json b/plugins/march-madness/manifest.json index f4d2c9a8..4eb193ac 100644 --- a/plugins/march-madness/manifest.json +++ b/plugins/march-madness/manifest.json @@ -1,7 +1,7 @@ { "id": "march-madness", "name": "March Madness", - "version": "1.1.1", + "version": "1.1.2", "description": "NCAA March Madness tournament bracket tracker with round branding, seeded matchups, live scores, and upset highlighting", "author": "ChuckBuilds", "category": "sports", @@ -20,6 +20,12 @@ ">=2.0.0" ], "versions": [ + { + "released": "2026-09-07", + "version": "1.1.2", + "changelog": "Resolve scroll speed through the shared resolver and apply the frame hold it reports, so slow speeds move whole pixels", + "ledmatrix_min_version": "2.0.0" + }, { "released": "2026-09-03", "version": "1.1.1", diff --git a/plugins/news/test_news_ticker.py b/plugins/news/test_news_ticker.py index 05429e3e..d3ebc530 100644 --- a/plugins/news/test_news_ticker.py +++ b/plugins/news/test_news_ticker.py @@ -30,7 +30,7 @@ def __init__(self, width=128, height=32): self.height = height self.image = Image.new("RGB", (width, height)) - def set_scrolling_state(self, _state): + def set_scrolling_state(self, _state, frame_hold=1): pass def process_deferred_updates(self): diff --git a/plugins/nfl-draft/manifest.json b/plugins/nfl-draft/manifest.json index 884bb2ec..0068e377 100644 --- a/plugins/nfl-draft/manifest.json +++ b/plugins/nfl-draft/manifest.json @@ -1,7 +1,7 @@ { "id": "nfl-draft", "name": "NFL Draft", - "version": "1.4.4", + "version": "1.4.6", "author": "ChuckBuilds", "description": "Displays projected NFL draft picks from ESPN with live draft tracking support during the annual NFL Draft event. Includes simulate_live mode to replay a completed draft using real ESPN core API data. Shows team logos, player names, positions, and pick numbers in a scrolling display.", "entry_point": "manager.py", @@ -24,6 +24,12 @@ ], "icon": "fas fa-football-ball", "versions": [ + { + "released": "2026-09-07", + "version": "1.4.6", + "changelog": "Resolve scroll speed through the shared resolver and apply the frame hold it reports, so slow speeds move whole pixels", + "ledmatrix_min_version": "2.0.0" + }, { "released": "2026-09-03", "version": "1.4.4", diff --git a/plugins/odds-ticker/manager.py b/plugins/odds-ticker/manager.py index fa78bacb..d975df42 100644 --- a/plugins/odds-ticker/manager.py +++ b/plugins/odds-ticker/manager.py @@ -3025,7 +3025,7 @@ def create_image(): if hasattr(self.display_manager, 'set_scrolling_state'): if self.loop or not self.scroll_helper.is_scroll_complete(): self.display_manager.set_scrolling_state( - True, frame_hold=self._scroll_frame_hold()) + True, frame_hold=self._scroll_frame_hold()) else: self.display_manager.set_scrolling_state(False) diff --git a/plugins/odds-ticker/test_scroll_cache_invalidation.py b/plugins/odds-ticker/test_scroll_cache_invalidation.py index 139a7fca..96078af9 100644 --- a/plugins/odds-ticker/test_scroll_cache_invalidation.py +++ b/plugins/odds-ticker/test_scroll_cache_invalidation.py @@ -69,8 +69,12 @@ def __init__(self): def update_display(self): self.updated += 1 - def set_scrolling_state(self, state): - pass + def set_scrolling_state(self, is_scrolling, frame_hold=1): + # frame_hold mirrors DisplayManager; display() passes it now, and a + # double that cannot take it raises TypeError mid-render, so no + # frame reaches the display and this file's real assertions fail + # for a reason that has nothing to do with cache invalidation. + self.frame_hold = frame_hold class _Ticker: @@ -79,6 +83,10 @@ class _Ticker: # The methods under test, unmodified. display = OddsTickerPlugin.display _create_ticker_image = OddsTickerPlugin._create_ticker_image + # display() reads the resolved scroll pacing through this. Borrowed rather + # than stubbed so it stays honest: it returns 1 when _scroll_settings is + # absent, which is what this double wants anyway. + _scroll_frame_hold = OddsTickerPlugin._scroll_frame_hold def __init__(self): self.is_enabled = True diff --git a/plugins/text-display/manifest.json b/plugins/text-display/manifest.json index 62640934..881ceb6f 100644 --- a/plugins/text-display/manifest.json +++ b/plugins/text-display/manifest.json @@ -1,7 +1,7 @@ { "id": "text-display", "name": "Text Display", - "version": "1.1.7", + "version": "1.1.8", "author": "ChuckBuilds", "description": "Display custom scrolling or static text with configurable fonts, colors, and scroll speed. Perfect for announcements, messages, or custom displays.", "category": "display", @@ -24,6 +24,12 @@ } }, "versions": [ + { + "released": "2026-09-07", + "version": "1.1.8", + "changelog": "Resolve scroll speed through the shared resolver and apply the frame hold it reports, so slow speeds move whole pixels", + "ledmatrix_min_version": "2.0.0" + }, { "released": "2026-09-03", "version": "1.1.7", diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index 75adfd95..14ed3228 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ufc-scoreboard", "name": "UFC Scoreboard", - "version": "1.8.3", + "version": "1.8.4", "author": "LegoGuy1000", "contributors": [ { @@ -32,6 +32,12 @@ "default_duration": 15, "config_schema": "config_schema.json", "versions": [ + { + "released": "2026-09-07", + "version": "1.8.4", + "changelog": "Resolve scroll speed through the shared resolver and apply the frame hold it reports, so slow speeds move whole pixels", + "ledmatrix_min_version": "2.0.0" + }, { "released": "2026-09-04", "version": "1.8.3", diff --git a/scripts/test_scroll_state_doubles.py b/scripts/test_scroll_state_doubles.py new file mode 100644 index 00000000..a3aec1b6 --- /dev/null +++ b/scripts/test_scroll_state_doubles.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""A display-manager double must accept every argument the plugins pass it. + +`DisplayManager.set_scrolling_state` gained a `frame_hold` argument, and the +plugins that pace their scroll now pass it. A test double still declaring +`set_scrolling_state(self, state)` raises TypeError the moment display() runs, +so the test fails for a reason that has nothing to do with what it asserts -- +and the traceback is swallowed by the plugin's own `except Exception` in +display(), leaving only a bare "no frame reached the display". + +Three doubles in this repo were behind at once (odds-ticker, news, +ledmatrix-elections) and CI caught exactly one of them, because the other two +never reach the scrolling branch. That is why this is a check and not a +convention. + +Run: python scripts/test_scroll_state_doubles.py +""" + +import ast +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def accepts_frame_hold(fn: ast.FunctionDef) -> bool: + """True if this def can take frame_hold, positionally or by keyword.""" + named = [a.arg for a in fn.args.args] + [a.arg for a in fn.args.kwonlyargs] + if "frame_hold" in named: + return True + if fn.args.kwarg is not None: # **kwargs absorbs it + return True + # *args alone does NOT: the call passes frame_hold as a keyword. + return False + + +def check(path: Path) -> list: + try: + tree = ast.parse(path.read_text(encoding="utf-8")) + except (OSError, SyntaxError): + return [] + bad = [] + for node in ast.walk(tree): + if (isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + and node.name == "set_scrolling_state" + and not accepts_frame_hold(node)): + bad.append((node.lineno, ast.unparse(node.args))) + return bad + + +def main() -> int: + files = sorted(ROOT.glob("plugins/**/*.py")) + sorted(ROOT.glob("scripts/*.py")) + failures = [] + for path in files: + for lineno, sig in check(path): + failures.append(f"{path.relative_to(ROOT)}:{lineno}: set_scrolling_state({sig})") + + if failures: + print("A set_scrolling_state double cannot accept frame_hold.") + print("Plugins pass it as a keyword, so these raise TypeError at render time:\n") + for f in failures: + print(f" {f}") + print("\nAdd `frame_hold=1` to the signature (or accept **kwargs).") + return 1 + + print(f"OK: every set_scrolling_state double accepts frame_hold " + f"({len(files)} files checked)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())