From a44314397af17bbc06eb101de9a4de5f2d750414 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 3 Sep 2023 11:22:11 -0400 Subject: [PATCH 01/56] Fix link migration for stuff --- src/components/debug.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/debug.tsx b/src/components/debug.tsx index d858253..69b6ffd 100644 --- a/src/components/debug.tsx +++ b/src/components/debug.tsx @@ -38,7 +38,7 @@ function buildDebug() { if (eggCount % 10 == 9) { // you know you're bored and/or conceited when you spend time adding an easter egg // that just sends people to your own project's repo - Navigation.NavigateToExternalWeb("https://github.com/NGnius/PowerTools/releases"); + Navigation.NavigateToExternalWeb("https://git.ngni.us/NG-SD-Plugins/PowerTools/releases"); } eggCount++; }}> @@ -66,7 +66,7 @@ function buildDebug() { if (eggCount % 10 == 9) { // you know you're bored and/or conceited when you spend time adding an easter egg // that just sends people to your own project's repo - Navigation.NavigateToExternalWeb("https://github.com/NGnius/usdpl-rs"); + Navigation.NavigateToExternalWeb("https://git.ngni.us/NG-SD-Plugins/usdpl-rs"); } eggCount++; }}> From 40b46337b45612cc8dd6afc6e63f631aa9384441 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 4 Sep 2023 17:46:19 -0400 Subject: [PATCH 02/56] Try resetting clocks as well as forcing them to min/max range --- backend/Cargo.lock | 4 +++- backend/Cargo.toml | 6 +++--- backend/limits_core/src/json/base.rs | 2 +- backend/src/settings/steam_deck/cpu.rs | 16 ++++++++++++++++ package.json | 2 +- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index b36860c..4da9134 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1052,7 +1052,7 @@ dependencies = [ [[package]] name = "powertools" -version = "1.4.0-beta3" +version = "1.5.0-ng1" dependencies = [ "async-trait", "libryzenadj", @@ -1321,6 +1321,8 @@ dependencies = [ [[package]] name = "sysfuss" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa4dd5879b3fd41aff63991a59970cdfeced6f0d5920c5da0937279904d9f45" [[package]] name = "termcolor" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index c12933c..2f48805 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "powertools" -version = "1.4.0-beta3" +version = "1.5.0-ng1" edition = "2021" authors = ["NGnius (Graham) "] description = "Backend (superuser) functionality for PowerTools" license = "GPL-3.0-only" -repository = "https://github.com/NGnius/PowerTools" +repository = "https://git.ngni.us/NG-SD-Plugins/PowerTools" keywords = ["utility", "power-management", "root", "decky"] readme = "../README.md" @@ -15,7 +15,7 @@ readme = "../README.md" usdpl-back = { version = "0.10.1", features = ["blocking"] }#, path = "../../usdpl-rs/usdpl-back"} serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -sysfuss = { version = "0.2", path = "../../sysfs-nav", features = ["derive"] } +sysfuss = { version = "0.2", features = ["derive"] }#,path = "../../sysfs-nav"} # async tokio = { version = "*", features = ["time"] } diff --git a/backend/limits_core/src/json/base.rs b/backend/limits_core/src/json/base.rs index cbb77fc..803d8bd 100644 --- a/backend/limits_core/src/json/base.rs +++ b/backend/limits_core/src/json/base.rs @@ -175,7 +175,7 @@ impl Default for Base { id: 1, title: "Welcome".to_owned(), body: "Thanks for installing PowerTools! For more information, please check the wiki. For bugs and requests, please create an issue on GitHub.".to_owned(), - url: Some("https://github.com/NGnius/PowerTools/wiki".to_owned()), + url: Some("https://git.ngni.us/NG-SD-Plugins/PowerTools/wiki".to_owned()), } ], refresh: Some("http://limits.ngni.us:45000/powertools/v1".to_owned()) diff --git a/backend/src/settings/steam_deck/cpu.rs b/backend/src/settings/steam_deck/cpu.rs index a6e4d10..4bea66c 100644 --- a/backend/src/settings/steam_deck/cpu.rs +++ b/backend/src/settings/steam_deck/cpu.rs @@ -307,6 +307,18 @@ impl Cpu { }) } + fn reset_clock_limits(&self) -> Result<(), SettingError> { + self.sysfs.set(CPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), "r\n").map_err(|e| { + SettingError { + msg: format!( + "Failed to write `r` to `{}`: {}", + CPU_CLOCK_LIMITS_ATTRIBUTE, e + ), + setting: crate::settings::SettingVariant::Cpu, + } + }) + } + fn set_clock_limits(&mut self) -> Result<(), Vec> { let mut errors = Vec::new(); if let Some(clock_limits) = &self.clock_limits { @@ -390,6 +402,10 @@ impl Cpu { .unwrap_or_else(|e| errors.push(e)); self.set_confirm().unwrap_or_else(|e| errors.push(e)); + + self.reset_clock_limits().unwrap_or_else(|e| errors.push(e)); + self.set_confirm().unwrap_or_else(|e| errors.push(e)); + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_cpu(false, self.index); if errors.is_empty() { Ok(()) diff --git a/package.json b/package.json index 02e97a5..11a3af9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "PowerTools", - "version": "1.4.0-beta3", + "version": "1.5.0-ng1", "description": "Power tweaks for power users", "scripts": { "build": "shx rm -rf dist && rollup -c", From 05f927dc0f3e1587089c787a2f7736a1c0d057c0 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 4 Sep 2023 17:49:25 -0400 Subject: [PATCH 03/56] Fix Deck CPU min clock min value being saved and shown in UI with clamped value --- backend/src/settings/steam_deck/cpu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/settings/steam_deck/cpu.rs b/backend/src/settings/steam_deck/cpu.rs index 4bea66c..c6ac04e 100644 --- a/backend/src/settings/steam_deck/cpu.rs +++ b/backend/src/settings/steam_deck/cpu.rs @@ -493,7 +493,7 @@ impl Cpu { if let Some(clock_limits) = &mut self.clock_limits { if let Some(min) = clock_limits.min { clock_limits.min = - Some(min.clamp(self.limits.clock_min.min, self.limits.clock_min.max)); + Some(min.clamp(self.limits.clock_max.min, self.limits.clock_min.max)); } if let Some(max) = clock_limits.max { clock_limits.max = From ebfebb6ea7df026e14f3199dc63ab6d44d97f631 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 4 Sep 2023 20:36:47 -0400 Subject: [PATCH 04/56] Update thumbnail used by store --- assets/thumbnail.jpg | Bin 136263 -> 71480 bytes assets/thumbnail.png | Bin 268287 -> 265256 bytes assets/thumbnail.xcf | Bin 611143 -> 633728 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/thumbnail.jpg b/assets/thumbnail.jpg index f74149bfda80d5961dfbddfa220254a2bf76ea73..e21d64a02a83bef59745c265e24f32e8a72e238d 100644 GIT binary patch literal 71480 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 z zUI;C0K9CZpe|_CH0co!+IAYVX=9?9p?4DV?RzN=QkGq?sJfj1QKrM9UM!*S!}X!1>{i`^t%Z-qmrHipy?tp7*p9!AXJ-{J@Ml$+;qF#9jXsbS z5#qiSd{h!UwtAMU(qf^Pl6-sNDg8pr_8obdK^5B-$jZm=*>Qy9&T5>?Vf9ZUh3b*I6kn|EB1>}fc0I%Q_yTg3 zv;!i7iIsc|OPAKAXop>rjT^~WFDpyrrW;PVYMj=4pJW0S=$Z3=eTX3chK!oli^ti> zT$X~Qqk8E4W=f5AaOgx_CToES-|Up)%F9%SUuq3k5p$3vj#{0HF;0IK$!i#7(~dUd zg9$q)fb3G24Wx>gdKd#TMlJ1p;>ZL{uDko8il(H z(M}FoZul{GWkzW-X`9C?82X9cgN7Pcq7|YW0~0LfSq|NR*f~x6kJ!FKjM%P1CkLFa zR`AOr98wF^7s{`LAhFZ}Rq%pU=_JAphu zJM)}zm%q^G2@o7VU&=+B+PUY~E9}~t`TCPebGU;Sfl4;!_s2&c?^%Bh6x*O^`J^P~ z#IP*YB!XEwhISR<@n4$7=-ROm*_$|(x-iw_+vDSiozO75m5+pH7dxwa-91L=&r%?N z6oNhhy#qLpMvEqjTFY?o0}I~pqyc>SyNQmZe!Dm1h|K>cKJ@XY7iZ_K4Nm5UVLI;Z ztH2=rHZkJ4#{3g>ampXJe9{3q3q9Cyq-a@t4c~e=*Sjk0+VN9s=t))NJC7Z|v#R75 zMg(3*V^>>7jD{Wbr%O5ChdNZ-Un^}k37@3A&iDC|HQ<2EPV)bW?9!I{&TjMSmkcXE zyMVrWeauP{Gl8?RCg+l#Iv`aC;M~F`e+4Mu`6i6NIpUT{c#!g%r>(fW9ufZ-?trDb zjs(6UUZB@5Xg#c=g`K<^zByxwCH2Z9Jh3V7F+9q)FOblV_r^L)TzSye9VrQCw;vcsTZ8ACW1kc zD&$?jpjJsN#TJF6>LAo;8l3dGrllIF6<8pGIn zqZPa&lDB*!U~iLN7THjp<`grNazMGb2;Ik(mGMPq!Xy`@AUf>oNtiI~`mkGN{Mk*b zF1n#XFEdwiY(`G25S^|W*fs_?;%WW_Dv!`J%W!Gz@!jxhfaCjjA}mQTC#)nfalLYy zZsw)cT00`J5OTGZ^E#};5ycVP=0~wTacY^QW&^NIcPC*b;o+%8Sg9yjuS{58jJ0}t z5&&u8SZduBKfb^Pig^HUxQ8pokJ4bB)9UGMyUld|KJa+;^iH{61ICXQU1HKqu!bxy z`5hpl2^Wy(0sxdVc#%z6H54)GAQwkc^rHb8OA!!d>@ZYNmM=E&1_2?v2SPzpM zd^pr|Hnu-a_Gt976MO_LKID`KdU;pdKQA{ij_4Ks*HE1?qF3TRt++&%0+8%==xYH; zx;hZja~H6w5#;oW%XsX$h#xRwZr{{Zz4`BfCIBi$mMgbd*reFt7%BNW0rx4YuK<{L9Do{x(^q5y0sqYbu#t)W;}@cy=Uu|Z<}+oP?YMXy zj&o&e&{1nH;{&!a8Li8G+)24Z@{>Jrz6RWq01g2FmtJ_$8- zW9O9GFLcr-rh&q?u7)$jvh!L&X4`~4lXIwg47#FLMQihPJKB9y7`|th=5!0t zN=ALLXROI;oH}NUn8z{25TqZ#0tdycI}TH{PS%S)lMB-SgI`PPW;-Aphe?@&k~=`x zXOT%(59I#f#`m4mmyLYAglEYUm1>ZDW))SL3%?V~(6m2FW=z>u3C3;fMbY#@xgS&d zy71^I+{$HQt*9aSbw*GTCl9BgY!TiFY8D?_a4*~KwN8baqFYaX`iJ%K*Y{Vb&Ip2laT_^ zS><>!R?QAfUXSnlHTS%$Ne|*|b4V6I{gcV!742w$PWa(iO{BSl0eZaEvt;_GFO4HM z*hF8Be`$DV0d0B+b}GjlH$wL`KR);00p2r&6LG8zUGeICA}ne)!tlJ{ z(2Uz6Yz*t@9ro0Zx2;_GT+IsH6-=CuV>5}Ba4GW2%!oU+*5uz=Q+{|NEqp@x6lW=H z{ljzIb!gx+lk{`zqZnqV%_FKCi@{$gr?^xW6}@8#Q=AR=4C`l~w$*7g5Omtl3%HG^ zkqP|4NB;sNr}Co9EcoPRgg`{z;?+cpG58+z_GQe%6T}$b5wXAky9`G)c{q9!X2MGw z_L~DTeg613tM#j$P%;m@OiTv~L7|zq9DkxpfYpio0^{p~RFuriK{X~6-*GXZk5Z>e zpAf&YH_H)r8+^UVLEer?Dp$-&^I8w)hsC@2^9Gr`J^ceUDqK=oK0~Wk0MpHs z(Ifjwv%{C?-|U8}Oy?ih6Hz^@pv}OBA7OY-DChDyNV?E)%4oQV2!xH*zA3U#<-Qlf zn4#n%+L_JRCD@x1*p?(@S%-c9nR_*=hazvhIeLrNMd#B7J0Yj!hk?|9eS8;4j(+PDlo1i{#LY$kW$^yA^Mw^{X#8LJio*2&QzS57SB{7w; z-55sjpBF0jUnCWYqRZN~veRs=wPA|)out{(e0J~gTvJOM%6?0Jqn5F_myFGkLg2RX z-RGq)IalIS)-Sf2#3lPp5;a7}*nTnFiYbEQ1Y&|P4N~<#6KZ+|32T~Oj!Oh_1T(1( zgQSd>IZ`_GU(oD?Ji8Dg+`jjk)PuIGq#nDIdSIF$Q6RbzvytH)Dq5eglm;BfOTc=u zT(p%qKKR+U!rw>2Q&SlMIx3}Qq~uLJknckW}CQG zaq}&BBFWPa;)s(kR#Gnx294rFXu>6=rA)$==ejotIC~Ki>Q9fQ_~b_?HWSh@ou;cp zC<_9c@K8hrrM|?3^3y4+Oq%|+%(6TC$%g*=)!HQ3u6r^bkBE64v zbG;9=P7GQzBWUa~UXXP-sfp9rqoG(9e`awcVZ5C^np!IDOu) ziB{)S987Z?*oB_&xbhQ}ws12W#y+*~v8xJyriDSp9JGI`VC|$IJn($WnY`PVOF?O1 z2OZpyYC?N{ys+o&JBV>$)eC2V>cYdy9!rluf2>_mVcofA7ISo;JS0v`m~#j{LT$0h zvaTX_iQ$@1`lY`L1!18JjEamyXjDScYBL%xs`Hq&L{dkzbLqg1AceSpTZk#TP`Ff% zyoY9BizH)#V@R$e&g%7Wf5{mRK z2BZ-q+zYlEZ>*AC()jk>eD!8o*TPf#tv_Zo6a*8E`-`4u2Ww0&5M@kM@WjLb8?4)7 zc2XpZZhiF-5k7ng4{rQ`#|wBexn-5}((sZ=-K>&C3gFZ3=HpfkRihQAz|UXz=wqK~ zK8B1Va--^>xJtbWS9uDuokSSaI8g9dRWJ#c9F6@+y`$z9Qtw;B*PHf}RZ-05$FXI8 zn9ftY*?dLkboZXn{lKP5ei!<#wgxZTl~4RZk87E2axh-0MS~|uX-M&F@*kY|$`l^_ zq)ftsKn!f=!o%8O8Wqjsk|M-RKX%9xgxKY%QF(h}@5PU>lZU2I5I%T`_iPgKVPvK) z%~TSVg^H%u#~+oe)mJE@PT6VqO%G>79J3LqH)q)ZW!?T;@GMPX)amt6?Q`@()kRDD zhq!w09Mr>~1{5F^QbCqOoT?>lqQYgHI6$c?eozXYF-<1lk-VlsqbxR zFSiK1Yv5botdz88d-JX0!GnP~;&#oD$JyA0Pw$zeB^Vhm6$|e^oH(j@9da0BR&E!Z zSD+ih>}||IJ%Wa9LS{`Lj2c8v{g5M@ExYzL0ZPTH$k>7_Hx%Kz_EM)O7T*S%FiJsM z7i+6CXjSyoOoEoG<9p=B8s*m*si80K4rGg>)@!PPNe9FMYFQwja&rbL`H6g{i|Jx^ zSJvm4$^yCV_v=DJ)py5uV|5M;E;ig{<~-~XwJ{l=I7 z-e>s^Vit%S-WUW1@6nwzP*1Mvx$QQ~U!E?N5*bb1eEa!_L7UgY-k%97hW@2E!1WQrI(cQ#sMp~EuC@1C~UW7|%dpDXCo+FW1BdO&R1FG7kk=DU+bFXS^W6T{COjS2- z-TNF`n=jPt`Upjj&rq^lI!n?(+Jgs$*sbK9*EEzNu1faZpUJU99xvI(8<4-G>3OLB z2N}gp76VFjGEJSE&RdprCc-trR&6Q#R0XyUZYP6RS~I~jD;yu;xhvb-)oP}$`Jz7j zZX9}p)~8C;CW2u^Y(YFi;i4%Go4(Nuhc*Kq-x?#@hg$L@G6Ya% z!%(riSJ)!d1!EaczD(pR+Jjz(V^sft?7d@jrrWkK7(1z;l8ViWZQH8Yd1KqQZB@m# zZQC{~M#ZdtYwvyTIk)fq(LcLKe9ATB4&kUI`e+r~Q2#o^S2#^Idd@*Fy^jK0j&5E~UHo5pdYFV995WOBF-lM-N< z7F@_s2%+!LFLOZT^z2WQMGqKp z3`usgoQZXK+Yq@WJa8KVdhU@=Dc0d3Su)03OSqVF02Ox(7>b{l&4^*P8sX?N%wHP@ zjNvj!B>M*=!phJK@Sp;P9y7r;FVE#U`(V`UD%ZjS7CE2?~t9D z(pngk`gPb5!7o8k+t@%FGX%R4EkQP)FzA?7K{P4EYtD;Ht*7D@r^F7MTYbmldCt+! zrktd}U^4Tci;I+&QtP#E!$Vrx2HFjNhinOWxfUYw2vMG)S0kXs6-7^+jS^AEs6~d? zRM;^=eG(D+WJY&|oE{;~((cTjQg))?fL)rR?y;+(+ZnAV(gNvDG8=yA`F`7bRQp~-@pkQA;cJEU8-xen zl3*RmcN#jq|HK@~>yr>4RO7evIz{Y`E*A;Y@4|IWx12ma+2i;$swSqUlxv>YeG)8I zhR=kj>wW1~0?qC5z*bK*=q(%j7l6*WI=p zeWN1*{5VcsjAjnV(alxAbDFnRGON?1f>Oyl8Mo-IU|XEBuH=$M>-|e{^pG!$LzXRE7=!Q!+6oDULI6y<4x{^;a2;=&Q3$Cmu=EoE+}lpH#zpACtEAD*ZmyEueeUY*CiJb#@-}3$q{ojlKtIBP!>HmQKui}5V zv<+;j8(3wml{c{Q-yp}GuU`Ju?&BY* zuK$E;*?uP|{8QaK-T$br%k(cS|Cbkk5c+S#_^X+Jb@5McJpO8D|37;3hh~4;`5)c( z1ON+pYw!3H@mF66fC#>SDj06!i<|?j6NsKLUc~0zr9BMKyvTT(9(&XMC!qHqK%dZm z0e;T~BK`j5KR&kqhhl$~{ew3C$_u?@1Te|!CrA$0e+Kj~>OW2ZKmRif|KlNI?g|?0 zTjll+k>%C*j-l71KiC0@^Z)6G|C+zu)$u2cy*^#tSG?rBHPwG;g}!%}c0a900UT!C zVQ5R>9Iw7Eo&)?fEw9r5p7J;NUHjFgY6ogH<-LigYEM_I!={_3ZxIjZZcN@%Kt(Y( z{_x79JYYl5bzylE?q4rn3@lcp?GAe(fN5+rBMh?vm|zf}EQCHs3i#0n^+{zVNs&3p znhVJt#+8M?KS^?+68SdJMaz}Hk5%g5V>|IUm z_#k!RNA9}hyF|Z1KmU4d^pbq{g0Fn_v!bvch(Qv{M=$hiD+WD~E$P7ezEPcV2;;-X zVkh8Qx(_q*O3XFlM=vLO%ZPe{EW3CvAtDK`8XxZUi%qbxCsWO9U;w;kOAu~#FqKff z3T6nl@*K(5jStus@aL-beZzahd)w&TqieXvh08luZRJ7pHQpCRFZW;lzyqd+)&OG5 zAbz(sHPs=ExkNJ0sJ2KManJ{HC3i5e>PNhe+0_gMnx)R=n!<&D_y}pm#yY1!CLnv~ z-g{vV%;(-DUe#&WcisqohGFZp%Ri#t{qSLWVybsT>iWTxmNtf_A-IbvGLBDskVX)^ z*z32n^fMxCKn=RLp|Q$+gJJsFK9HkWD#2<I36x@oP#17@h0CI2@ zXfMLvO&@~FgSH<^xzp6Ph4q{xa*Xk7s@H5@PAj$!gHc?QV92(tNs_~C)%7uY`Eqn)4PtmjC=G+Y z@KkG@wtD_uP;lw>ga9%%;|O_Ag9zOXoFW7l1mh$Cq3**lL`$O-8Frj2o?=nKdE>vo!@E_+7F#McTKHdnz+*`f&>cKu{8BVZ z7trwE#=(0jnCWj2b|G3Enz0|J8ncd0(6cuJ8)|;wnJbu?FNc#Ip!#W93``Ypy37Z! z-Ct4~f|O61o-kh|FVu9Ind-jim#I^-#N^kW2ob=+205r_JOYlvE-O)xKOe#w{yF%V z7KFJEUb{gyzEPl~&i>-M6t@>dQf5>D#yq!c|S=?pT{kW)GrLP`2rx52UG7Mu{C z+pj8-6|Ow+al6h{ISp}(v{$H+9hHbi)jkvxSDW3cxSnhFZtFR^@>pDZ>WP1Ywnu%( z8?1})U8(E4aQCk>D&HfRk1QW!lY?JaN@PueD|UtE-;@h0UCC4YUyW$O=aZow%~;7*tPvLoG?5j(d^N2EwF$>+&8hC5AI2VzAr3S+?^ zL_J7N5QZ*{Ex-{%Q9+InXv3Y%)GLzmP^zpa=YxRyXdpwp{`|c#+jk&-`{|dV9*arE zFC<2mS|j5h1@$4?X1$*1N!iRP{Wn%E20cCFcZbxEt&=uN2Z$VLbjVC*Sc8ys za5vPE83YxGE9(V2Ib{y=Y5)kTY@l6U*{KeAX>`oXYyLAhO%z{5d!cyD3}@&d6r)Y{ zk)0P4F^O#Cv>Vu%eKe<~5q<%BMp>K=OQ6+i=SJdeE_r|9gwcjETC{_$cIaoNPH71Z zi{S@U!>XZF6#g_Du+UAI#H(l-N25IbK(H@-pXYF}GT|A_R4L=moR+Oqfc^HMjK$Q^ z!HRTfZslbq$u94Es`&V@WQJ?PxsHt@n;V&xp3tyr`J8f~F(W50uk%Xegnw)0b{swn zI*5GEppU~-Ei^BQsF^ho1t5fQFk-Q?AKxv+b9lAlp0MLZCM3@FC(aWRyqS1X&trf( z6Brk59v?Kj9}NjYzn|w>NB#kNZx4+{&9ET&xo+ZvtZ#Uo!@@6jWu|exO|ZeP9%#0N zK;hjF%hMH)WTA4)p=vwzxmCt&YT^p%K;MoD81v3bfe}<9wV*gHss- zgrv;B-kXR;peIP(grO0C5;siPZCSpXj%oW@863Dh7vE&w=7J?i*ryvP$bP2WiHV9e zrE~)CJhLH1^X;~%&{kR4?v@1wDS2@TSmVd9|_tAp{BHW+gC#XTuM zqPzVZzSe1E*KT2tyea5sazMF((0M0XA996*n&&XHV+W?(ge71Q4=)}-jI1qEH;MLR zoHIHg!I@1vkYo7f7d2kbR>}D<|4hfpdxo* zN}%%#jP}#Ex4RG(35>3fZOGT#UAR5DfF${CYu&TQEDDrk&8#Q`8zZfb%GR1u#2Y z>i!Mliovv{IyQGx7cXPx{!F1zoyn&!X==jl64o^lY+2EO-*xq9AP#Vp9qp8%xkF@dAgYnWA~h2^6PVk zvB%=(8NI4b7&~Za?6G@Ba(W6|u*fqVnvJwTyulx8a5(H?gv<4FUOK!feT4pf+n*nq-pWQyXPA68-!CSu3d?MSIQI-p zw>%gkAJLHCxekB`r%XvIg~U^8sHDEY2+lWY@Mr=zIi^d-OWaT6`ymlvQO&XDy|zvu z?6R-ww2|_Gs~58w>-c3a^^P~B3!+;|{)$d7LT}~)`-$mYlE0(XhssxYf!N0#4(Nlc ztw$hQVQv>kx3-ZDSkr0=fB{3#T)N7+MvT^Q_n-xzQDNy>M2z{KYHPeWozhdb5y_+V zf>ALdSK|Mb_M}mBxP#DXM&U~;EFZ)s*Awc$>o;!5Wy!OIM$?@89@KIZZ~3f5YkVS( z7LcrI3M!@6CZ$F|DVl#oGI_VSGkQ<`akKb<`X=-dD#IviDOl8?xQX8|C#`CxK2&e! z5Og|22%o|T*}83z<>^hUlc>N9mF<*l27_Z~UY$hNKvH0KkTU)wo<0q=an;gK*gqHg zH%R$sQ}r&W!}rEaa=Y1)1qwtN)t`8t21)QF&OtdaVd`yCe|VIausm`GRL+ENUZk9_ z2FWm3pPHHsqiY&w2wBlH*KdEVNv3j3q?swm%~W75u`q%w zTfwwkkptGA7!4Zq>w_XmxHaR@B;=$K;OIgX(6d|dwTPMpRxY9h!Bcu;_IZAQfPGfR zOiyJ%Btb>7x6~Y2H{qe;H@Y1!{92~yl$JP4aW!i{BK@;Q4f^YpBXS{QSYuq#*JXPX z&Cy08IMIq?Dvy82BdJb7TTX_{6uwV_x)mFJ z5vR zAXOAttukrF3Lnm9S`6d9!ztrnsmGDqm?{b&<2Dh+v67Y;vHe8)PJSS#5U@s@*-Q3u z>!C!azEhCYp0_|7(w?bYnduy4c}U9!)!=X`+;YX2AT*F?XjdPbv2D;F|2T=NSm>9$5t#^{fQ(6{)BiDzNL ztxh{&=Xv~Q?jqH8FWAhkg^-1>F+Y0yuDvg*O(%Dqa9M;YQ_5xWN9?6p)g--jD`v49 z^Ej_I!i3t7wt}Ffs1h7*@8<($e%Ui#jj8xx&jEvn@?e4?zae34PKo-NOU(y_gcxE{ zP^!i%6yOsmlTUX7qsos5EesR%;SEg5Qzf9|9t#y1Ac43`FK}ngw)}+AC`+x^~?%QfL}d;8SWt7QSlgL2!*}j0LIx&3%`XDv<8?I0>jLJ zfOv6kJhko?md(HeTHbFY5minB8l0hu#b^->yqv!Y58O9cY@Y0nZvtEH+rHk!vpMP< zRhNIv7@j53nSpv6sK!cK@YRY8jhe7-FomVMTRI{qLdr^L+Z#JfG~0m*wJSRgK`ML4 z>`h-Gm2pjy$9$4ggErU4w@AH_HE2nd0d2EHDN5OpX6o4;MZ9!&Jaq5u{ousF&PV~4 z`_TFLz+~G>aj81)FS*q&Xt~0=0k0&44VMrNYe6MgflV0XL^5s2v#7TFFjC5;MCzP& zla}JagkLVNbR>(f=U?pC^T&$s_M63%f0h2L@*?0T-#)TD#$|n{irdd3 zUa`9={yy(cQJ_rZ;gL>Y^;U*VLzq`m1Ni1kEUsO}R=2XZSN5Ua$6u|S(9@&CUa>qg zz8{vFM^%efi;*qkiO0%<*f-j&v{}|HG+CVL+@uKy)m(8_K)ruypU{Ro3^XZ^tJ_#tE5LJ+XBJ744o#f$L7h*|ErHOP ztP3oN{bfn_8)T>R$mT$$P;3W`iCS(^hznVSezLkyo5=%iF4tuNNu;`qIUGWKLWft( zKGi}bxuVKq0@XT@LDZtu~YnF`Aqd_-$Qylf1P|*KDbi+V!v1^ z^+BLDxbmo`)OPKC`f_)p_FUWZ?fB!Fugyy*!?ze)QUzVUTi$VgVHy45>#knBZeKN& ze5qcEG=_VhSmj(agyJ#a5H6miLFm)h)&1PH>lNFKtZ^>aG;AJ=_lCr=+*Pp?{}8cA z0bH0#>##7_`lTj=ecq*<0eRj(F_^ka3Av(#B}7wMwAhi*3TI7FWu=eSk^W zC>fYrfsc{|GGcZ~aDs3UzXr&)iG>jNydKGz6_>U-&!e8NSdXdj>12i#Ht`C$L0aFk z5ufTL(DwFRbj#e>;r5TwC?KFWA_8sI;e4 zB{2b(HmLeYW3>{~XuJCI1#v058ggapj)h+xbHI?f!kjoUQ|Snu_u}B)#Tb z?h2Jn{d8nXeXK1M{3iaVTr5Ccie^pIk=VmH(NX#BByv8y1XH zf*>TKEJ;GyE;B)1L=qiz-}X%mAS{@FusYOnT9CRN5wq%t1%9D7nx?UR!~|e&mF9kL zLvy0*EOT16Hqq-1^Dutq;dI3zy`H04r|LyM0j)XhqoW}d7(#6(5({2wGtIQ+o&Rgs z+lGV8s!s}LkrE@(aEUL(8$;ePo+0RkTjH-PecJ}=9rA{&zh?XfdFujd6GS7xxH)-X zP()@7NGMZ(U@NW>nz>_N0|mdZ|y%~WgStmE-)l$v1 zeF1kXbkx{}wZuaI;+PXkS7v<+ewkpT+(jWF6t?ozI&KwctbsLR_@f-rF6!k z^ZM>SCWPy$@@IV?)jO(wVJwKao_}X~Yk1A+T?L+*@`ieUE>fE#Qialyv4s8tgpv3y zCuXh2ruYli8J`4-8;yp5>d9@x$}ga}q-J&s(E=eciLdYGov9)e!HhL)WFrIjH6VB|I`kz?K!>>0br%nP`~c%)kf&wFtLzD3suUbK6*}- z+iJF6tJ+N_4Om4f;vs+gu~pAZQXEe1AqP4WY0&AQcvL*p5Qd#Fl=H7E&|ou z?6p~=xpB)e&fHHD(UIYqhSP?HfevCAuC3lvLkNHs6FQUbb{g7@7|ep)e+n0qna($u zZ+VpU+n4=<^1~G|fY|DFwx(22L@I%iMMaH}rlH~jhE@6C`!|y#8Wy48x4RWdGT_xmNeE_JIEhMIFK_NshT>k zMOUgIxG*3nNRAZ(Fe0pRAymuOpaRf;uCYG^kZVeb`^VsTsTIlzB_2ui6LMXa(Ax;_ zwNlwst8HsmLM(PjJh;zC1Tf9rsK;&H^G5hu@auvjj;pPQ6b$<1D3Z#HLc!H!OYx6h z)xi;>6sY^s90 z)!9Xk@5P!d!jLy;n1eBMVZfU>?F3-6fuRI8U9Bt{t?)!U|42TaUQ3U^pl{F#1&6iX zp(hxSVlwf@LCPL(vx3GpYl#MLu|h#J-HHzA{w9;40bwm?nyi0FD+KLw4^Fdmfmq+k zVnIe#k+E~G+jFqdKgJ%z7M%`0+=c^g{*Zwbwyr|99l(Zg5+#$kWPR3iGS7yj?cYt= z$=r+;v~AkBwr7JFj-(o2RRI!{f{GhHgT>X@jKheWtw)NyTk&B!K{&4XOXMrDxWuE< zT!JLny<<9pR(y7o5c#+!bgS!P@rrfP z&BT6aGuqZxR1<{Ov_yXV$yZfWGZw`>z2W5hNriFn03NiEK|&1bJol~x_9dgbqvom` z&w<8e8jIzI87AFYyQNp}#eG9HYO(Nx*R=VP&*hZ2l$WSM2$5`g8?kVV_ufw{Bx;p% zqcfd`pffTn`%gP|3`sr@@p|6QJ8wz%r5_QV|*L>rD*Je&l2K7dia(-#@gNKQ0D7 zzj3-vR!9&pNttmP+ntl;@Thz2)<|*ZYqB-T;8&;A$2R)@2H^xo#5Mt^D6w}AyJ4)Y zv~;Dx>W^H$2-*5rEa%Dj4N~y;xgHC{MbKvgI!|jn11kyU@o~U0o&cYiVs+9jq>5ID zWU$o5ms0ZuacVX9a+4(0I>jGv*48<0+zV<*+f_^>i5SCs$D(8En>Hct0QwR=* z%YMWGPC4HumdnvVqh6T?r#?l}KCMfPGFVnsh#bq^j9krPqxGYke(HI3ZhAOIXMz?N>C@RRymb~kmx8Plxn{~SU}5W%E)DqX;e;H>OEd# zmT?YSV~KseR_Yy%&isn`#;Vqm6aD7O%78=Xy!q1-Y{Jj;rk zl+r=XqP6J;Hq2vNne<_n@G2bx)3DP+!dH@IZTqFBrdL{5jhSxOxMwRT3sv22?cqv+ z^>Rc$mOY&9dd-mPWz|ay?E5OQB_>)&3$n2c6Unv(TN#6q1PUQ~L+}o)h^^zRQ!+{! zxqC#Jdu0{b@lMVYeZd(e>?iPdv0VzId0z0EbH-n-B=5 zg&yK);b=h+%L~<)LO=BeYAlDUx(di{J#Ay)S3O%06389xzG}sb>|sMCmp9+V1F$t*vc{DxM@j3+vzLek&&2ko_du`s2J8J zRfwtGX7FuPsy1T2bsU5;CQ*wP&ORW&k?8^-_BRMtzslz*!N!8HU(+z}W{VH`XU18@ zl&v@|$GTbfN{gG0<@ZDOgQ~@n<1h7$c^|cn3z7V}0Iii({C;9EL9loQh2wpfE76QcG zjTnbKaK>pM&kav%T?=|x*u!Xwvq0A244y|^jS6}dzimIJv9Wp8vTZ8(wIg&Kz`9&7 zHR}5wzfU47#B5LA$4wVWCkg2%b6#6Ff!{%VU&G!Z_OcGhoq^K{@0cP_PBX}927SRl zDqc&>z-GBhvk<|a368Ig*w(741!*dJmJT!v zZnI-E#4#|h_+dfrdC~dtb^**SxeA4Lg~=C{V6iY1<21RV6%Vx>!ut*IMa`>_!D@Yd zKTlJ0a8kNwe1z4Mh$1D-U}U&7H%tH%DTF;*Vs$+ZSKLA;awf{tI|d4ER6_G>h9Ih2g>%8pzyB9h((R%NimL5SuSZW=9qpgZRkM z&cdU-I!?Mh=#DV!s= zZ4e`IIk?HZv8GB105YPKr|Z%m!A$&SkRD}VQxo-T7E$16scDpGzI9?LMU`f(aKj9E zC^_s}8RZ2^*%?=5cl-%#i1}p;7*?MrB_|6=D$zMkwK1AiG=SS{h_aY_{sG zhp^d6hDjm3;l;o-CIlHJCo2fPfx?=1E>mmeuhqM)dw(F^prMwy9Pc?DE|TIFTxNqE z*5kfRE+dJ0J-{jJBKAF=Q*9*vOrT2^nYGbvWv(x!yg;sRA&LxQaA&2t32U&fb0)nX zS3Gvf0I@sh!>b1OJCln!Q?VkitWFzR)Cf3i>S+x|Px}S^h|y(e?*La0dvvKPZxpv>>U&V>#U{e_Y>O6*7A0MKc1Mv!LmG?mu?~FZCi9JSsA~bWD;z)RMKk=66 z=w|de?$+(#ZN$C9!N-Va>;IR4+chrUIl;wNeA5F!OM}(7EWVrLXjT>9S^06@K}JzD zv+j)Vqw?*~mV3o*)Df>(e-tV!Du47CZwk9|CpxiP%h#4~+pR!TH*?LU&Isk@DwbFs zY#&H&+uiDo4^k7_da^Vc%s98}TcOADQZ(uOJ($`0Y zvk%Wt5I!+gtQ~EMpB>@&FmUT=eAj}0V_*kr2ml@r6ik=R)%Gm$E0!*-))sviTeWs= zom=;ATLgE@9|K6gPf!WC43?y4T#J!p?Q%w&av4)pB;3-~g-%GpATe}u zGtlw22hrwjAS`VSBb=d8XzgSf^1eR7vlx|xsUzgVGP4e>I$4@ir3FFO+w?KXeG6L3 z;iXRY)10+BmpC0v%y@uLbu<;6=uY~v)#Mg}6$0v~A!YpN2Oq$|+$@~vrlGhY1Y!S~ z8^FVX@JYk*GRTZ*#<4T?to(>PG(YI3~~@P z%1rYCW`bBw!Dt0zM_+*-z&W&-z59FtNU8TE&{uSa{(2-e(5N5P&zY`~zEP0>XccmO zrf=U0YZyY54F*}hBs*edi<7HfoG4LIbW39VwG|W(M>G>MLq8e|U))ef^Dv}3TNk!s zllS_nmS##gms}>sk86PvcNP@WrWyUR#&P&&I*KBQ+D1xjav#mU%lL|2MBQpYr2oJj zS#(K_b#V-=BwZ|I36l+kHm$C}MHUpFN6V6=b{y(a1aQYWaewU3N|c&4EY(N?&FQYl z>GcTpA&a8Ko-Gw8o7xH6#LoU@Qh)1wx6Dc9N#efzjo+cWU&d-ALzGd%H^UbGf!% z(s$s^Y4M|q%`DH?^>X>wQdf7oWyC9@hsd?@`=@^t6Q)9woeQD#Gq;Sr!^^4;Kh=|b z%zm+0FYU8cyY#)BY^De{H*?sh9N;JPQ$9bLP9y#>)UbkuxrJ0&~bR z?MPG>oQoc{{SDg`g1C7&97!kYkR2K6f8D(@sK9dO7RGTu&}m@Q5raamrLkWmed%|3 zz~%i7lEr!AvNW!0XH^%UF0G>pt51;}Pl0|Op6iu|v~FoQ1(_(DWz>c**^uM>^;9y+ zC~(GvyjZjWa&0&4i?9PN1l+90xc;*Ds}vh5JKOZg^r1{&@x(MQ{-Inws=Sq@!_3S0 zP_1l+o`!m3yqjs0*5hL)@=vUeaoWUN*z|fa!$2rqmOkTb7MS&|V6Tl_i>WhxLV=~p z(TFGZ~;{eHnuzxlfGfuIDGHppux-K#vJd-wl2b$6Cu@K|8X6k!?*!LgC+D1$XG zqG1FcgXl1q;+>t<_GO)LP#~0FGK3BA8zX}FL{XF*~wq6;q@3-8;HhyV>v|PRreV?<_I&;~U!F%q(c(uvsz~U?PjR?86 zc#tCE@7HU|@LpT~EW~S}{LbI5w^`k(GUy%E{kc1LzSQAXxWy4)0HX*N4DVaH1`Oo3 zoj{mT3*^dEns!j{e9vfF#M{1Dgv2*O*E`VbDp#;4K?4Ns!5rSwi8#>HO^CIxnish? zJ&d7hcvIix@iqF76nMV6Ifp|b!T@vbKvu2S{Fs74K{|eXwXP9Ua9CmS3UqF8gDVOF zWnl>|@{h;qOj)tS@=^X<64E70l&jv7m>9<-w3w*4!5P+Fju~2{{J9-D$e3{o8D^*_ z`)AbXU@$R<3L+SCJBQP&^sg$|vdu3vg!LW-m#!<7#J48qCTGI_WIkc($O_*>4eBui z)}Lq{4{M6ALa?$r@-=baj1hi7O6%0-NtUw353o&mYE4w5_7Grx>{Zug_Le{M#)Qjq zSn6pe4MQ6{Uo5_Md;U_sbbCLlZjlBmlkqsQ^EgpS;ukKJsxVe17g?hbA=lX+HT6hF zcjqB0W#?%oput9h3kB&i@LA@eZr5Q%Z``zDm;m4s3s|h&vu&{Su<)ldGvMTncKI7R zFjntK@U*HCUS+tlLvSbnsR-wXGv0iQedL)gaVtxK!sSd@ya?f_8Zj_wOz`7S;j2t%G$4$k5&QSvJP8Rlehf8 z9YhZM=;uXT#b_or;;Hu64Cz>ZK7U|{yqqg-yyhzjI`|js09$kTqEkM>8r@nCFjkkv zo*4m7(AggXah9hr0nsiuRA6{q1KD6HenemFr9xbqu|YK-3rNHXU3w1bR@MqXv^EEy zA|ANQi)M8qe87B*x?Qi7mtH@fT)h2q5cP8BZBAfNR5$jxpq#WpKSM@^3H8YCOy)LY zV5%e$e=9eyNX8=&W%sP9(ehm9Q`4mM0arY%F`o&hJL_3ifg80ZRjx+>XfAQD=T_P@ z0oNiMH_)*9n-Vnn{a2go?j1mXXIj^bMkX{OEq-~nK_c>kjiGzp&FchkjEeGeaO5cPJ#KG8j828 zx8IUJ^hU2zO&80uc>6m4HqFgIK&2#yOHhonP#*~(@-Sn<;QcW|Y;6-su=@Ocik@@A zyGG{2&V34S5ny^7IORxSg~p$S@0j+gpZ>OUn>D-+G5r%d>xgfK+?RvrC*wme^KC$!t6N!_fWRMzYExU&`G1JZQpW$8iH;=2xhPN6SpnHPy zK>HL$dX`jhCfne3kHJu=v)237{}j@8iyl3*-+PH@P@7}=Sv%~<<#A0SMDCr>L;05~ zE}$04)(Zpga4Tfbt+&05s(oVS1iQY^FMLLmNb^c#ivZ{;!d-Vqq}ziKYkcgzp%<>F zpNICw`nvG?l&8E>eAYjI{(89jVEnhH4jul6`W&MAs`M?Vy!JBVbzJjW@mn7JnOfUQ zB2MnpjGkWIDb0uFJ zGsKN~4yc_6{{Hj$*B||@X7cKT2e3B}Uy42AKL@|h6RiTLhl@j?u0QpsPehdQiPv9B zn^@}%54w~mW=VFXY$SUm>*UqefR4=2_;apr>Kj|Uc~?)htrg~2t%LGif)(a3f-hxv zr7;AGEBD@ibnC-_75SayHz0osD?j@t^0zJ%^o%AwBZt|E5wE_e)k9-kO^Az*u!q}a z2+cpqQ{l5%ofT2RhD;K3VKAN8no3~Wd>p(6R3O499EbudC6K-L){c$NpR3<_O!4p6 zC1%6wjT`@G6;Rd?{l^8PTX81; z96S9`=A(!Be!EsmZe^(fI=AvhbzGx)+hxh_mdGmhbRXua*+LN-P(c z3A@Sdq(gRCb9OC^r~~!$4u`Go0H5XvGqb4_II-7kn&~6n{P3e(=IUHXv%WW3IzMKg0<%3%b`%6jk<{5tzsLzNj zW9IEZ{s?Y0JYkwg3~wDGGc8ktwl)BS<4iG5c!p{dswynwDiV`=?EM)TAz(%c|*u$hp%#_o$C8=l4Md<_F8z53}H=)~`tQ7`F;)2dt(?To*~6E|zj89q>ZD{jZo7N4+~GOtTx zr%?yL3<)(f8Xe@s6F=$hw|Y=O4L2fE6RWBOU;+kh(Cq{~DDFatSQJw>GHk8rA+1e| ziLs_9MUmp`a1b;`F&osUP~x|kG)>=!Ov8z7wltBOSkl44KOpGu2_z6twM9FMFi|Xx zx_dK`PQXNA1BU`*$E77sZCPLL<rRc>hE^W-ur%Ar>~c8uM#razsQ(YgLDioRmzjGttW` zJMD@$&dIbY+K>l;pT2=S6>eUKOsK;)K!VJWC&l|2piv@;iNwm$Cm@>rUdQ6$#DSF& zUz61+5LVJ66aa@UHm7wIzQBz% zf4jqm&;)8<@OvId2%3GWe1*&Mv{id12T(A?_B!#2}U>$&-SyZNqN^bnLzl zLbFv1>|%YnZATTvx-rL(OeB$~ho+P_U7qli%oRKBoEZW)@MscA-b#~rX#uI3-a7o6 zYh%TmappsrcKTpdxb)2`ZN-UiHWu_D(wMYWmB?pvT$VNLkaiz?Yvbj8Cp1sc5qH5w zKjn+C%-G^YgVZW2=U|B9ju18-XFXd1W=Igc(FC_aBG8GPeBZntyY6RNeNb@+XF=ZB*c4|bILFAQb?kj8W7Fmfw%MtI!C`1DX@d5G@ zM+im%Edkumw*RdJp~GyM->T ze2I~rZMUB>`q4(30Q~HbUnWj+n>ar*34Xc%1|fdI^liB2f0Y5cmRqv-@?S8SzE={( zK1=)ti97;IyQ>*99G3K?T^$3aX6MHB#VgXO7&mm;4m^vl{V^(tiM*Q0wRzlU-q)cG zsc&o2#~V`u*b*|duqVkreHCaVaX6_EaH4IDPj!GNXMxjT4T_x*@dtIxveD8+M;NlU zJe`4HC(3Acv^i>95c@dfTf5))i~(n2CYFE=YpSO1X-HUIivD7q(%?{uM|dH7-A}P~ z(&}+uYO|UKcQQJv3yh67Q-?h8#25`vacyV_@R!^-H^<_dRhd(Aek`{C(fBLq^hJtz zQ6!gAMNRXeREpbqXJ}`qR6^<6%X#D8=^p(N=ijGl&GI(gv}{xuejgz!!de#`%#k1& zW(-O^j3zXu!A4VqUIPHIeoZiB#J`F8@CJR8{^?PSw;S}$MEHF+c#xl3nL|RvqcUSc zz$_b2Ty?jeY2z6`;jgP$|M$8?KVIz^3qDO6_dbSFKV^7wPH2zkkpsF==cc`R_Dt*d z?8kDbR-b5kc+JHI)^cpXd~JnQW2T1-y_N0ml+>|TpL;u2agT30IWrN-)10)z8bhoA zahOTvD(tY1PN&<@_0Xe3-I0NC>MG}1=83vXEFm62v%>;^6K4A<>rrYG+x-%^<&H;c zt1W}545*WQ!xpSKT4YUQ<*4%#4H#)CK%T+2lf}wzd`na-sqPbo<6=$3%;+-h-8>t$&6<-y;XtzY=o8g zyBM8N=Nc(Ty|_(O=}BUZ`ny*9P}s<8b8-wF(Ma2JvE^L`gu3{){D&)z z(TYU@XBAMZ0-R$akE!~K4K9B3)vb${zx*j)?{$+IdoLZI{_4$_KfO6RzkB7|`@qja zlj((Id*9&Uh8Yhm10yW~#azxUpMu&M6VU;c!r6i*ut*Pl>irF}XXRaH@@XHqCxzD} z7k@p1qoS5o)H*u-7Bdfw7tBFg4%!Cq%xVEgv1k1NtojRQ0*6_t4GlM8n%>k%F_Pt@#4B80 zikRdJVbmauR}ggAMEl|=U<(o4`t>N8)gfRTh?#kx`hqyd8lydv4J1ht>nefHL>`#S ziKrB@c(;V$RyM6Jhb~p^3-`;-E=RoQ(uKX4f=z*Z<+MZ`XG$i6?|baRWEB86H6$qk z`brTr9Mc_pSg24G@gl(7Hq|7Ep7ZITWd-R+(G|eu=CJH^j=o#Fu z3Hcm_aV9B2yyN{DKSJv$Vl)jd_{RT>ytfXDYkSv(vEWW{2`<5%Ai*U#1Z!Ly>Bco! zaCg@PCkZs(jXS{_m&VQqjkszPBBu!D(!V2{lT+E{&;60m>G099a z6MjyU7nvJWgS+^s4tN!UR&O`ZLn zgyQ^Yk`00uSCn?{JRig1Nm!AX02;G5|0Lt-a|xSg`KyH=C~hyq2Z3%eLMLZ zwjNstA80JLjdQgs6ZZ6EZ!o@7mk1`P-HpLAmupK$KX7!@5?(+o_vRV3KrH`iHfZsd z0!<9aK@s01OG$n3&I)qBJfYX%T3)udSI`h}y#K*=rwnkd`G&%-)~;pU!7F4Y!j5X6 zuWt=39Gt7y?3sb}TCB-~FXvam`%-k~y(Q0Pajo?=Q@l^vdG1)N-(bsLtzQB5Lg4;d z>9E=XiGvOwK4NZ6$fxwtn9hz5@2D^P?4F}>lFVA`DCRkT%tvQ2uwImT6RMzgK5Uyl zrWPgIX+LdQ6oI++l=%LW{w;}H;wl#imW)9Lf(~X&7s4GvMK5l3z6JW=0tf z8*Dv#L-~Ye5>fIBXN2A9iR?`K=VL{`X?h6{rgKduVbQS5vrR%0ETv|E5j;7aNi}@e zK@reCB5ilBH~eMP%7QY7K9TZX!kuyDR`WI}Ha?|%nNr1%aTG#H!r&nRrqo)`astRB z{0P7>LNKFus#ndPXi`2;j%m$RcNWl+IgOFO(PPcfJ00lTT&)a(Ht4aSW*_ zLSN(;V)ZBW{QC#((*6?C)~uMNF*V#dZ+m%!wOJ=F`|2;0X4>~i&c{mf^xWIC;iD7T zdpqyj_m6_@)wzHED|qwv(X->vaVu@d8>`>TpL`@FxCU2t9(yx4d!|2CAN=brb}*et z<;!0|MYwf8m=37%Q$Bt4YBd+>{rg!3!!l$zatr0O2z>RVV@7~ z52U$c&Z!yqz>_t8176+6ro_Iyc}Pn14qUR$>0FLE`gM~eeY04sA9||&_FG&Y2OK5f z&!*2=M#kd=K&Lk%gv#kAs;w+7sF2PkJ|)rBmbvSQTL+}dfWCxQ(%MsMi3biH;f`i= z{ln=G0%_JTr9>8o4i3}SK&EA1=<<6LPX3$aV-O+0&npW)?29pNE&SzFi z&+qzHk>s?8GJ?XPWy|KI-&ijS`J{umNf$_x?N}r3w4-pAzu}4A|8fyBo8e@+1i@5Z z7onnPrNr{3{mQ2mh(a2Kv5F-h@W*HI@2-lz@rtu(Jwf^b6;i-Mr-a{`KHb-}TlG;$ zpCn1C*o>~w9271c3LJ>Ha!HC{h^#_72InEszL;_w8X&qJAL+S(Sf^DskRb`^FqPx(Q zdr~&4-R5-}h>CfgJZej7@w=E>^o8z;T~)dQhij3rSr$tcWBsWaFKn7{vs}ypAjcYC zj-oQgyPg!5V^fN^V3zbbwPge36DlYyC3c2(eYRJoL_|L#!r<(f4I&p)6fJG~l+}Z32aiGPJJk3h{cSzlqTT%+ZQSVhC5rT7QmujxF-UJr+?B#H?Iv6lDj% zzCT*s4H6cbqIMXv``Via6jGxW2EJ~V2#C8;Oal6dw-ULWC{UQUPZ8VkA!*Fzrh-{x z)FBuXa$eETg#{ZBqL}llw1wuT%;sc=l|%r~a4WEXe~p6#N*1LN#W9C`ST-Z#lJWc! zL=fGLhz`8++wP8!rT%2ht_8%^ix#?tnfZ`vjgkSVrrIlnsg!nL714JKZ3_2y-Pc!x zqGN{=VkC`>?-;*l&uXtZ1Y7VemA*6p-Uk;2#9HI7(EfOqzA#wI=;rV0UW)^M;Na+Kh@M^c*t zPLpeT!%re(OvfoWrlKbI_}VDe5n4cAcZVigSj0rbTy@OY*^a*dt9U^=rm<-Ps>u(- ze2@<`*@vA=PnJz^8d-|Ci*9Cj?R&;7RLh!ZgrayBf=l{V9!Uw$JTX)?dQLXd@&<`% z&nJi<`;pJA_2?_mFPg`AE3)q=m#J5exPM#Pr>r%zFtP}k_2vhcNPLPs)z(ynTRM^* zH}VGJ*XONE{!;G)A74)BVD`?|w`E_;zI*r8_>cLw4ARD3ihfebrG5H=U0msgGI>@y zde29PDPV(a)bju4469C@Yx1A(c8!OA2L|ow!2gj+`u!ZK@$y|}s6O%M?TPW$KO|Wt z8C7Cx@T{oTPI^T}uAIFoaVL;-4Lep*SCpXGKQvepzg}?<>QPKK2W%8V*9_A2?O&?e zfet^4xWelI&3+Gp`3|&+ zVtd$e`*lW%Xyki#LgQj`YCBnlgUMKB3v65@*C8F4ayeV!0v<5JgvD~U<4ueJqo-{% z7DG%RkAuB3qV0OXP>PA|HhQ+(iAITLH$%0BJ7aWx_G029v~w#qssBYj?&7DX3kFoW zJ|yamwPdXf<2z~%IC0Sd!~d}b36C-%U;cs9E$z?eSv)+dL{d|{4~`fz|rqFdxN6cf4uVr?&HGW^n=~fm(0DH>*^1^(>i)q!Nh`< zO3a8X0_>`cxH7&~;wb+KW<`;?lnn!a#CVm-vY>YKBzmnqVS~KaqpQX?LGK8eh&zh0 z`Evzb3XoGd_nQ)|L0TE>VpZ0wWu6Jrai>}1=sgyro#jf<79&y`g4X}B)B5r3JtgTG33s#Y8?ct0$KnTj13qi42pyv#$}^=7trimZSp0o=V@O zrikM0+eieJIz7_&y!|%v5a`*& zQIBp)qP1asCFeuZF?nO^gvBqj5-soUj2>m2aSVj62@n%NZG1fPo92e7J2Al!=SGwa$VyZw@746QMV#LtvH?|g4R!ACW88GbV+^H*KDTu*l%mZiF4Mw<-&d{5W9ovF!*q%E$$O#cl!^o z4L+9W4SrKdbkQknPArG0BcR(m5XsV-E6S zVu)!`00N5I@s{B22BGWa2IcQdds@Q3{aQlHOG{Sl31fv$3&#ZKytJ#E+AMoLPUY+X z(5eeSRutbY64e_}+IkUc zVFplGeRtZIhe_>6#TQbBI%J!#NpoUYXx`J1JlyPPwCKy7sAvH)@2<=EoP6C#lAmd` zk8!(WUAl*5%$lF(gF&R(hqC5U;p*uG@;6`doikZ@19qYF&iFmXd-5yxS0?O{t+UkI zXwmgc$7FY1O`WaFOQyEAaRTT!y^9>+R8E}$UEmrMO=FZevsXwD3}}O`Vs^B=OM@Bk zT~R!Ih_3l7nsom}y3KQ1H3EY7rvy+7A8pf(7FqMGHG$UfsM1G2YjB>8Fih?Z35R;K2^pb@ z$Kl%`K>_9$ek=Ya8%<;w4m}<#&2j6kl=AutdQ){Y?H^-Mju~SCDUC^=X+6bo$Ajbr zYm++BZEi!fHx>cXQj#6OW|Ep{lp!QZZyleF#rD_&N1eD}46A{V*NYEM1p%2XWxo|mWj1RQ5tpwuP&E|B<}p`zI+sWT5jACSO^hkq0jZZ?`ZT_ZH6@| z?1?!nf9UEZVq(D3c_^TE52)lyPIXgmivN{_S50VfQr4F+nTLNHeV@j&GnV%P?yV2z z)z{%w(8kmd9Qtrb8-jIamNGNS%UZT2lA#XeOK9dyr8H5(O0Uh?6DVnv4TY(`UY-C4Ro3bD91zVo0YJ3+=3<_2X;Y zk`a5I?vQ`OS%FHQZ#CY%x5ou&D-jr-dEp$G{E<`y`~-?_#`EK@FC5loXA?F5eWE-Y zFrix$Q{_^O4>*X5UIP@M8wFUH{&^+3@yx?IZ@SEq_aj|{WMMwInCaAzi++`GJ%%j& zcXiWSFNZVOB95|&s`!#xfpMMvRDwi?PY-?7#z~U8ZeiLnmKW3Q*5DWpyAAU~(gsmv z6HB#xApKcn)ePId7I@+Ab3AAH^F$tSSXlp%R(+y+Q>9KAv=T~hJ+)9CqcHT}v~ z779#yR!Pxo-+Y}dKRfsDjrji0zP3Hme%yO?Qsg%^DW8Ks?KN6GZ#xGa7}G|zmlj${rNUTzUOrw8ul-B(LscOP zXdaccdHx2Ir*?x(2(G}Emxh#bSRKJ0@DR!b`ShSu<9{WvD=lT#z2V?j6jE+$E4)sn zZNTMor_27bihD5v7K5SCGL4YCG|4(_*&7GA7X0oVrj!`ec`P`x0GSm^1>K7+9~Wm9 z51A54Tb*YcK&BnCd7wl%LJr zEL~~)Fn8YVn>^6@aEuYgJssIM!r!C!SfqFf2bQm^$qG?+timO&`NM@nelxI*DAe|w zFANHNoLSQR&RE`&t`yTCUP82dogm-oU2;tEM?A6Cazu6Pf?v!s@RIdVTq*S=&-}K}UVomW0C86qP&)$9V%arl%EUTpkbv6cX~4_-dtSW!F(qU0ER0 zRH$#)Ocg6By$W+u;&bzeew~t*papqa;ov5=*V&^;F(0#yP|YU-#Ka*nv3zk&XK<;R zGc;reFD1U1%)~>VYtz!@l!+k=mLYo_lNcwgbFFHXu=I^moffh47s`q9(r>FbafCER z@0&ek&r$cvP>Cz?TX=W|R+0>#_nJJ=g?1%j&CiL=U2!!>V?0KD^t4AuQW6p1` zlMqL2TrMBO-q9cujx`EuOpT%XX%s*3zMl@s{`w&O7Yh7BaK`YJdRBo7@Hfp|!7J^k zW+DFL*`)fcK<51w-y-27vz6NyNDN0p>I!_b!W%fhPsGn5gh+vX)ZCz}a zwZt469D+fhLXmWFT5vB7 zXI}c3$;eL)SWZYA7l$rDe^Y#LH&NLjoBiV_ZuR4j&sENyEG25r23{MsI_k*h^n(0< zWiPhP9Sxl3HcUB0R*Gj@U+JuNY|B3t4-;z;<|V|mkyvGMV~*+<=R>zUtINPcB11IS zOH@u+8cFUT%38BU*Hc+2_=}Mr1rONc-c;_3Hw)uceX%5jSd@HrLV;(MV3;J;se2}Z zI5rK%BkmS>bWco+&4kL}ctW|&Y>=GHb<=ThroC8^|DkKVf5HBQFsxHLQykQGjwjXM zei~OnLsXe1n)Xj9CwZNpX;;f(xfnhy1fMid=$!x}%d8e_GHc4>>MIivG zRuo1s#l*znt*MJeI(ojxDy&#F3tkOJrKTVoPlmN@M>m9{)JwlL`3#E10Co6-dpJP*MO$cZx+vZCM${sAv(cw>Rq-ooo|iDa7I~ zm*qik4MqBery4bI*wA{H8?w+Fb|d=a>f)Hh0|t+XQ+-*~hNWWzAb{q=+3E9B*A_bb z0s1xU)r7G~cWaMb011(=$dpo7WOh~U^6G-dW8j)w8#Oej1N*cT5Xd06)93YR)1xm! z>$SX1jhJ>)!hj0M8)AltN5D;>`d!UelvhWb!tW%S_DVZYS)OAmX~p`BHk4e@QqTKc zT-H_-zr&DH_;!ZLEG{aIALLQlshi&t1QkV@bwx$p6cn!7BfiBz#qMt+i9;F8on9~A zTe9cckn~c{yTFC619H4v)f67n-zyfaf5$g(f+W>Hzgp$B6lF%V3`$AfyRNsGLIk+9 zY>tiC8Jo5l@uo7*ASl;--XdWJlRLEhuLLxwG8p%nVmmW9Z->^I*I@HYsmSOOlwI`9 zB5obLT?tEi*uqjn#SB{ z>up!Gmqm>b4=LWS%_+iAq?k>Yd{yQ{5nacXD741;ZD75S9Ae?|3AzJuVf?6WTsQ*z z9bPY<2nrx`%FH+%y&h(=Y! z6>mt8E^tvuk2of({~<$EUxdAdI)Gb=>G?V37ph#b!#;-;_OCU``N zIw;yG&M>LAS*w#hPZSPdgSMu+gUmHXH&{%VC#ALKSIRJ&%4A}}!FSB-d!Gv|V`%94 z8vDj_BisfxFH=O3rUYs>qgto;>N&{*iy>`}KVPPU-~9+l6WcTqBECvBget-QE1L>2 z{?5CIrF%uhAO6Qd=WoU$O-F~%-S1;D`b?L2U-&Jyr8pAIMEAI}+g_q{*Hzz85gKHs z;V~6weiWDxBBBxXrNbT}4GZk2q&_7mhCL~9jdTRTV}@C)n}!`Ku9%FbQ)oJ3MOg03 zLS4-Xbt7H`a1cO?)6Z$Hq=tzX6OD;1D7w(W3+!HA+~307grDYVU^UjS^ccQY67pG? zEG9u3{Z6QT1Ns*1qqKCQ_BRxO@{vBswC4)K&=OT>L2Z{i{OE{7FLRcYn_QDs#79liYG+Q09IWG`-p>BpEn;F?P z>DM8%tGQl|XhUvB!FpVE4pV*EnQTVUtoeNGI>-D;)4M-Y2Fd!?%E)gmsGTft5Cm^ciU4yqim8 zbz@1a&qLQWJT8aOC2OQ&{Oxn)J7zj_7wqdFh#pM`{x*w4hSF|QnP~|`^q7#;pdD_$j zvfxw9gs>h(YTN^VxSLA4vAA3)?W2&-MfD4)={^gI(WYd|v)AEGW8ZKv(vMN(S!B4z z^H}#hC)_hf2AwfuezowrRJaLAI{XkziXWT%bcR=g5Hxzn0#ixzvsUUBz?*SBc-cWLz58n?6|B(LepjoEy*P<@e?T z=Y5Pi)WZM63_=!UKHmtk>mE@wx^8l38B`CR|t5dmYE^%D#% zZ_#{H={Tq$r_MkjbXh(5 zB;`sD{|(I(6wf6kura4hcrX*#R#Aj;rH<7+6hqBnecW5ZMfGClDo%vE%q4L6g9wi* z3E;FgV(;x%FMaH5O)@l=qm*~GC}g^j*@**r7Ys8+dn6p|+R%=QXZtTH0d=I50OF67fXo2# z5@6BwX9}&DaCv^G2p0j0B2^fWB`=DZ1|{mxTW@IAeeI^uvcQ~dtI=DNVblFDYUN^bv43{`c}s@;c76#WCg4y9g8qY+HMpHFB_u$ zhQMaOgQ}M0+6|Wx0pA$ME!fLCxl;moN#eOiqf8gh9+oy$7gCY41L!R#VbDTsZ%Vix z&Up_qfvFq_V8-|zPny;qxGcvC(m=ToLUesd;edAfbnG5IcSyo-V!s1ULbggf0i(t& z2UVvQo*=zU^xBr%tjYop%qF7sZr%CEN$Z%_8w>V6kCXns2^VyMyhFx=|qRcT7zQ+8O7`<-^gJe8B+bmo#H$xY{lQuds0cbaENoO9U(5s z-^!^0hU7Fp>&1x1(lAR7zIn08SuqReP;v69&QiYL$MGB{eBXtP*`~$4q>q}pD znHJ;gVIH%Fz6W^oN*&Al71o8t%0t0mrrCEMVeqcL@?gL@jcVAXc^bZtl$I_ATh*^J zaU=f#;c8+1DZ1i(yA8G0Nd1uAH*Qnz+NTbZKPU1uq0)GRc()^8p57$u7{#0Fwi@7Mp=O7aQ3UzL=;WL*@vRnr%1IW{-4QU552fM5 z7RS2k7J48Ljs2Ed-|$aSyoIVRo-IU|$VIxy-k*n|J3g7IeoM_TQg(R5H8!cI4%Y*b zu>c8>tYo9JD*XZaS}bgg*eS!avMjI|{(n*@;D1{kVQaTLB+Qb^s_JE)*;G!1b!l!4 z^!Y$GrQ!#LDVfdrzP(kf-;&>oM_Pz}`d&&!NcwGP6~+{?zF?(GX3;M0f%r@9&PG;{ ze+tW_2!{kY;As}%eGQ~%{xg6**daKXY)PXn>R~4bW{Tl`IY^X5QM27ioFH*aF+i3m zx-BGwwKk=|-i%T(12JD-1%;5f7iZZKfBU@eX=+%H_R$h|t`gTE`^`j<@yY7fyiYl^ z!B5W?$>l}fCT4B!KK90o-lGQCgy#M5(FgTaEQ(gXePZIwxME;p5zSVOwQlT3{*y7j zisj*DdcHwcc|0>V>NB5GltB;Q2Pr4H-b=KT+&-i*y&|>K8B~?z2Dy7lKNZhKfAeuh z99uM&)W+wJc9t@>R@mo5rynXkQi3nBId5bxGKKT^JPUq$XNo>Ehe|-6F2RD58$t@) zOkW_^doBvrYYv1N(QEW(k~T`4ezcR%bErW~7{e>)KlBm`Q+Pty?UcEx^@&*#dCywJ z&C-xTiuyI|`w#jUiYB6zJT05y6K%ux$BZgDJU_N`b7C=YUNxh^os5%h+I4LEJTA<^ zDQ&}Ua*Ok;MAnqrGYTl&vca5vO{)*G-onS}g#f10-0a@$vP~>C{Iz6L>;gYD@Orx- zi5I6bK@5}J`-toDiHRRN&6?;+q7$<_uSep*$xo-6(Ttjt!WnMV6z93U@!XCS^<)Tq z^YgE6+se0^^CxoU(KVssth+TY(p$h_@U!J1NGd^(82OI({zU_IZ~2M*h7%fS(FIB3omSP z=?%8!(Zp76Q*DZQ9vlu&)*byi9M<9)+O9I`L@1JLTn)9oB%{I#MBMn9FZ>@Xj1dWd=r$nS3%DU&tTi> z&tutR_UYTMa#^>fKKCtA36_ls+jy=^L}*D8{^Z&McEunS9JMsnXP<*lCEb z{t&;CT!-{)8J?zo{$>tNO_EhvSq_vS@Xf0o>9S#O5NDCGfvZgAhI|Yh9j5l9Ez)#Q zqicMD!-*hO{wd}ZB<)z8CT+{dl2iuyLNIJ`GBkHTe2n68rV~#Xm%mM9U;adZ6I;5Q z;Z&bz0S2j~ciX}Ta{DQ`RzoRPui%>sWQv_hqjLgL?mSN>l$L~Kvf?vhRAzSMi|~bc zH*kWDt87`*S7J6xOZ+i}%f!gCYmL?I==$A*kA`a5 z4V`OL6inlpzZ}4!(&$v`cruYx z*lnXfN7K2F>PDB9KA9w1D-WUrg7ZUXP(0jX;NxGt*wWtfSglY#lW`t$d*9ButUCPp zJnFi|FOR19XM2(zmD_t0;UFfVKney-FH2>e4f&3#yL@5v)_lwc(J#aV!$6ue^GL&v zvthBRUy~lzeq1ZW#NK211G||gdQ_q za(y@!Ypew{fJI)1v!Kf*x9O&CVmoQcs5mC$>W8TUxnS}NQhM#NR*dk-bZ9U zBF@V||9!W7oUC>IM(nJ120Et+YvGQ-OBe>ZyGNThf)=5$MVb$PUL`Wz@$!D zo@P(8YE`g```&)ByV=IP6vU~kImP7ctoyib{Y4Ew(BIKJgW@LNx&M7bs=6?G5Vm(an@`!I(jz4J%x@+uX%MI3rLT#jD?MvX)p8O>#kgi@2nR zmZ^Kj>`dM1P3jqiqhOnNb;;~}M1&uM!p-u!a22?6y>t^v;7Nk_(yv zL@M8!AUYSrY<_$V_(vuz+y&-NYr=JT=1KqVASWAsOW}y*`G|_Njp|SK9;X@g{;0^9 zT8wL_we8=KU{2WS$MsJ4ozuhhO-mH|Of_C zk_7qF>YFk|Kgx?1MqDn3aQuZLQH)pV%eb-o0A8ybm;5p;#Y6Lj2|;%kRI_|cx=lJ; z?M{*vNY|Pp@gBxlHZ`EVchxH~=3JN)x!OYl&d=p{#Lm|&3M@UD#fiKSEARUx^;)UW zi+8Hq+i)>Tgr{TiRLlc_jAmBG&`n(VOdsHmmUph@<&GUIc{gcngcQ^cN1pL2CH0#_ zx$3JNGLhJi&tT7JIltD^F_E<5EJbl5gd!GC``La1HS>a}bG_D^nn5@-OG2{dAY**u zQP(Bw`^nlDBq%*lVog`X;oTXZ4!@599H8RaIS-X`evyyg>6u3%MHtR3m|vS{d*`qS zzEI)*Ru8~`LiI{uf-H#(r#+_f163;2 zzx;mD(;_(dawBwL6p392|{=@zy+ zcz|zgqA})OyC>Uo``ITMX#qdus$DXqgw}bnzboFx*>Z>3D&%zhzN6v-opM&_pI+y5 zzE3c`_xxjx{fhdt*3VwRIj-Fr@6FXnuVbL@i4Qr7Ng|>aCac4(9rt@uw7+0Si(x79 z5OMjaW>_f&y_1^BXu}r6V)yJx!f`95KvH?OSD#R*{| zwPo{2Gcg65xSy$=3EUdla8gzhHl|M+F4QWC!8W8`;I-~nuquhN!!f4V1y7OYX0u}0 z5!Ht>qxcTEAU&~mAtYqrVJO^XcZtVHdn0n^s{EPE!HB8IT{Jl>P9(1>C2>}nL`tX3 zEi7F9GKt4i1n`+G2hAW`j-`+7XQOU+#MYN7Tvl;PU0neZb=oz5OyO|hh3>BDhtW3^ zhK7FRS0R0MyV-}AwxcI{qLl#w+?F2_E+~3H*V;W({P_Iy&l>*NowBs2mvX@Zy_)}Q z{eRz_)Dt+H8j4mrukQs~@dj+>!Y#!sZd&$lDHm%(E~uSwGi zYgOgV^pb0Ybx~eTc&j}-%-fdc+SQ}l2h&lA@X^Vp+6X8h0W5s1&1N8(%Rxi1@*x0! z6lv8tFb9mL59hqw@VJH^CUCoIowyv7P!=|Xsc;YursX{eX4MS1F zIiGBJKku6!uQtMQm9-W2BWRhI_n zCJMh14Xb!Pk1f958i4)HtDLv$MthAX1oSbFNe~0Q2b&;>x=>J4>p2Z4^~ZfR1a`Rw zT`IznP!ef13W|f-SLuCTyC!L%pf>Ci&{sa{<*T7DU`%Elt6AyVqmEdJcWx%ahD^Rv zxH+TpiY*YWs2~JS7g~{m604Xw!Rz5q{a`YX8h(7@G)I{tV5t^dBAH&6e@BWEL$nF^0T*pk zbUTyuHEBkFczC=C$``d{&D)BAOYK2+e6ut*kNtAA@sI`u;*{^xNqG|M<6M|8=CmF$ z>-C-j$8B>kQ{9^BYhszW=uY`^SfC`evBo5Q537g6E{!iLi-r*9K;wBqX6t=44FM|G ztpYK_eqQzDQp(m-G5<)P?e50zti8L(UI&siA%Cb@U@ARTWOJ#<>SqXEtGjUbCVG5a z(2wb-W4%eerS2vRbZzc|p9fvOfi0i`2sii17`uu57UE|971W2_k3>wYIo5v3HnFrX z-L#v%&Oq|My3GOBQ4zfHt1in+RY*yJP! zaHpUn;6^At6@W8C#k_YnFxtix%`8uV$6Ld5HSs`U!AGt(t}>y%$f%oNW*gnIjE>!Ns`B;gVZ?TBnb+%QR7!R(`E3E6(~it> z1irw!LAsbC_PEo^2F-?>ND}*E`N=r-lu{jp%#VMvQWvrTQq<+#DH&RbZQ3LbC*>9h zqXVQAmb>F$iCH{t76zuyTn~clKf<{l^@}l>Stvs61y<9F>JTj)@8%e`M}}2Wz(Ms) zd0G)_ViPh3bw`%cXduWbcU$n$<^Z&y%$I2BY9GKGCbR>QNOz6!=9@+c_|m;zZcrX!+TN5B?)F|IvRFn5W3t5dt6oZu$)D2^ zkWTV7Rqn`x+r@gF&&OVux{1b!DcLVzu55midg5nYoIP;%g3d9^GL~K;&gRd&t@h+PrNYaSCP+)#mW??vu5f%sxcXFhJBY;8+E#ILMZF* zcr{Wi734@i z9=<;KPOdT{nx9b1vcjCd0Ad>1jF<9y0%>&(OU`T-ZUS|B+m+6)L3At~@_!Ehc$M8u z@XChO={i+zpP{C<6O6vxVh0eof5>_tGr@Pb$?#IIh*i{Py@{I?F6J?HJ6}!jvh7Ms zAH*Ib)&Vjra@bWrBt6JCkpMOjMyUBWg{3*tw9-W8;5l}B*D%bkb}kGC1pWSb@sfPp zl5S*$ja4+laTGaam-)5-NO0`?_Sd}qpI2`tyg~z{ z4`0YiGkDXze@FD<|IHK6tlBR&M;iS?j2#!25GQ^zJJkm|NKxY4FRvaJSH8jC=OW!+ z;6E_FbqVCZNq8s4U#DOUT#S=y<)7|wTei6E+%rW%vCCr;A}?q*FVFo{<|5Ze(98~5 zEM1JoN_S;7rqx+90{n#%wiB#XAdL5Vu83t%Qni;fyL6S`*p5bTgM%B^yh)HCP)L@* zxN3;>og-EuMN{|z4ciCPm%#j2vzP*DBYFrL+}$m{+Zv#7iGa^^(4s z@DD{^=i=w9Bf*XDT>q9lATQW?%~3SszhC|THQCj9k#hN`T3i3=E&lyx|9u53qjNVsNmAf zH$d6KH>1_^q(?l4PYUlwZ_3(~{Jpl*5_xE%QecVNNX^WQ1vbaAcjx9N;kQA%hF&(h zL#mMEkTm}APtNS^EhCAqzWKq>Q@L7b{nMocldm5Bncx3yJN}n*^#A{9(SM5mT^%Xh zo7Yp;o?Ti6L?SmG6Z?@)%MNVw5N4+*n0(eaYGZjz4rvL*H`OId{xKu>ERt`Dk-%+S z_d`r?cX?h$1>qGwXo`|#77X`))U-;*SfnfeopxhyQEs{=1dt!!^ZNQgOz2OW z+XH^~qlkwa9Z3*pfhf`E2SqeeRdDA_ZN{jwb^TJ!d7(m+<;ljrKEu7sdK>qvg-lHs%!zSsl^y~0-{+B$<_6k+Papk-GL`TDn|$Q{t?X|fc<)u6Y%@~+;%k+~UntR@zur&(g;FeZ z`I9^K?DyNBC%JET%nkCTBzp0(Dhu{i@_#&bl_Q zzT({Mu>oSnyiSj83YdT`*^H{`F3FYFEvpds(mG|$ebWC_pr2JuU{HAQkX^z=N;Bc zVG|Ht((iZxywYsLIpX16x7>%4>ZFjDC^x_~4>GjrN>zGOO;RaDY}I&71A7g-H57r! zW=j-{$~#+<6!FqXQNDSb5a%;7snOrBDp#P8s($<%J%_I_1d|@f({l}|=7Cm@K+8eG zf&>r#7X?Ytw8IJq8!hYeU_W^`7COrv@Tv)pH!8|Z=G-gd7~%>nG}nrLL87qG2hTT-5t%K%WXvnKUM-j!tWK~cTn&)CB;uQUDhz8YkG=O+ z0-w+}_?FboF8VpKXhLTCjLI@mRY zsh=5IHJ_i)Rx9uvom0Ek`3}P`{7VTyEm592fqU2_f`KL%|NWukojnc?%ID@8x098W z+hd`bDAa~=Edta}*RH0RQ+Ix4C+ad}J|XE2DybYy#cXsOx`pK?8A&Vmd+9FZfM?%4 zR&#rN@e@;?w;PFeYm5vE5=K(NRlDU4Ff(hHnYC5{+J+7rcm;J0G+A9AsohvE{ey zr(*W1Eh*aRR4P;XL;2(Zu81&Eb0;4p_hyM&lYT`ItFXu=#ilw&Gz#+6mLwxCfeJN4 zo)*KJE4CUqtI3=GB*@pFYl42rW%Wq1E?o`B{9_+)9*Sj8gedXIVli!`)PVqg&fSDF z8{viEC+&qPgMQYW#WP+Yi*7rN`?woXeDis8idE<=y&(V0Wgs#;9Uu5fZw+Uchi4bn zxLMLaoC0wm>wlg78=Qq^E-)q!^pj`yiFx(yl&>VT9@xW2rmFo>Tlvc}Lt#&b*BYYH zcJ#G(Cg3fvI>E=`g+^G5 zJlLwqMsL;f@ONyuuploG*#0dtuo%O9roqHBV@I!@IzYmW@V!6ww5Z2mf-(P4%ag(D z6eu`qzPZL_wO6;pe2MQT;S*F+Anzs>mLwjtA`X-k!w@uY2AqOdZ^BH11cF!6?Ma6} zV-K@?OjW3tw=0HVo*Pr)MUgIf{A43fOX@x)F93Z`7g*Jh+dra*WE$hrK9yoplXbW!J#tOvNAD$1ta8kraQKKhljIYhr-Zsrb6*WtW-EIb6 zZ}m=6lXKE}?hoN=No!P>pu}l6;OwH;rma%+gJGHu3Ax=$(@OCaCqjEgMp*pjsl=D443{jPl&$aq~wZ4a}EplZH%MDXE9h3dkLdrwg78y>S9QPlk6bA zkvkC^a10pf*|7(i3kg{JdCUcy_n?e=x*oT$2L^Z6sKkGv?X3f%>bk|@GsDm|bi)9W z(k0CRLr4e+NJ&Vyh=d^E(48VFFf=044N}sI3|$6_(gF%1slS6h&wJl{-+SNh{_)*C zd(PS&YsXn@?S1xULXmpD!s56ZP}Hi|Td`Y4hnw7e*Nlk{bu1s()v8f{q}k8T*&-fJ z<%l4@%FHErzb-8IE=)Q=;_FX~dy6u*s$Vs<-};Pc@Gcb+6Gx3njQBdU6jBczG26d| z)u$;2I!32>*v;C?U@~a!CxXJ7f!q|kKrse8zAz?Fox5f<+msY%C8+S(6^KiwDfIL5&)hH3Zt>*$aKia|++l$r=ISlS5!_DO4N4(FsXGI6lGPfP z(=acqL5+lHuNw`aqcU;k1k2T-2I8=K9raS;PrGgQQF8%Inc()daa_WAlzqJeFHF4h zhN$%G{Xx%Ni7IDyPm36OMiC&6>8+Re8Mzkn^^v4O-Mtoh$qC&ETsQ8V)5QEtZ8^Yxobk8qf0NlMCT|@aHYTcKQP#{*t=;7L<3?HIWGLvUD#@;GT$hU{8KXA(HhEx*T za-bZab=EShhjgA8O&C&!!XI03OMVM1ekf2T%z4-9eFK3mHi`K)YT4@>W!RkIbj(=T zrof8W>3fS*+$*CVV_p4nv#FG146V@%&m5yHZpai4fl7fKR|GPJ#DevZxe#P#-Axr;iGL-$|f;!Kazzx45SZO!t@ zcj`uqLBiB-g+;q7;?}Bd8UaZa&GOh-v-IXULe_UL-U%s78Yf~7a6M;=ojvyEJuLD@ z_zGw&Y#0(OS&WBFFrqtOnWH^k&&S1OSmJ%UOjN008Z(9E$^(y}y%LRs2AewLlLlv5 zK<4!ZVI;C5y7z%+o9=%v7C!VCslhw5h}_IZy3 z7gd48PlFmT$G5YUqg&$ii)@X%R(RK6MI^exHLA4U-g!mg5bulX4j`XJoL3KbTKhh< ztgeO zsv8&{Crt=u+6zAKpkMdM;xv06Vc(cfCdb{jRH=Y6lOV%>f?KxENw+awf7eARqBvM4 zxt*L5GF%^jPcxzU8@YIoLawSy_EqN5&{n@j4GWPNvFEB9I-XhFA~$7$E4`xaVh(s} z75kbtc~UPUD<`k0(T1Pms~p}K_V8qwd9AAB^r`lIs+F*8tavkD)cd34I&20di5LF~ z@hs7vW8%jh?4amWshZ>C-jsV!FTXZb9$+-=A9PuFgEd|edh@I_)NJdmwLurttS%po z^rK0GhEli7TsL%GpBncoFc3C+U5PPgAsG1nQpKbBjl?1S@elJLtv#;jSA>k7ba9qd zFoQuQs}GE_I2wlo+5>O`V7GQwT?Wf05(sH`{89&Z+(FylWC%m~!j#p!Uc(+wWZF7O z1}of5&W1liG~se9qH4iKaL?^RQQFfzy5B0*{ z3c^;k6gnCbD@N~Pc+K#=!`ts>%-L=bhJ8+Va7KMzk)2B<)!dtuPL@q-v#{`+wJKz? z4risKlA#?nmvp6^9XP$!=2{Bz2cBMHjT(+AeXQ}SS~RDWBj?d$_pqKwc}MU@F)#FK zZ$j}x`aKDQZ`=GUH#LP!>kbkeGaS>+ZdW2h>7yi{^ZAwe=+v?d_+qiu^C7)8W$M}oB8=932tL0{ zJ`~NQ#fGVpqD+}{m$Y8=D;g!-C5n++lIO^1%GH|b^Pp8R=hK4E^t!^oB*r-@g4aiv zq_b3BbrsEht`~pctE;VlSp9M_IIj!b#1Ne_c^qz$6eN*2<;fRG+RyIK8$Y&!`g9+~ z>3z$VmRt9*aYzWAcq_EB9BWPVBP~u~HQT?`>u$;uy*A(ougK!0}`I-okWJe~a zd2e}oz9hA*RgO_R8Z9#twWm7q93wL+ksQ_35#P70t$!#n=&u^stD6-N**a~eSLGDl zzG)}yz@&2L&OuFriEc&f0~_!FR>_xVekaPG#OZjmlc~k`$YigV6Ku2eefJN18Q5%q z2L%e}JMZ>bVxEDApvP_7H{grbK=T+~B>iYwxVnlRl}YbIzi}a7_V&rkx()7{e|Gkk ze`xV!?Uq;BRj(3wBDz)*_eQwP6oN5^+smnjfcAjz72>0vBn2^!2{DyvMkKcVHA|6T z9U*Vr3}dfG*~aJkblI(NQ^|NsPJE%)ZAp-kpu`HP*B0;-UY0Kg>v>Yj&kAwcMfI0N z{VTo1Ux|6|uxSsmG-L=pzQnIB)+ok&rFL?*GBmMIU;BnYM#l%bx-j%{fhG?l8ueAm z`dUgxyxX-LPG05hxe>cKv4b_bp^4s``pjk|yY3zzYZPq0yq;2Kl56h1OS*5yBT=4R zTV?2UWyTV6I)XI&9%%$R-R`K39nC0^Ae%xB)78@)@dOcC#Hy25aLMeMeOU`Z0q z79!9@0L?q!LNid65yp=9mC%Yrz075&Lo?>9n#T7Asgn+1r+0QN-1~j7BE!!f9n`62~L;0dtxso|qSsCyDV9Wi5c@-@MbvI4%8n=`MXNE=?Tov zw)=CFKeI|*sh=LF6ZhWGixU!z^)LB+i^(_lnsR(mafCAbOF|=^!AfH8qa5)C`n$2y zwy)l1RKHpl84V|sA$`JD?#0R8KH2hx?0n1TMeN3v_`5>OE8ZsU5TZ>3j58xPYK0)$ z1)`Q3qXKtMwt$Y2m5%B3Tp}fS=uBHzUZfJ4NN7zLiHWVZM7b4#$L`+8e6Ot-Nf`a| z5^BJA`Bv=1ODsz05Mg(068@fgyMS^-YU-2TjK$K&{Q9(GnXGRKJwq{$o{01a){6J& zGWNuVce^4pvvz^mbRXeh+4u$Ye?5Qt3z*aU z$&>uW`e*xtukwP`rlB#TmHhm1UF>u6XG#P!p3pJ$+)o<%eX`Qh{I=B8K<#nXPO&#_ zUdBJ1L@(b7-pN}xqIdbhcl0hp|9V&e!6UvPWeLXac&$wHXTG-^iC5{|jMHBv1!B3y zcA2@P_)|We)koi$%%P7?=e+-X^1G1?p~o3*+l##;263a&H+p=5jW z$&MPd!^&=+yPfqa(DP(3JSkaCDiO4nIzdn$&9Ic~C4Z7@Jfv(5^r>yGJUJ=dZF?n6 zBgZymPxVa8C5ltHtRtU-?BV63cX0PPDJL6fwL}75yk?XZ*4(F;`90cvFl<`Tc`o1F z_@VSOrRV;?`T+qr0KvIXxDZeQpn?Dtd+egO;D|yIWrI7Rbx3`i)QR4&AH04o0t8?X zh!Q)9WekFE`(?3NG#+_m$riXN+c&~`Ni)KFQM>`VI2-WlOgZF{4yOsl(HD>EM>5MM zL$`m5YAU1*)^S>mN)RK~pY`COCzPPFZcHgX1PY2|55&bDJu+(Ct?_qlElqN&x}&x5 zv=?0ji(&THwi-4LK|^OcDuOcA>mPa^3ZIDT+Pq$FGTV`Q*kc|Oil5Ih&1p{>)4PUO*TZMI^#ND3S8@T7El zsP#gr1^#G3@W+9|w(zK1Tpk|qankBJJYSj_{iIb5(_OCt2|Wkz8;|ncP9^@HhufDM*Ng# zNPQJ?Zl$-XT)%D=x-!pN$}oWHKm4dk8~GvVJ4b35HIcuEdq$m(g9rE6t!C8Cb(UP) zR>&L7-8xrKlKAwcu$7wHz0Oks#qX9r3Qnbdnc#f@NgYG$&O7(19k;$YmL4n8)bSwR zkoSq8YP4F<@~4CAlS>(3Ak*H)pV*)-oBXbu01xc;3|)wWdA@1-%B*z zZG|!jm@upKyw%#K0?wl(2%Mu6s34Nr-+8Z4a>Nx z7)dK!;2ybz<8-3mL;U7fRSOZS3WAjR+=QGE!D407IXVp6eLvF(#wxJ~8+RZi792Ht=|vnf3wXV@sgmt75Nc0DfHQA53Yp~62gwfRiY-N z_8IaViDKz&zcqg->h|1G^sn192*ly0$Yta-=wZ+#7eW$TO{j1vyk6=i=1U~ zgP$%T5lsEOV5;lu(`boJ7&_^2swakoDnnped+_J@;r8p%)>IvmYvm#z%2IT@UAnF4Jhlsz3yS2vE?w)(L=6IG5o2UU*{mz zLExU!9e!Xs$N4)RYvS#~EBRCveoQk{JrzsW!%pmZq?(G`4@tJ=F=o9a>skYK`bv$h z&Q=b1VZ?FTC;LyShwf7ki0T;r*T;|>HK_mg39R}PfUVdz>&sJ&_0DpFDRG_9^o{CD zg9o2k*7%C1Na9xD>qpc|v3vIbmgL<5yj zV3cx4OiT=GMPTSgwDa7QnXS)e53$l$E#Je?&FBh>7l*QyyrbO~=DD-_{y+J)`18OE zDyD0{<8*)3Gt_&s*aP59k?F|2yrvSfD74eKe#wP~4BIwq} zN1;&2>~%1we^v3+w{rxt51n!EJIHZ`c{iMK?wlD#t!d)tV`2#AKS)U<#c)(j4C)4F zgGrO9>=M5uNfI&aMFqJs>|+v1lcWG=@eepFx7eOP<@UfEA`-}D<_4c=dnm9-(MX=C z-}yRhN&#Zqph7{Ah&I?CU(RO5!N>v>9!$(GbdyYlb%QxZmQvEXNfPnS5F&hDr@;vJ zofs7N5Mf=Pql>#&Sqv$Fu&{}^D2mQv14hCz&ZRUPNJy+@Vs^$s7GvESxhO;Tx*yp> zQo$%31(BZ~h^9~7teVociT$82&|Bo)bKEe?XI4yLY^jpC@f-$k`uG z=^L=1e6`9!hl?zxo2BP+To`XrfR5^4q~h%o1(PHg8#Fhir00oWRWE!XE8(CZ60Vh! zR-FMqThAic+q;eJm=`v>+7jS9}^ETlRv5g0UN zvf})*QIzA}BUM*G0E;i8Ac#=G>HDdVk$|6qtwNFzI!*Br#q5J0f!CEyMaPO8R=>#~ zL3e#^nh18l3s(Lw9K@Wug64J9Pxh4>uqa^NgCC)MI3B=VA>;}-hM2M$_p4F9e|#>0 zz8DyBQ++oT#K&(}nwa$#WFgiC<1`pG1Qx>G3$3b1_YM8EVt(buZ#2hjbah6bNO~}K zHe}ksO{2Xl=GJn)YY7hDx!txX-W7N6fuR1ISwUmvgZ=5JPrn|Ye0Fo){hZ;3EIWOz zWaBK%Ci=5KzS3$(^9h}=5xz*^~ zusXNoudh@`2V5o~H#S}OyZ=d?$*_kmn7Ix2JDUWo5j=Ex!ne3R-;x%`o90E*ckiYq z@vrqBf$4MO*Ou9mGei3&z1G(TMepX7bUd(+b+fq*exs5Uwe(Ro=CoTvh36}2O7xV( z6a{vPKB-7a_0%r$UQ*O1K~h<6&_+rO7EdKI4ALkijpCrPi-1z|;!Ue=q!eOG85s@h#TkPM{qA#khWv zJ-0Md`4zD2F`95<4GA!MtsEmnVvM`Dq7$=M1+B#q*$8?|S$Vju->T1Q(~}?v^-F*K zmin<+ELz%8e2FV*DB}8)h}t{bHVnX~2vP~pjv+?QX|;~nclw=dM`p%0vvFH5SxV?E z)Lg5Aer|W3v?w))kP?r0udUP!#w`GCL3%%}_8RcJ<*nei%vo;og8)uG^^$PA&&w#Q8I{bT6h4%*7zPF#R5F|7uvgm}LIpm#B&2kVuvg|3HMJ z{C8i2W2Er@Q~s=N(I;Hbnjui-+)KDBJ!15;hFvz%F;jMLU(XLFxgB{d;d4J?&3#18 zG1Q6okO2HjPvzd@&!P7v?rSU72-g{AW_Z8DmseR(2T#$Ajq_LiQS?HS$b}-?Cyvfb zBH^K6SLKgyI))bSy0=Ow*5)4l3~{>RE?KxOt}*1j?|kcNsN@gkq#a@jr18Q}FKPWE zq;|xb&$PgkvH=$+eNh|9bTn3Lt%ge~F)XafCzuHR`BSpL$U{4t(gwLQ%iX2Tl_r!v z$DT)QC5qLP5dQ#6boKBi1as!dsY1aBAmB_E%&tIp{$sjmqd-QD5g02M>kLp?;oBTTNN&h;JCNJG7 zJ-8uJdR~j?x^OKK4e~(WiiI=Pf!5&vZ;ixQ2gs4<8?NOcF>-qOeu@Fu<26@kq`qA2 zdIEpwg9dxO-Rhy+tmct?;>+FNwD4bF) zi~hf}A`4N(yFB;sO~&Q$$7MlWfhbEqUPJO?4{#xiLEj4GE1I!HxI`GUgCRwO@pLUN zXs8ddD>RY$NM&En_Jm`T_=Q)G(!dJI>GA{H7rn^(pZblj`wXXC2=aiV>7e~u|M_cS zzR=NWXSzi{y?kGyrxbkquhIHKi4*6NabM6DHj-zXwAolNQyXdOGlDSEEtWw$8e^7^ zkVkRTwJOQ4d|$}n zd@i#qPCbOknb?C(zI>ej1fD2K7Q|UVbPjn?%wuy6vki?_q-L$`CsGVR)dzix#S$q@ zVz2S4twpFXd>tW?NJ~zec0OHJaDehg#IAglM~d2?(6OZGx#!qGxB`+T;oVMlX}l4` zpECmaO4Un7S~V2mitr*_#Q?CbRXAlu?C@M>#1&v2Hou|_&KP$ZBO$;!oCK{RF`r+t znG}QwLik*uwS1g!fAs&uu&%|$d^}zEfL|#PB>ZNbwK)v~GWIVwpo!Y^INl$|Bis$fBglHLMUV5&#P!dAfX7U8QmSED!6K*+u>j(_>=w)( z1kH&_paZgCkp~F|C&Fz$Q(yXGz+~l`QzZ31B+N%K0Esb>h1kp8XP?&Mq8!fCH~5Lq zzFd!sQ+=)05UjIsMZ^-aIc#4}Hwn%N0ZD>&Qdl(DLGpl&uzvelc5Ax8hpcJyug15}X#??rbr=w4Qlr<)I*^`CYwIaJk5KmM#3 zppvWN8kTrz&K?)1aQ}7mxB?vNGe()0BI{ykOp&s0$b#|+q*gaz()?|_mry1fvm~#s zadrQ7kCl_g5qklnC#o0)jx>!QFIcDF)>yKn2xIsT0sVfC2S(~(bO(y`KZQ&GV{0Nv zC=!bqMrbA!#gzQ^p*Bzhwdk|~%HH;Y&+DLr9`5IG{0*4UNf^4&QVdwqmHgS33@;dh&aR z<6X<<;9@n8niOf*YdwBl0a$sgZ5;1K5th0|r`4CfGNA0?sGk)@F{7MrF$FU?6)eb2 zu>HXRi&s$?{p#d=U)+ms4i2bfaJKo4$UmKG{Q`e_`96Pp57n3)Ik(}i8tm?t_22zW zSs=eo%n`DWKMb8w(W6!5f=CjXl-9-RsJxn=5`oQcACtlACqC0tK!cL(Lc>zpOrA@y z@9+R(+f}A~%1x=~?9koOSG4#G>7jKf@sE~1LHiXrv<{?H<16^P6T@67g?4{3QA$ zvln?$)N1k16t&46y(ckFnF$eq1;Thy-bR^<#eo4rK6p86G4*hc{X4WF^>fc-kKO)v zQm`HC0{Kmxr&l5_a5AprSUvS-k0=xKy+~H&E3m+J`Ma7miQYlow&Ihxy!VHjSJu6A z-+Bi|f8gCvAAsGl3%9NW)~y^KgoV!`J70&YM4HuVydw!Bx`CI9K*zv~H+lI!Hq@!5 z=#@kX)ZtbxWTUi?kzEu>7F*@0bhE-Ce@Dk=jzzM{7gKJ-l+pM1-}X zW*YOQKm5maSG{?wVY1Kn$i3d-O4ggpn5;>Y+xSy>770FOgxCv7$biT2jgj^WFjxF>tkbR+QjK zG!4;;XMrGG?I6^gd7X;c>n}*K53{k`Taa>yLG5G0gbjuJ;YsKAVad~@8if`3pQR*V z_lIWE7>1lsJfrRtuzb#OwE1&r?FLeXG%b1v;qq0%wkZ4fX+1PzJ^Y=9$>S0pF$JGK znyWO5I{e}bk-%BmAR5J+%-k}MJ2iEY#&pEng_VflD8pd!hW2liNKw;8N!+ad1(zd8 z#0`W=S6y8y?{`zW?3)$M<=x$eZQH?mgTd*DuSd1_1n3o8RKq!Gz6zjaPr8^d1@4hk16V0zM-j?^k}fl? znaXt=N8B7sYVQ&Dh-=ql`@9^+-0ir6fw51^4bAjgf%Yo5dDBYEOvF^>V-&9gd3cqu zFjH;O9d`0|_iwjKxK`;HhshDDuBPlUufBhKgC2d=%foy<8k0$wPQhm`GU#QlDM+&G zk74M1OzYR6hGkKZ-c^9`W6d==hs$RnAI3$c>83|U zbWT!9eimbRCb@Bs(rbu0-PZC`FW!9>1Zd7>s`q_YPDKgeDFlc*m}&TYetK ztUn;7OXZlj?y9qj>4P?kMk@#6lg8)Do7LLqqX!^iHxHN4JR6#^lz z#!ZEiO@j(>gUQreb=J8a0{6`s~=HF)F>KD<&n@Sr&4y~C4^8O$9iP=eh`sAMl!z~$}FU2YVDB|~q9?ei;X z`W>3AcOb2tRLtZyaWnUeh+hEDTpN@V{iMc;KP%i|b>!ZjJ(WJ&6UT{wM{Ff>_b(qPcgU7%jaaZ?o|^I0;i#ReW*( zq4&3IdUp>lg&bZWkbP;vy6DR??%cOf2PZ4#j|AaxE9;Ggjw_UsOO&tbPFNpxtq;=Mvz^|3%S zx8xH-e3(HVZFbHL^2pRAitQZ7mywtxAInzcO! z#rgg%MV;V@oW^I$sz4!5Z$W&jrWN0ly%g zCy8I|-XWMu6|L-xx{8_6n#t-|M&)y^i1g|Ovg7Q=g+n#0Y}?4>?5Xmv6p6KsdM?c>84ckQGTYm>D7!>0V*h|M=!-#y;7K73`~z^i(eye ze59c$v56@(U{EqU+1;*1+PNDj%R=*J=bQ9?8OZawKg7r`N2 zbEH!>JkO~Og`(_l*gZ)u-GALUb3wf7^HLLmbmavlJ8`YY>(nQTXcg1)KJA^?lP4gR zyI)ud(!x(?bDmU(u08>I4EwhiEH}tGT)AL5BvB5MQ zcNm3IC2!Fcl}lz&kP@Goo>5THHJVYlsK)Oip^nyb>qQjx4i(Ob-aZ6GfQ$ZZ1Lu3) z&nCYSvd1?43)tOw@UCc7uoe^+Q1dENoq$yS=x<^_Yt!yYQvT_iWO5Vn#5E=H#vIkK z@)V2iRW#e8ar-znHL%Xtze}prs{n;JtpJUBJ|ZFO?iDi+Sk?a)SVeE3w@1oV+avF@ zd_4G6yYs~OlyJwW4U0Wt1Z52N@veT0eo#Uwa@#nzI>5tv_-67}yYjC^{3eICRlM0ElMlZJlaTPV10APPkIT2S; z35uRw_>CLY2ZvY?Fl)`OvLQxD=Y1TmjT z-|>KpBScWLEdAKTGM5_BX0(oH^zp90@_PF1qbOBH{qb}a2!K{H0@x`qjKDDdJHQoW z``w9OVc_Hhc++^;_=@v$ud{UZsO_&ddX;!oen&4`95Xe;G_#T z(?76+q#~qy2Oo%a1T!u^3GLA@yD4c+?ifl{8y>16{nqR=-c>~9;4wA=AEW$De`mc- z5E*&UhYD`R+hu4elW(VBWilhUU=DnNbNi4Jk0hy~FB)IWS|J!V7xJuVlp>d0XjyPp zwTdyfB8$xl?`h#$`X1~A6}$RQjmd)UvXCV^EPyh(rXF9KMrR+v7~uUlzK0`_!#DeN zk2vf_CN)BwliBR;EyWJV>rL?7qlcH*Du^r&nax*jvE%p1lX_huH3)~X3f`g%B|TnK zCi7J9lQxZ^obesCsVRx<8$KNG3A4ZQ;|o4YK3&G&W*{?{z*p99>o%670tUBGRY9|c zCh`=4=g@Bhw?gt4KEU#a;TTZFdLc(~=hzgk{XGRs=>+(2$Y0U;V%q&2O_uGUal!M3 z*W&@BOSkd|`IdRZq5AIe$g>B&WOT(A; z#SMGHlv5wB65gS%rXKRNCy*1ob;oTGzEEb+Tz|Fc3|BAp8@oG&%LlZk0+kFUjHxZ4 zZv#Dqh7bYFuZ-Is5;$T-mto`wf%5yCd`QM`ZFNPM5yWabWx7F&x2$YbgO(oe$552r zfGaC;D$sEExR%?rX4bsJDUx|p;T$AjcGxv`eKHGFh=+o=PO)`F5Wwd`G>me^ivYz| zyAW`B=Z~2A&eIEdFa^fu1_DliA4xlZ!MNjf`;%a(#fGq&sq7GnA1Md@U*cfnM-kT? z(5#3{$FoBOinF8J-@Nj#=6t3$^p|<@VA)gQnUQEXoyH{;x&eJRb;W&f_Eq9?oP9WJ zZk=^`^F1|`CYJj6GiB_e+M!|qNZ#ZDrUXwCc_@u2d~&J5x=`DIp6r}$}T10#KO127s|K{0{TefQ5m26c99zh zj4UzD=BFk~sVRb(#oc@#oEpgq$;so-RLyoT3n@M{Vbir_zSV$o3)8Aa>Y4pf9t>Uf zge@-E4Z4(EmF|Ni5-7}5$<>w=3?Zxy&6A*)a3-*fbD~s@yR{YGx6g}BIp~YXI<#ZU47EOlTvX*byyCmu+7G)Mry2`x)Shj^- zL>M8IxLjO~dKT~c*-04Q@+&J3@5^0glWO!7JY*4NK~6i|jBz3L{FY0|Ykk!I`h;2h zFII>M%^q(@d2>E=cxX=KwyN^6VN&%wetts?@iyV<(+PMrDIuRvXON_B_z8gpm{vY92wq!xN$o`;RBwt`{z{rUpM9Ye(X z$71Xc_&n5A6(D_|W*lfXQxdek865R-OY9%=Yd-2QZUz{iKg>+7Dr0h;D(4`$UZ%7B z*-X!Gh%}cwj4<+bc<0UZLrCo52irpE@LC@c1rA?Q8Q5{a7w-QE*hUVF0!zCBrgMIa zaC6RqKkvu9&_ANL9SBGjA;IbCtKFXFSqkHFTdHl`x&f)o-*T}{?n>9P|E#;KgZr!W z^e?v$sb6Ae3uc7DmH(F-a^h1^@Wzdz65h%}Q-?4>H3F7%enJ=C^#zFI3M zBnDi_QpYfr;Mh>qF9Bx}N)>@e(9{}4d9`ik*RY4W?5vMt_fOP9J$Cc`01J>wU>n8n z3);uzK|FeRDj;X+kgtjlY)o+*W%d$Dvo8ACJCGv1nDzgXnag$sEOIOXN=Z3<-bLSl zY|fj{Aj>6P0A7d7I2+a4cjIPhlqWt?0sdES#ej*MXpGB2=I-H2m$o~|$O&V(aee{Z zPcDz>7-ep2yb1jdyKfjWM74Hc9QbUWt*&lxIiwcwn_n6suRFyRWDw{-%_{U>_+~?O zZapAIcS%V;&6KT)ToHfKdf2tE$~h3%m?+!eD_ti!FJ5d|OdFn&9&6)Ja%~lan+tyv ztgX-kw=YRjPzm=t(|`xX9DwvNJ$VBS;C7h_wP;{aA*Oj29it|MH|W7Zc@ncR0@D$y zQ`*a8sp!>NlKUw`P!l`sFD)=6fY8c1Qd_;MXXCgCAWL>Q>BL*Z7|9XkKe$$i5%8!6 zkulU!mks+_VRmRU>>=q^J-a1mTaKoY>8+-58}dvU>e?4U;Unz$PecU+ z;+(6%lj~g%WJ1C+zj59|k$)PXbb+x~5Q;szOp8hE5%1E8Iq~WC{{=*7Y{?H_!N}MC z$Yv1{&=s(TKiiE>D*Q1keAwz!3vJgd2yX#z2!|8Epg86(t%6Axdk{GF;iP|-Y*)O% zS5$=xM=lZxC&z$GKhA$I^sXQ~$0x~^sTzJ@VEbmMo*QD}O9e7=P1-YN+slmgTd%ic z)-_*)jGV8sT{dqN1(QT5F^_CR3dQQ2ygTyh59ZPd{|Z9g;g!zT6qy9-X!TxlQuEk+ z2=9m}WK$!qiJiI8e%9Mr3*`@A^?z6}KA?X~mPMHylpID?xhp-r4Bp ziBK74jH8mPCVoe8OLHL>3SmN-DS^9NqAr${>)=&0wsim+6i@m9iM;{o!eVG291NWC z!p@#~=SS!#SzD&oTw<#TN}Hfvvl1g#q*4{}7j2Fl%$F|}2h2xEOpp&!G2jM=aZZ)W z6t6K(g&s2R^9jU<4^tM?9$dVZu0?nDSl)=UfM5b#bHZ0G2GypuJh%ju7m6an2RTXu zU%I~-CXfrdwk#BiB`aRb+q;-x^-EM;V7~xrsxy7PugXdT|70G2iqRcl&?(kS$c}+Z zUq8JvdEF$3&Tb5_RshfK^)uAMH8vsvDovd?RyAICkr4yqVIQs%-*aJ>IRZ>#)7N zU&90f7i6jelE|GKWMc7c{;UA<3u6k_*EerTxX~GY?rAB1w2U;N6NKw0Wgp9o#;6@$ zJFBBN`H;XC$E2zxa(f-GWiTgI+1B3g#Y=5vyfI2-b@%?tPg?6BV8eqZ?H0M?MZKzvfHNjpY)|4ZYkg3csIr9TNk4q#^;=2=tra z#i!U`623$iMMIKGN^7SU$)&U`adeFPupYlC5 zRdRL>oq4rATYCq|{j!W|sqp2dH6iChKxrx#;pvv;+9$<7@QyUp|Hm7+hIy}DSq#lb znGkyf8AXH0wRNdo1nG;2@5d_ZLaSHn!$JB?%7$NZ+MfD>sEArS-s4y0KL=L(^cqIM zQ#}HGn+HefV>5Q(a4}8!PtFuagx980%%CS-e{I9bW{#hetC8>@84 zgW={9i?6Ch%vvF)R0stU?!xodEEw7Uxy01BmI=xBdwZ1<_z@c*izMAPfDyDNpFDU- z`5~BcQNr7k>s%DRNyGNZCl(t3{H&V@2L}`x+6Q0tt}B6VlF|`77C1MB{sLCGM6-i7 zP~!Cx3VaZf)|5qA$YIIr(gon`xK)}hFz9+1TKk9vdtmwOzAf+bBdo@G>Szw|UcIn) z6W{JRg5?(w|7x9ApMX@oqCI}y{SGEV<2{=0^v>+)P06>YXamhPID<~NHs`Zcq>g46 zc(cjPm)2f_!PzdVOXy|b&f@Hp-w{)LTmmSX?r&3l(-`b=?xT3 zx0AHkeB9!el<`(0wvvkSQQgvS`%s=Qi8&DzZcmOkn~9Sz*FzJGR20kEsbOM1-d$Q6 z-)_dKUcKg1?jU7AUVl4RS!LOttRaTWYmO;HIA!+dn-}^4Wfk38ECN6^nrGSXP_?BQ z9pvuggWE_|G;q6N7sE?@-m?ynm#pXUmNu-OJ@smDuXNn9AfMe^jK@+23E`EW{O)4$J0G!`(!$ek zd%JVHYMjKFr}0fn5MoLZhWGhs;=iFKSM$FEB%M>^%#Da(1$Kg@ zy4yIN9!g#VaclZ~Bo10ES(Xzpy;8JS!zDA)UbIg=&X+9$@ZEVtTwFlF`uI$5xrg=) zGG|2C+k6k|!O41+`bq!e%h9p3xd^_nD)f(bVIw;g;R%C>P*XL_zQ{h9zLrg;kn$tL zp=Tm1m_~S323sTXIx!}YZMD7sCmG}n*eWYK2t7dnJV@am5O6=oxd`sjB^KbyB;*JQ z00^9F^69CIB?(c2HZ=PKnN|aJOm7PL6=8Yw3F;DqkPUNK$O>-DXK3OCp7Gt(bVXR* zyAQU6c5MMXS+Q(`p8b1xiAlOwYwz5KFGBC%%oF))r}75AIQ5Am^M-}WT|O3Y)%z2# zCYE>hZVczAVH!`tQ0*#*M+-KT-!d)6$jg}MzoSW0avBXzP`_IjSA;_6;b| z)c4H2zF>qF9iF&tL-6XupjJ1KH!$Gb5UR%OR7}BesSK~tag50OK#tAR7slSRo_@pd zT`kU5cLYhEq~lW(M8U`8PF^&X_lU)rgEH|^9=)=)UA>e(pE5XT@n|MxQNYyNl!UdPMdr@*!uIKx z;@?}AfVU7fYqwuOipHn#bDr@h2+A-Tsf3g_T!KC}o4A9<0mh$A9$EdLflP;(t`}?R zykQ)p{l}l>c#49)gE<%J0HJ;}q{$wrG-fwlPg6mAnud6;;y~zk(xP zrdsPv5jXSHWFeZeQ|s&H{u;_o3-|qTCCX0og|&|+u~uC79{mHv2j>T@<%j&ksbyge zbok@lE(Ll8tQEvs2_9aUj(%3SXkAY)<@?8j7kB9zPOKl-e5S2sywzjaCVj{*Z=Kr| zdU82?@|IIxr3Y8sFMw{hFPMN?yH2RTgL2#4X9artBk!CUkZ#;~^EmPOyNtPjeA9Ow zx-u#!Hl|rXn8*0d-U&Y}?1opDQ;E6a)?exWI}q=54ad2Mls4tn>cnH3#QT1|&!pma zrj)W@5w#OG%~M`)&?2VXwm!@4d_&2Y1}sEC-iF>3qYgX^ChfQ}UZ8m2|MtWiPO_or z7-ESh&@J5lK4bc`4$H#*`^1%c-92ieZ1zMH#?>!OfAmRgkT0L=ci{TzdYGkbEudFF z$h4MG#}w^^6X4+KbMs4ENor&(jYTcg6s{XfYP~W>;U%P;QxA>S;&T*(FMngolGoxXO}SUxrCuN&sZLM?4oMHl)e<%zEl;V zQXAK;?8WNdlZYNWD_p(%+@tnWwp%cI`AJU^=p^7;Wr}tM+XDHTzz2&;O zPS7GY4eXAPd6rJ;FfeEgKjGTnW=-XPJ>^<`RAv)&w(YK67F=at0b_w?sq z5GJvS>1Yc)e%z-PzkrH`Cfm-59Z9{;r0RQNhOPn-&d&?;4-t=Y2h}9&@JDxCxN9a% zm$q`zGC>HH9eRE?aZa{6!wNY|2j;yv*iwGA?yXTfsE7}@bHy*1k~hS3bpfC=BfOy9K&Pd+mCyE&j{+Jl)$5|+sf`gv)>wCWB-7f zohzxvPwMDPHCjFJVEddR6kd7CJ3~;I{B};YpXdDke93p*djiJG-NxmGzT8bb^bW~H zWXBv5Zv9&t#$GM#zW|DU> zqH$;UCEQeAY8MG@!mc5kd!HLjm*i@h)Lajq{{niRmO%=h#e9p(UOLT5{oL4MG;gG~ znT_fVuoF#Z%V@b1X?zL(>cP2O)%gLGsJ>9}vA=R)y8qL~f z+3;+Tp!ukxku0MlpirO(l4b*cx$iek)Kr!NB#A4YOX5EBeWv9Q1)ld5vK?*Lfaz-w zy(-&USV8_Y{Y;RY*aAB>-O^aLzj@z9(G2VRqQR)kGAczmKwLSQT9MwE7^2d%dlXH? z>-FB3tuI+W`fH1Le6n7vR~V$A$2i((c95gk_@@Hg?xVDoc}3?^zBdtdASK-Ixy0|%64zdu{M0j0x?SISWqGFg zMP)pp{kvw9!_hMcIt^NtM3;FYPh~JfP0b0$deCTcuqafQs%--_}0(_8$B;j|2w-Kmi=^ zT?YU2a{XUF3H}Z_t;O)S@qYnr(ePS;`V*SI773uyPjpEB7jga$qJq9;1Mo2Yx6QqWm-Rb}BLIA)JpyO{v0`GZoVGu$8wq9fk2NOa92yi*@w-W*c)Br^pwDxh} z<@}%fmpc)^-T&j`>7X?FtArbcIKVRKj{yJwNg+TR5*N%L z4nspgZ*a`~Yv15A`v~TEp}^;}7!Y2xGC0QmR!BGvXhdGbh5qgK2f|n#nSM7QKmzpq zM+5)>#qxhC{foGu`qz9RI?@0@c;QEYD7pZDD;#i(0035*_jg<*5K0YZ4n+cB4G@2a z`45QupWTS@2t)>90IxpopCA|cA}@?^0CoZXS-}9Cf3`BC|AhP#7wlKCf9)8c%bzGP zKtL2|+^Q{`Q=~xc~J$@B2L0 zcU|vc*mKT4JJ(wKx7J>J&pAiO>1)S9ou{F^=wGo%H<8dcH2&)_O(6JLBj}hc?5MxT zDE<_sG&ayX zAjpKUG!Tsj|A4gCpv^dR*b68I6X3p&lm)~9sY5^j*k_1r040F9p9dl4HV7#Gc?1eJ zl7PaoyL(8HFgQ{Uz|qfq(03pRasHKD5sRh4U?E3o;fA1I0NQ`gi~f}tei22Hhz}~l zrWd9_!*DPqL)V6w*5aVSCFTHa|9KpP)g8$*X#LOQ6a^A=_2>!?+6-X!=WSUi8Z;Mh z3K#GQ>{?=lqU8LcFH9f{sEl3yLcw56sDSfznAl$j8k~UwsNpae94hu_*-#4P&%D3V z|9OH27a;F{kY@l;{)E?{Jb0MYe?Ktw0`jB(9{cAN+yT16<~^G8VqyrPHe;Cl?~kLr z2s9=MsOtBDLqYh@xPj41zsmcS7mx)bAV0HHz;GaZ1{}Z#y74GI{QONM`d9IAm>h!X zdN7kLh#@@c9Vh}EpaY;@I1C6b;3D=w9G5^Io&Ng5U@AD2un|H~3jl8XbDMuQ3!4xd zk_n-~3FaRhvQS9bNWu<-@d9b}Lmx#!@&9E2`Uk4`vtIPCJm4Pk*Tug+0eSyfHUwu$ zP@wY^Sd#qfj35@!`6h;7e;#nQ)-=pD&^Q0_fZ7atjBOzjN{fL1$s;K0p99dYf9NkZ z4F&plTIBE3KhH4y1SFxxv>A=P@wXl#P=BVDC!vu2^BRobe~DKB!G0#gmIji60|Ea2 ztAi442y3BpY@uj;C*|0}Bu^sc_zu+wE=i>1Nl?Gm!=1!tOC*K5#g@CTN_8I@hl^9n zLS!&};q4w2MZ>QK{KpF66es}n;8k(;Fr+MyaTfX-G!+X3=KnDt<|ZC{AHkHSENtD6 z-2vpqh}GYS1)~1_fkH#~0NcO;p2xv+9O!sFLL3}iIA-jE13Zs|)yOfOU(PS4zHfSs z;ySVjCA)sj5PBR3NQId_^q& znKW1?#K&W=-I z(NM7-M#mBLu<8CMf8ws^;ssOn{ab}od&G%aLqr?&O<}llG-C1S7QudLiJcVUQiLRZP@BVR>pD)D5U=s&iE=j{LAiMW`B3k$nNv8w|X(enMd^Jiz3*%_xwQGo+W zAGO*D?=DVTUf8Zr>mY^uZ*aKZ(7X=?o}^xSEeag&x+{C$a%1k5kzw8kb?`#y z_N~t7S@pQK5YtIO(YxT=aCv=xUV@va zC2aUkZGW3U%Zj!|P^v{Irx~jtH=(9k*~*Oa*ltZwuI_CH9iGIcxe)VPqt&5YPP4WL z?~WH;doH4Dx5A#>UK8|kUV^WDn?kYag82<9!KzW;ff9tx%}1)`Zj@?sw$&96-Og+= zorF!x{DLU(x{zl&?V!jz|`pSivt5bAlvp&9&_2P`G+RgV1@7~XW?-yq- z$Ev3?+OHrZ9>j@^k10#0QmH?BCwS&&9ET!Eok`L8;zfJX)1#bLx?9?@MWURnkAHTi z_N+-x*s%4i?Y=qT@QWKa4h*-ij%gk zNCULX^UYa%SCt5PrdIGIvvb8;o}Mize?n0);q)Go&B;5blUM8uwF`rH2h`&}Cn&d; zD(RF#{Rw=(urPEk)_+nbJIds3Uy@7b7qJY>Ey)A=my_w91+QzQUnDgiY!@cI_{>(F zDe*>nN1$0$^zCuu){_tBR#d%ybY1g9>ugIe2$d6>Ck83#ySn~(_Eqt_c9_+@WfKI=5Kq~rme`F2Lr%~S(^DS*sC{0oF^(n znCBUoKDqkDT*>i8`y7<%ZzBc_8-9`p#`Gtiyx?VVh6(f!#CVK4Va)M^hM7TCf|Bnd~aXFFE=eqD)Cpq7|mvl(Oq4Ek@`>iI>2mb zpb=I+K1L{s9hV&L*wNSSkrg-FwJfY6&z4m{6k@7Q(fWd?0m^ft(^+?A1;O0#nM{%< zgb+mkQ75SOk8A9mlGep>GPixsoHKXs=CMnI_ZIJoQEd)boEg$fTn}TUc{NTbm#}_y z#CCAojHXp^_lg$=GG^BsOm7G)e!jfE233T3C%_H01N1;W6EvqU#PsIkJApIKzp8JT zbaOzioQO5meHASEVOe-byDMjf5}t+Twc;h+?4Nroa4wh6A3^Kiw=C0@zv0O+HZquR zEuH$<4(I!4{<+=w$0t_&`j_b%1&wRP)rb1xI^bhZ@>AZ&&91d!K@37ND3F4%NImO= z^2=Bax*!GBn}AWm*P~3>N~6wpqB@7OUjpVOy^v5g07Oodyzg?(r~sl34-xs>NAKs% z4VHQP634Q0&KwSEyecF8zBSnor<^(dIz1xzxwiLm#`nMwhhF(4oU`n*93t(X$du8I ztq%CX;D@o%aE^R<1iLcVz(@Og9(1@)QMDzR?;fehOnQ=TZ^Q8GwaXN$R+B$d3w;l8 zJR{6ym)G#!lhj8x;xVX>JIbQwJh{n7labY{^21x61HHS?K_l*Po=@4_JP+MWdYWFk zW_{K+Je?*Mpl6L!a4?n5(@T%ZI=_Zdv$N?EBnWT7IBwuF^19Z|x1ruy;+ZF$_tS zSZ&cY8reSUr&6Mr@5_aPA0H< z)ZF;zQ+9V>+f^@@FP;F5;1vJr4->PO>F0?!X{|JN;W?otKJ__A|D>n6<|>ZQ+vhl_ zu7 z)?Kyx2BQa)si`G?GO8kXk0i4QJgjc{h@M63)#e8FVYj|&EDLM$_bD4lDI3UKJI;q5 zm2XV6rr-DKF3^n;+-s#{CW`h>);;J=iTLg3BbT ztzgPxNCwj)c!udkYJB{Ic?tKb1TWFaF&HBrR5a*w)!ZRT+%yobQQ-rX*p}Sn&8D)$ z8N%Uw1W@01?j{DAVRYymQGk|0ww6=jWO7@n?D*n4M#A`W`-8)&wHuB7v?@g@4{%yK zjkq3l`}v;?yQ1L4ETylCfJ_WG#NY>)jyX@#TST3U9c)Suf9ZvurQvZ~Nk%5Du)O(- zF$Z{VI*5wv&V#DA81~VHFgw=FJ;v*&MR4IrocY2%f_W~lZ+392^Y@SjnF|3g+Ty39 zaB^xzJRqBQ>(abRXJP_WsyE- z^s*-CE%WwzG_GMehNy_Cs#!0NZ>%t(hjn;!gsksU95<1?EE9Sko@G3I;|_fwr8L{U zw}-|x%qn1E$LNEbansg!da%QU_Qq-;#$NFPM9WVcN&SLWHeAi?h(1M(yHJc#-kO&r z_I^-ST9GkF8zvT+9PSOf15?a<(*8L{>j0~8-KLF9SX}QZt)6{gt%_US^dhtBVL;8k zX#tD9QNfeMr5#|%guZC6N+RNL-*M@fwXWHcQ6ToGH!^=6dxwb2Kq0HocjSATDV6?s zg=x~t*GBdsH#>UH>S~8J;qlQR2k!_!x{RAcO_m{!$4aK2Q%ne;;V#+_`4d@0{! zb`oK#&E=xu;lwC z39MZZpw?Q}J-bE|!B>B*n2*&DM~jG*Hr^OlhnH+h>$*$OY{!;2TV&?#t6A6ITQ6-s zkcjY>j=6NbAgBNIXW`8s3e1YNsY;p8hX$6YT^N4+kCTQMW9lkJ)4;{U5h%JnX2T*s zdHH}LeUoM8gvUG$$z00)&Od{`!kPd9X+oU($HpKEqsxEy_#e+^=YZWrTlnE=0Vzs-cRUTY0z2S_t5tfAyp8N%?ARDZr zk$c+_t5Qg`wH@935j^iCK0cLk*!tTSZ)s+Nf`-oqjkkM1?K&U#&+R7^4mCn{x{AEu zJTRbz?#+(L@GT3gs-5Bt`Z9BynGc|+)I{U$Z~)poHp0;@Q@pk&p8*zu!J&U;*m@5< zx!BP}cj?q|e7omUAKeJNgi3Eg1ACGPV}9-)Ax&-%KH)w*^K-+|bv`uv08JTry8+D) zdK8^z?tl+~ere7enRF*+8fe;)l>v~|m>$d#xPQ(mROAH!B7$gBHx=oYx?WpCUMq6E z^r-+1f}u;3#c4kl_qJ}H)IaIdUW67;?I+l)?GI5kREUyD1(iYi0Bi3YozA|)a7X3> zrs{vL<7d^HX{83`}F4AnI6veY2RZ z{TmY9fOD_U54WA)vifnI+&9&yJ;ZY#bVh!1VX`>m1jT%k_tSQ(Ehp}rk+B_MNMAM@Y%_QuzL<+Op~^$dLv6ccmGBuVe$1N3t-=- z=T_~=B#FUhGv0JwfY#xfZ#Mvc;>4wm-xl*9nDvtc9PV1TF0^TN`ya=@a@YBOO5 zE#?L0KTPhUzu31_e;i9}Hp2J8$^o#`XCYk;^pz<8z&`zpo@Ge;s;c<_%V4dDCN zmIm;Aw=f9DKD`s*!B+qxL=rbU|A$SlC2$6q`p^X2$D2>)(^->@UwZfdc4n6JG-sPQ z2i-UL+SoI}0iu)2HeuZYqQ@be%;Eo?Q#{1H=K;#(eB;wkITjPK_-Z{hoFvJq9xC*b z(wm6_uHH*1)8)N>!?uy_toqoJRpkJ#f02x+b9;}|`C*l^KH@o-Ko!w7HH)_+0)LAY z?!D~qbWP1nPCTyv@)8G>nElm5O=X@eYy@T6(;nYqL($hO;nyEWB*^Kr?{j|2{sfHw z%j<_5(z&Ovj>mI4Y$se{H2Vwo1ka42nc%kVi7&UW*lb5opC;(YCyy7iC$sg-n3pF+ zP7xH)PFkA99QRLoxbncmY4Yv&g5ffh*(yhEs`cp1myeB~dlz&!Ql2(8%88b3?b&6xWN^kf?R=Kw-#tiF4$;tg%6mio%pgCEg0@27T+0 zr6jQKn(v-7lBP2sY$_ine4Y1}aX{-5j3&Qq-lkB?lb^l~Erv$n65HaebzFhdf;~6! zTp?))oDuvWEfH-~#95Nx>W$L>i=S(7L$q*tn8LH5 zG0*gC4qFr@u>NR%cHChFv_Gz#*(YMV(qTGc`go!MS(|uNV3@3b9SYwMFT&~$Jw^At zZ~|$(*1UKR$?i_Q#Az-C#9~@9HC4t!$ykWg{iBPPis z-ym=KXYV1amTD*9w+XgXC+mHl9iz&Yq#zVQ=Xzf;rLoSg>=Xa>EOn*qCz=D-I-7a0 zwL6WhTx7X`Nh9~GrB;Z=Or?II2xKyCH6`YYimVsBh_A!iF0puW+Ig5}wP@(XvK0Oo zJNXmT{oI*`YiA9OdUNb!()dJ1{GRX?RT!r%p5B(bRCP?J9`5SD7d~*wv7jxpV(0~1 zpNN}SxuV{e*R`uf!In4G6`9koL(RnW{Q*%Q)C#74?kuVdf9EAkLw=Dj;*=89r9T?V zfckn2OVy)R_1IU!#F*_iB}GkMoj>;&uG*dGj43%3B1vwtiNXPR4sk+@XM=zeUMFWq zYt`sSn{sM?92gNTg%n?l*PR!bA*9%9w;>ane~0YlC`JhTu_ zoh^xp{8j69JUV=m`-8GVvLf#ok2RwbtnQE0P*jI0lp8YWn-3$OyiPO9{-z^Z3b%8c z$P1$pxuW_kPkh>?tC0WrvW~sI!^NHed($a4K9a)t;I~0(rzz`XOSg*hMFd2O%F-0z{;}eDzBQm#vlE@Ypdv9Yf>*ul(j+IrGcT*|l+BEhJ#3vC zpS*&$XhO;E-w!%Y^Q7SX%IhhYXDc{#J(qGAVYe>unu{{7tvd17m+Q3BB@#D!1xc~_ ztd%-)qo)2V z!L0Le{A%IQ3F?;_aW}DfYqVT*vM{Gh)VclKn$RgI(VVwjt9V>rgrj;{xXE#J#W3>N zZ24F{(+TR_Z;yw41H4)v*a_9&7GQf4fx?bEJy*}iTo2RLprNQDc_SlEo$NhmD> z(h1>kR9)`TMS;>YjTVc_hxd3qRz((dW9n^he=4~mD8@5?QQa|{7~j*|y?ET-uE?ya zK4qLO?iR_Gmf(oOneQjzEX<$)J(VERj)+XoeaVOb5zqPW&nL6pG|aDq~aJz5$0P#)8;Ha5() zX$;_{hQgUmz?XpYRgNPorYToJ5p0KbL=+UlX$-Ag>>UjZSbafk0ys+Vq04*OdmEVgoW_nc2zP`vjorb`3q zjPfMI%6Qonbh4MEnnfsZscb?SY|{;qk2^(5KZ->2m5%z57~(Y3*_YK@NSF0wAAuca zSW}#GH6NMQjf!8EE|c8*;!V?ZCd2rP*V5j$VUOACausd6!e>}k14e8mw{(;TRvR?W zUZHwHM&sL4TfK4ht)rXg*+U*G5oE&^S)2}KL(&hALW-IxN()CYBHQFkq-#+oA`?AZXI}wu4fw88(9ZQ+3)-0u~PJD7uTpaHJh7 zBHy+^t`IbYKaYr0OQ*DebMGU9ZZ(Lvd>2$}^pU!;^fcYm0V=CQ)>0dKx^RKs3?ftz zOQBjSleYZ;z9(OPJjvG55xxV>kWEz>^cmIm#H-Mv_}^opYvOisw|J$TgmbkGvD<>4 zk6mMVA@kX)9!qJUV^J36n;}}TM4WEqQ*sA2f(kC&F)F_mqPlH_=|wtTLlYEf@p;j5 zAc66N9r&JJgj^RVuJn+CAK(%70Rag(U#pTb(-4;E1j50F7=Q5CV(x-b;m}UT2#G_=~FgB37-esnsu) zi>E%Uk&`&A!9uRN2TKw4T7Cb!4q*3KIn99i#cV z3b)?PDlZHs@HfZyi}TG$I*iGDWBv%o%Qr?h1V_d7w!tt6uyq9 zrE)m`WdD<&6)$spnOQZDd4o&AmvewYwX|3oW6Wo^g2%^In-fp}0*E*-Id=tO!NTH(fqM@4K_BHk3pzb2 ztoDb_>Q&yI>qz+h<-ki9jpnD5OJj_wtLwkt_)UwgSTdsJwc(lF80s-Ae}82x=0ITh zTLl61!B^s|DEqvxWND)H0h2~O10+2{*^D?1tj$OLGW{XcH^f6Cb^(Bf|A=6@Smoh< zW1RRR-e>CjtV-9w^I&+o!1UzuF(r-)t&Te6u8IAfM-!j72qVG^Yc9_nP8p2oy&WSS z03ctrg?4~XRG~xoYva5Y{z=4CXK!0R&s_4 zQE?qEyqsoCKSBG-^&(F{aVs=~uA9~~gXo_I;KjyLZmbQ9HU21U&>bD;a_Zu7Jddnt zi6)GOxQ^8ozwjHf34THPkXV(11z<|`ZGN5pK&1%Tn?&;QEG%h*~W8`Kt zr>jU_NOZmb6PU2ANRQFBJS!HnW|`7#+{B<5?#6J!Eq$b#*%25DK+!CIBgfri7z`Kn zuOl+&<;{f2XMVNjwY{I;NphtKDAM9X@9!PfW&le7jsal?9O ztI_T!&(2msmY#&A$ID^b9Se`Kpz~u=1{SX4k?qWkwPUk2l`kS9C)Ebqa#e$iS`GHn zpVh=_j?XRX>NDZRfu*%C^qCQLu53!0(`Z+`c~8(q|Nt zSa{R0$G!sa)+?ZH*J6L~OR&2kUNM+?Y{({@KVgGcMb1-B&`l^+(0IZZLW4>T>Q#hl!5Y z^zNC}=dW}Kc{$vIG4j$EZwV~Z|ET=BjU>RGt$DkfA~)r01e<_|+<6;f9YEJ?;RLAN zL8A*^1PcfSl;-bw?{b}>opa;t^|`LHLGX?i&!Y)kdHEsXv)dcw$TgXW;5$C^%rAiX zJqM^53|KX+MDq}~qQs(6O2^p&x7O)=f1_Y`Sa5V2Q#iSms34u-_|csCdX^KIBnCT~ zftPE1T?2-5B$&XaB}?b$$F!@dRHk&5v*K=9E_Vppj_T#XMTP}zm(6wrze5%)`ECWa z%s-SEJv+vl)WoWl-w?A`RFvFPd7$~IMQ4UQUo_jfROpmnA!D0u{OG+RU`#CIbX1I! zTOZm$CNF5_*0@V5A(uiJANW-ojqX2!)UOE#iOeZ{Pv@F|bFP|mo4wa2yQ;3i>kb); z%-*VK+XK7VHxv@q1G?hL)nV|CE5mzG?ThY23zzEE zi`Z#j=tt?qIqXMFa2kBTIX3(ikB#|t_s_(?QeyA^O7rXT&qDsg`LFc0Cqi?tI6I-H3DL45QebDjdE$-}ju--Pzydat|-5cb;zEdRPEN#JTiFXfg37j9PiD+ax9q@9bmeEkGu#|w6BP%Y# zbj|HL4w}e|HJCp|44H|({Cz?|AE$|_VAb?P$>YA&@OX1m{1>U_i!{*$D*e5ZlBoxY z)!HKg^3Cr@WE~Lx7Rb;yl1Sysq%_8!8S)@c$D+;N>q;?JvIvWCJM8>fjPaw$p zCXmy5E4ot!S1S?axW?O`CTAC@f_uD<+rJX6_*pi>k>G`FtN1&3d0GQwr=_E6$En-= z`JpRak@iB3Lb2eP@duaia6A>g5x_>ZgW6fF;`uck#Oro6Fv zQ&`U5c|3*nX)mcuuu}FrqWsgxzG`o5Uh#?{ierQc#>SfR?Q=8+p@R9`i!FsHjy)?2 zofV@SnyZp6Z>4ihmU{mt_C!QM>qSFJE_@n4$}f8wB<5#>Xvu*40cpcnsra4Qj(y@y z|DXp9^hA`SXvPVDSL+xn;RwTd!4HD0B1Bc2)Sja39d#;BPYLl0dvj;BN|Hki=$PEB z_E-OcncC*b-pPTT7b|Rr;qV#``<$MI>3%>c>ZS{zSp_ZO&1ku3iY$$U@|!U-Z`^AT z?C(ySf~;xu{i+Rs);8&)VXi61XjMFhyk*|<|$MTI#X=Dzd8tD|5| zKuL`yf19<54}}@=T_mSSVY&G5K2q(qA0?WN)X$1?jdEDa%If(!(*>v!yisTeh>==>&?dcKVZE4~3PL zXi<0)m(kT}y>_P8hOIZvX4{d@H8N(`-*zILNbk0(Pvb`r&EG_1J=|Sa`6};@SA0r< z^hWU2-ecS|MQ_-(HU;l*x>cSMH*y~4r( zETJ;XvEwttn}?HxmhDM>=CyFnJbt37M;h>`t4>le?^F(t;RxTP(1=0Vw%9a17NRxm z)L%RK$mm6$GP_iN6OLIUBl59@hIaIL7hu?dpypRn_)DW)*QaPA|4?_L*l~P+->>p_wxR_ zg*WpE$uF$oQF@GciOS7Vz=tmrEuEqug7tGImGz&MsER2~6->IKP|*cLH!yCuwIx=V zqZnwmMJnLk8R`wabdlY(`7E^MRBP-dyJj;~4a!sY_uLY9aq7t%96oszG+UfTw`_#qbcX^L$x zBKgA+GnY`?_%uPAhMs@Hz8X|sni2jBHhqEZ7OIl{%@(Iv0B+rd3%19UntEVJ22Sbz zdUtW$Q>WGPO$hpl0^cqOlQPZhkosh{?xHz@`j!1S>LViWRMS*&GGCKp+pxOgNOCWV z*P8g`0Mja`>avI7Xltcl(p`kblWwaN2h%Pfamo|?E z3CEW{y!=(04K-jJFmwM~5o!Dd)3exHd3_C6{iB1y$l7Iu-zG~%Ut zon+CZp0SO%^RJ&?C%IQ)-0qA68;G%-@3i>z*;HFxH2zth*4k2ocJrnAcTY zwZpnp7aO>fQ43v-Y=}KO>#-Sm#pr#ddyAb)^UWnEqVQ+QOE%2-pS;%U`j}__^_HM( z?mf@r;o~ox?LL@@-YNRyMjoPu<2?3{%in?WAMQS1D^7RnR;8Ma`!*Nw{LsAfdZk>F z^8$_DMS7Q&!THeH=q3I;_FQ+$V_#p2$cwQ^I$(hdJ}IK>`k2|!e!oXt=xU^9tEX7z z8EkI#dy*P&l3z}!efX4k=KP8Z^H_^Te4;~#_MX`GsfK1RGVS@dm(VKn%POX89V4KW zy!mChqxxBB1g9J(4Bl}?%4apbz?Ooo`JUltna3q35)8iHX)l)SPRQQa=9_h7VeJ&- ze6}X@3YjuV(rJC}PD2^5M2YIcif;3alzgIts@Ye~J+6Tu`$Q>8$JIKWw&9%92P`=4 z-3)?UXH`v?)$axfEp_u&!1IzHbVWB1x~ef1%brBrj1ACNp9mj+&}`RaB5GXpq)4l4 zIM00fS~>fK362Sw;T1;-PC-|mf0+m{;{V?p?_F=lCBSJ~K}dkib9c*x(V(VSCR&YI zl4OIjo7OJE+lUQ&dEKog(|&Z@P!I)+IgMlqTYqUi!2_7iZW+}B3vL+? zG^S$*RV4-5>rpaM0$Q8|>+^cRN6sC(fA8U^FVj*I)eF+J9+>%RZljbXp2+t`MBSlO zn^RAY18NO2!ggXVzme1)V%dGQ_Y{8chJfq#u~jv^i3h7{Tn;X7-CjOgy$|faDxBFD zUl^#Mb3c9Wo_17IxRXOmZPJFWNq`qLH<)f4YH1|Tt~c5oc4+h>Td5gjoN{wnDQ*r_ z_w^!3NFgJCwcDltwV^5TFIa%&2&Q+xmK~1V69ya;Pn8xFp0>Q59Z3SS4Uf|c3SAxX zs(t=FII9Y7Ih5QQCbe7^1|3{^Nv>vbzLO;>g(~E;ofT#ggxQCKoqCm|)2K&>$Pr5|yp{^0TNny+Z|Bg1_g=e* zv|kqQlZ|F~%1HG%*A?dLree0DPVWg^{7_4)`;Kz=&o9lHeveh_EH3tF%QV-T(%y3t zaT*A}@e|90PjVRlUoeELc;6+7 z)9jcEBc1hLwX7AZtfc`00LJ!dn_+GxO~)0Q`)bU%)w!Bq#<;L<5P`(S?bV=1naQ+i z_PH7f0$ng&9aqy78u#8~wo90mh7)hNv&Ddbv@iP-v-cbm@3U-Q{BTKvXTQ`gM{ocq zT}eG{oN>;XG1ChxMWC>};>_d|6Az#UQHu9jI6FVy%cOS77+}f|KJ_DA>Bg4fI8*o7 z`t@#*2KXEn6sTW5?!Mi`wD@Wh#I0VGdXtw@Y&jB7b5hlR;mb6ZJV#`^>#|lUu`^vp zRrmP3=|BnZI#xa)se)Ly1Nj*+O+W6O?U^v_b`9;{z}nSYs6y5fENI?Z3M0j=+CH!X zE2~-87cx*PZO5ie4(~z~A$P$6ew3qClRIIbb3Yt<82=7HZ<)urMcFBp)NkI_z&Hy* z(s=FLVUop>Ynp;B7PjMW;w5KXU9jQ^iB8_DSFtj}cOspyaWa;;aBfCAYmYN?`Ybkq z>|8uW`@FGIfNfbsI6zp4jsZq3C@?LitB9I^R$XFRtkmyWGJQNJ$L_Dp1h_=a4TVl% zpk8Aqg3fOgoa`BDfX1sDS@y^kzn~rFanC%_x|J{EMzsWP245iAtTY*_q;n^ZyX(=G zW3H{W8agrrmb0d3;%;IJ1Hcc!1&HrKV-d zQ*kTFo1NbR3j)#N!SNim((9bjU@WKg^@e+;T)|UlSpNn(5=D81<{k%kqcMGe_QHN2X4oFdDUe7E z-asSyr|7EPRye=e^g99fPoahq`=Q34yb#THyQG|2#jkK?4aneZGBpt8bd{CJSQ%&I zBd;^ae}nq(CI3bh^@P5w-u$4}Bk!g4O>UfD71ACJss)93A+{%b5Q@J&iB)Um5zdWd zEWcR|rO@=?h4B0r41)nOI}_*bzM$~CRBE|dp6~GOWaI7;u9*gW^eJbRPh(2qd{dqX%63XD@DTEP47U;Aqu~SZiC+^UM6Yp1uR_ZUga^8%1HpiUh+a z^KkeqaVryuDZ0|pUGmz`-5Xy?WfPj=+qXvJcezGo)2FCAk-TAq^#!63l-?CV_?M7@ zS5QWqk2pH;*)~wS3pAyH*0X6EHuWX@Nc2p|sQiZ_9_6l}3C3LbxjOL2?O)3a z80Gq}%EVUkTe1IHt>Zt!{(Gm4P15goARN?&G(!%7rq@Cv1b&d=U`@IvS+)I>*@lTH zTORYK6kCnNs|V!PI!$AmsPShz)D4*{y%)5?;QVya(M+gEmpP3u5VEbO&7X|13%Q?d zW{u2eLmQgewgPaXj2G zsFW_Sv*2O$MGDzO8@zC~QQE3%K>?+Ftu9$XWI1Dd>sNOl)Ej$xpF&zJlc zY)xif05Y#oe_$YQ9yBm4B4F7Q@feff%Phu51X~_3#@}Y8Z))12hXM&JQK?piI%Q}Y ztdE<~RIDC%x^_(7xE0R**h*zE4G}G~d>1O34G=gip_t#`7?mx zn0ikx>?eDjRjWNQ(ZTy!)oly%_(PRefZkzxH3HRB;giC*;C}G-QVazgFT~)RF*@{k zEB)NSEL(;E?;Un)M!EkdK2Tp0Zqs~wBaNl$&XMetbf*p3>)CBhV-_|}{3It0W16V< z3KX$whFF9WUAEM0;ng{^)iGQfy!W2r>z8n<9O9a|i7_RtywYGImrSZoSxj`~gx#Cq zNm-D#aoaEsi5d#cyMFw~x-i62Ej#r1b`_T1z=j8PV>1Fxq}W+lc#+`CyTRdILawVZ znO2CR9{P=zo2J+Fo4v+*!D{k~WQK_M*T!$Jc6J21vxa=F%Og-dsZo(wQy4RIcn`Kj zrm9UDO#yCWga;$e1A)~;(@^C=PL6mkwoexa^dqA{3z*8Rr2>!#+&_!qc%D!72`Di} z?i3;yBT_vbR&V_UBfHb~g)`!@$!^h)!D8TUA4yX3 zLU{6H^~e#K^9zXY>H1JI!s)rb$WdLq#I)WRa6y5>J#q2r=~wG@Z0rj@PirsfTnGuz ztE%LtU}J0gaE<~^*c_S{NGVJCp7`8fuq;afsA?lDjj)W^@vNx;6N9v5F)_>xRVKC| z5^R+3URT4+6QR1@%KO%TENe_W1+!Tu6E~BA6OlW~$)0Cx`R3dm*VLXGVz|C)HZhUt z+7Sd%(B<2lJLofF5z7Wb^nrU)z2D(&h#X*o+5qNfMpd|O6Z=k(@E}3u($x#X{zw@E z&T{VH0mG*!^*55LM}w1M5~#V~HT63hL! z&+ARu5m6GJJqLJith4Fzj>Ca`^++ezHP-8U>9((Ft%Qth7auXft(>G~rdhngWK#9s z+K;>O^mn`dm?ga0)Si{)E-y&!VSuaF0A-(#QP^V8-iV*pbr>}9-}F(d725_Xb{=aq zcMP<*d@0h|ND<#?BOnUq+K}Y^qa6?oy`uHx0b>M^*$uQte;I>j|01FSJL`qUUeU!% zU)~LI-6oDLxDO~==|dHF@5hdOM|=AC>+d6w2Y zS<^bVz7+Jj9>0vX_@biux>T05ohR|kb<)~*8zc3jteQfx2}0?9*39_6=@8HIXZ0QG z4MWJm2!(*}NM?&MW9tqEU$L^LvCD^5zzYr^=b`5hImNydL#n$ zYaG_vUqa|@rojL(+)RVyWqRCAgLa`Hjce^M*~3N@qkknJ0E!BWFF;WBmm%=F0x1S; ztiSX0iZj+yN}pQ@f;1Xdo~4hp+i5e66c_wz53Q1p@ZxRomIBFjNMS)R0B(}JEQB~f z@)F>d+oi6zRs4Sd*A7NNu+Egq)Pg#rhvFcI^5>R(ol;5 z)6i{_4UPwFVDvy-2jB&DKQ3(XFIaC$m9#CG*a`K~_JNLrz0<65H?1f!-4z1cmix(# zb~2S7V#z?Sp}&l67shc`ly%j7%&GK{Fm+lH~wz%V7n9|we558xXXO= z3I5kYtY~^4DJFUiXU1Hw(+`C<@xI@AV1*SBPY1hK7C!E$`vGkX(6#y3RZl5lIF|{w zdS?l|gycr3{qt`5#x0;E+{y_th*#D?e(ub+g1@0Ji)rPL9jya59*8m}9;yMZ!&qYo zJM1v2AuGm|$vkC2%eTVyoEdF-i-A*%^gV}E562w{dMvnoZ(_R>mOSPY)K#l7Wx(z>2y#h{t{#-#}g6k|9fBwO^T!Oa+UWy*_S+r82N z^MN%NCvszaZ_7d$5*h&h^}lx59}$oJ?<9oC{kt&_=rxYUIJhD3GQnfr!CR#LiqvOP z*G<4%FbmYRZ6C{#xqWOw1HfC;@skBqdNopc7@aoRsKzfI5hgP%+3nt#NTvhh!xt@9 z@%}m`6}?tcM-%QyOy3;&yakt;PYQhY9s1R&9&qcuiovJF!z1A zYIhdnD4UKNGRE3O*dIkeSjy7Q9twWyjS0>_5_rkRB9CwJQL15tVHtPrz@T<#V_@iH zZH>qGr9tPV4|hI%iFIBi`?4y{KO0&0q+!u8)b9c;YwNHh?#(hv;muiR%__dKIY_7E z7bZx&GDy)pca?|w9dMaLO7|}qY4NI}QFBUKgy}_qR59VMbNJI6f@u#S8iN=B) z73abU(j(Qb?yN_%PZ1^JT*k%cN}R&!T@ymRZgnEaWR6PqdhJ3fp*0^MiyjoP6vAv(c=r_UFphlh7)hE`Jx$|;!x^h$^}9;=ekb3X;PWRkP(1B8O?AD!PVxG@t~|}% z`e0$22lGU1jZa`_ktY|VS)@j8oD355?u$ygM z)R4)$d-rtK{W|Tmx02mkahhrvchBBsHAGLT-o0A{Cqx9l(L$T`mcism?ig8G;B;J! zz&DUtkH|YGKt|E#bG3=C7j}w9xR9KF!|8Qhb|J2=tpK@m-%f^O*3Icg_$`D0AFRTk zKGCu+JHYRbXnj%U6QWPtH@DVbB^rGLw!;o+7nt^ljgPX-pVJR#?minult9=KXx1Pw zJ7bK5!TeA&*EY1KpFPmI)*b0Yro_b}E0qw)agZ4~8r_U1dXYg`@b!RFp=Hul5kK}q zWR0Sf6EYyi$kEaF+r+X^8K!$$D1f1rYCg3z!&A`I+8ScKabhZPJV5SQjCF7e@-hYl zHV!Y5NxVTL&h7LZQ-`yv9Fu$Hcmhsv-a(JM8ii!S<-6N=_-KWWRg`Dc$d0_kYnVhe z>Fh>o18cZ`SBsbI740A49uLEM!=C3NA zTw@pv5ohSVu?~-{o65sMOC`X}VJasBPuv4f;(0^*aPmB3&X~>BGQ)2B$>32U;E^7C zmYr@`w}$o8KnqA+OCrppX4ok)#_4R}SDUXbN=*38ja6EzH!V^=*+shrUmA_(9MODc zoe)TwGc9)!@9i4gSW$ekj1p{jb}DH2W4|KUs(<7|VhUx|yCx zT35{^TlXv)r!;-Rf&a-Qor$R9j~cJ(A@Zk^%i@zuwEpG!l!pdNr0vE5crZJ=kwU!J$=Y53F zY$=*88$$l1HY~eNNmi?`!wpZAevzs5-xx1mZzo-HJYS!_r#bADie)2XT(MmJ%C%VTvTzKP%-eF zjUdiE3`TB{viP$OSq^r7X4(OXYe=Rl)o}CI^p`1y7uV;r9#T;9zH#x9;D%9O4m`(A z(kjb7%^IxB{(=1_{07x~Iukhg2r4;V(@m+mKx`#dC(DZuLyAd-)(OQt>q1`WC%Ua#|9+GkatprN+_ep?7zi4^ zGEOtcCrpe_5Cv8YF%H^F{itcv$J4Tpv8yFMx>ZuzNLW)}80&!E+fY9oO42v{bwemb zlW|w1XmIQ5by!R+p2)IZ1tKdYV8P^6|I7tkf}4_z(P)Z>gJuO{If?)(!~2Uiwyf8r z)IuZJ{;$chtieU)Jv9avYyZL}4cqSG@YPAyUzosiilT;-IW+L#>gpbbc z$U>PjaclZ$SURD$lL$E-D?Boziu)Lh@L0ZMv?cr)^?3Vxc{Z3U@eBK690CGHhN3<= z>{bldGzkKSPi)_k1izkvzXUN7`r~0}M`IFNN!H&r=7v@~tX@Oz(R;jTl^=5V+O9<- z`Bgp7$_uemys2H<;i;V?ovQhy;6b0$hzbREzb_zZ7%Q}wOKvE(_Vf~%mJj17k*IOyJ2#@}l@Q~=*_qgJgLJu4I<+0PF zLq?O3dOt9y9ktWIK<55=;C~vJwsqI-9s!;fH?TfeVdqJgK+#D)-b~@6{P?1(^NVR6Vutk=>8s51?$|xlpDqozIoSWeSMS(uR9DM zkNg~uZKWPi&qYmh6EBGdqUH?jQhf4hx;P*AoZOCJ5VGvcRQ&IM0UwY%Py$%Pm9Yk_ z+6H&-aKfw{RO=po(vf|4dpbZ8U+zVmI1L3Q2|oP9-82fq0!uy~d_!3h`$^!I@sTt^ zSN0j!rO(aTmh(Kxi7TUmd|CH_mj$v`!PEqr#Z7;M*2w>Yh5jI${XV=e?p8ip7Y#!r za0!tW{zM2`icc5auLx|Ol|Sx9MReBp%_`M}5CKKpo5Kr+NIvUpAsei4Bj#Z?8_qB~ z$4ig7KS>$K`An$QN^=fRopshif!zqejGbRv@LOAN1TF`}QscL^ed#CUuVZUMWdFbF zz62b~_W$>p#Tfe-lHCkr%Qm4xVg`wlB_)-LkR{4eqLg8*MGddDR6?`~(IP54A!{iu zcA^bwQKWM}&!gpifA9ZX|8rgEoa>x)O?u{;XS?tFdw)Nl@8@$5cGKooDvd_96_0M?B2u$XVX)EFxzZqx$8IIKNf(^Qx9uPu_iI zP>{MV&*6cHd62k(3m&99Z$h)IDL}k978&W`nw#Fv4L7PAo#tYb3p0*Rm#Jm2ZsRPu zww)1TGWhFk8r~EQ(&JRh)}>l7AYO$d{q+z;+oG>RgQ~fBX{`)Wd*b?nGHD9SjhlU^ zISEUQDN&o4siMaT?9{esO{mSPq^@&;L$^OT;}FnjH2C--wSjxt6ONXALe8N{2~MR8z09tQs1YvtK)UJDbG2xJ;}OcNHC9@BV0*T7$?BMX}xViL-`~_u% z%+F)1cUfS!h5JxqNaiM|tx1&;%P%gkz&3L*eGiax`AfNI?cbIRng?a*kWAun>!pFm zefpCvLF%qO(W`DeIVHQpfq(6-ky-0GKZxiqCa;^6#qFkX(zi~%zBrq z&ZqZESn*n$NQOaS5$yXf$#a$-QaV_BY}3wLa%y0VYHpbeF_@WocLJ0zgi9%?$}+M1Wa%`_oGeT?hG{*QHh&7|g(LcRLbkcGP?-Rd5hAzrWQC0ys!Bl+c+oeX zMfbJlgnPRWG~C;B_b&ZiQbVY9>)qS1i00QBvnSlSufzCt?S#8q+vj4t`rbzSMz3Zj zgXLN+ixUSW1%7BLO09tb%73(67T4S+jbPbY#G9z{?M9KOyA;54M}xly8EX{R-6X>a z?B25uPy7EUw;`kza%Ct;p1D58VvX)iS>}Z|g$4-+J~dPDSicUh4_I6Nf_le^SKp?A z*HG)MF81^xSfOAElrP2SMf;v1_kGR!-eQE`I~ffwOJM5bs@^^!gt&3A`T;plyDYD? zq=@V5;uzLsK4wR!m&*}JTvd*^B!mG9p_&-wozl*LMah>oIxQ#;E?W*IePX&?s%7Kd z9Hkump7X%Ox?aQM0=B72FTBs+Zw;gQ8&KQXvQJOdJM(L>5IL*V)v{r$yq-BVZ)ayXETJ9c&aQ)T%-@hK07(+=cguKsx?4CvO_x^h74v|Gz z{-ogTmfT3z0nYGz>O1pDnIaq8vB+Q(#+=Qt0a21yM?moP++<-J@BghA;)N{Ntxv?$ ziu9!Ys|c!XECVdXUD_-M^WX(H$)Df${V`g6+`5gUehQu};WODa!3ep5lBWofm@JaL z@8*2T9RNcFp1Xje9a4)sIF%<6_yH!~ni1j$P?-o7hw`N>b4L4KiX*(yFDePCjsT($ zKXyvsz5RhVpQ0l4(**f7sHANIm?U|)H)g~1Hu9l}~8%nH6C5mX3WZnvEVZ%&J%Gc2FycScZ}~QBb`w zDqUv_j|V?pLoaQzaC7jRW4~V#^k2sG`#c|jRaX3$0lNTmre`z7NVra`Ck9mQB zK{<%ySueK@EZqL7g!CBcHk(W5M^m#_5M$!sA}TUMw)lxBz& z@x~VlxjkC~1s;jow5DBoF}G>w`HEgKsp%tbALHn4=5?iG1-^?4vTu|++&tFZvGp1C z{QJDFcb{e4IwtR495AeGAVf+kDQ6UJ+O@sA^D19?Xj75qyXhsB<+oa{VBhy?|9`ou zs=VNWR0ig-YkQg$d(EmH)^ovJbV;sQ`fj=&CJn;2Z^fn8dtDY@l+3>yqKGxVd>~Cn zc*jHK4NeZ|!-MI_h2D-d8c$dF*W!KeK{1H?I=jb=b690nvL%`Mt0>gBL7>@}U( zACmq;v{*5$hl3zLFpEi}eaH!4zqe)z=`07~UjBz24ryMr&u?6yxEG2h2*sy;ncefd zc=G2Ct8V`stVp>RDLMRPtbgAM!c-@q8p2E3JGX~DH&}n*LvM}#&34_%@%VLe4fx8@ zSW35I{duTDXHP4ch-KQyOYK{uh`9jds@q2B6Ema3XnaBGaO5g?%x*o*FUNJ&m+N3Q zGTQ>#03x-JnObS^+2Jm%k~!}93}EA5RU?!DLFHIVakm4Pjeiu{{1=0^LFOyQSv*q< zukqAhaY8C~LisP2Geo?w>8dZjS+c;@Qv>|<*J`DS0C6f3$rhr}-)eHG+~8jdbVv#B zU)}`=(Z0M;-aslS8m=BC<$FPe3Vz6d)d#$N6798)Loldbzuoa#>wwWmP=Nn?RIu5i z`?gsPQ5FAEbp40M>|bPO$ReL8{nbLZf;qy6Kokx@CX%Zm^>kU)%?Z6fwRXU(_n>nX8#7;V zb_-Wqyqh$44UrP>?|o>#;~FA74qlb|Op$Y&QG;139rapWfwvG>}OV`I#02h#OF-&{=-O^NK2pO+z+ zF(d?R!emV$CV8E5O@4k_DLsmMk2Bh!wxwhc3VtzGaSnMvK1p zRb-zn=#_okGS&0^;6BA2h0|S&Q1K;=Y&nV>!+vSY_=ah#H6JlxqLtTOzv|gtYY~C8wA035s8O~j!Y{+nHNac zbY9>w>*$NKOL`z{AGMWJ12htqu{ZqiP3J6%j(nn3E%;VRXoHYF<0->ryMgrVXH%R2 zu4gidq(YUGPb;~}%fJj}WbcP8uyZ^mA5NJmCK#G&-L7FlVl7nDoNFqQ;JdsMN?%x} zI>ynG*HE2~$%ar|;+q3KY`L*mOl|PKEtjM;RroE+m+-4eU`Ybpqx0*N7e#)~9@G9Z ztnPkUPw)+E&!CB9pDlsm@A?A@=G~oGbh>dDW!ZlGQ;p0sktNA>J@K1WKIWKCWLM)g zgI@(41@O8m-06F$7B;Zw;$;2Sweu6bb{kCY>5rm>*h1LuMtEa*2nMfSG6jMzJm`R6 z66tk-kh?UhE)>;zGYIr6Kg1CspK|mp4DGz(QZXEEUl`2XN*QPX=w<>(Py}QR>VydW z8KmERKGUfvO4Uoe+Wi-4iPcH<{*FWzx3gHr^#+VIrqc52MzZHp+g%B6@f@>V)OFiQ zyrYo(A|dUv7@tTy_tx>Ymdm0+lm}4rvH}jkaqL>i)`$QnMM69{|dDuW@hU2 z_@%46R@0a-rtK#!{4Djq3@)pxN)iq%Ja%hz21}D$CQV}KWvos%A=0kCedIKnZ7NII zGUq07_`~vm$FAa$G2W{J@z2cgoc<^Claf+Nv|&9B4FMvLX!W{LXO^HJhUFn>qN1`h zewE;!^Bm2v)8{8a$Z@2rohyv;IB;C;Z^w^U}=?1Mh($O8ywN8_|1!WLqYt8LP6_$KTBi)78sWs1@C|JN0DCZ*`T;1PcGw@~;_8=#9LEg=7Mw}xJ&t4% zLvwIWu?4Y@h+e5$VBrsDmpuj#71RcYZjyi>2iBon6aLSoKTz%N@f&CSCr))G7qz82 zai^ugM1XtE5NxGsg6EJccLAK|eYIopVRgsg8?huJKMV7!ZgzIS0@9XM^w3U%B*SuS z-}N4QQu^TPYq~Y;M;b$$ej$pztjGdUY{%oq>7Lmjk7WWm=I{0AKiDB^h2JtXkl2Iz zw}tX*vMD$xDsY^Mvgy+Kj>=nV_7f)VcB{k!)m9yA^I=WEVt`f#CxUq^K}7ad-h(_f zATN?zRm4U#MX+G3?F|IvY$u*FN6^YG>P1-$s|EoY2jrWW@%CdtQv(EN`A35XoFTH= z;+|nE|3Q_F4z>g*xy-tmEJ?83UE2IW2Ou;fNAd@+q<0A>yc$wOf%o-Msf%h0LI^mm z+-MJSW69M(p#iVdXO;-W4?yDp1TMdFQ(cjz1c}#2_)GMYrX`R&Ur4HKtuu|#dxXUa zh%)1z9D-w!{BrGr!T}boA`lc6q5%Qctv00lI8Beg;5S@^Dth1{gT$DK{SJ-*vq@~) z;(|sbpa?+pbf~ru6?P$?K_#LKkPVTe?17acUtscd<#dBLO@SS2T{%RiZox@9y!Tj> z7YUKH=z2OFW08uFB)&zd`lW$(aSX*Y0e!Czv2+f@HuzH$21mNB{$zg_i-f(D?wtIR z6GcynoZDJm2_;P0dpNG8C@9Dq^eRn8#>YP0d4BwL5=|Hv_gUBwv%DA})6Vr`SYkN$ z9nr28jL6G1!ZvvxkcF!@SuQ3W$`+Q~wrozG^6K2qRz)}EQk=$ZPgVp?!Q)$|h~IIE z{$&mBc&do#B@-6kezxK@9w$%2v!N1Fm1Vj+Lvn1b5=0SEyp-HiL?B%idLt7=j$E#C zzN~kQi?yawbH3V>C5|TwZU8Pelx9IxW%nJvxXbk~`E>Ba?Tej2RTL78Mu&SlbLL^q*lqx5iC@c^sYyaK<(;nycrWG+& zBzRoClv_SORu_FBM0`b4u?cD&0}FiuA<{1=+28-0W4nUn3e1VIzWjSGEKoslny`91 z>8BuU{4KbDP3Sr9!7c#-6~&AupCr#Jtarvr(7^RCTp4X{Aj zSoL7%4=5$&;a^URng^Wj{XgdiPeX^fy;)OAz>bIxmjV*4wVzwI@mzdUE?wG?81?sXaU^hv`xCUPYx zdU^68;a=M7-kK9V0Df^rcsqY8OdkPO~Pyinfpm0d3RUOi4EiW?=_TKKgpUpitnsKgk7AEK5R>?dHPp|=nhTjh2gt)f9 z1*T4P`ZD@CKwvdI;#&a52e{?(8x{y!q+Jnq9Blj^{!+&{!mwfr%-Q2J@;D$nL!f@jug7lFc#$ULNm&zIiV+o6KYut1f6%B{bihRF9i(Zh6&pwhYeKhAH2Br>oA zLw4~`=8Cl4t1nf*CVlWF2lKhKc-JGmFvCFbeF-HC7Xb-;lgn{>#E5#DDR-Ir%*I4X zYJ5KN>D}ZVeXqyG2*n@%lo>N8Cd<2q`@i`n-&^SFp_DU5x0P*9AXbGT%^@d@=dC9i zx>$Ls>7$nAE|=vSwf&Dwo0zI<6x)hA?~^hfJEM`#mUyd35w(`yT<^_z*Ul3`iHbr7y=olOQd8 zvFvmzSW%p{Un{X)1Pd+i7CUrjHJ-YXzB~SUu$oHyNUJ%-pm?v_I#&r1=w>xGdMebY z4R2jiRN_FOrqCL-jQ1e_1p;ue{!gMU0}7Rh0|1f_7;K6@WakT_>+pM6reM2V$P zLz^L6j?}gHvR@U7IN|u~_n)m5fMTy3`|i9>lUJsHGrg|eFTo>V5x>DXng$6osn@D4 z6#^a)EVtu<))qjl>`bCWlz8t3D93iuJr1-~(1gF*W@tR;afyo3>lghpLC|AtS<zLcyP!Ph7XVaJQ8=?Fs20Pi>XFCvqbFzMq-v<HHHG0JC*rO^!G)&U0F)@)R z-+$pkv!DcK*wbnpX z@}iD}S+7*WlDaovEMql7n~UZbD?*arg@o$v$lt3qxVeV*jl+t4!?vg4`RtY|+)ud3 zO3W3|pIl^b8?JqH@wG!s7{(mOH)~boiEDiMUrh~G&M;Qz5sh7FX7zkn9vZ_%dHK57 z%~ee0-5UG!^-FJ^yjw+3J~{Knzz;PTB2s;X1wpAGWC}<|ZP~w6X|Rb|3|Y7UEiWSK z8q|)u7zU*%Z2(rIL}o4|&i{OrNJvLqi{9rnR_f{u!YpMs;=qgfNk9G35WjqjtCtiM ze?HMbQ>oo3xAiE4*KPM~po31*V8TW*h7*(We(pYb2~;B4`?NE*nZPG#aQrm(T7Ufs z`%7JVbb+4M%-)UK0vCgMw7;iM%4=rv^F)=|Gi>RUEcd-06V>@S3QJBW^JXa$iOzz0 zk`H6ajJt1JqG%RX!e2wOnBsgH#wDsw(U8jZ2X`heScJzsRtxA+7nYC|iMu>|6?{Q0 z&vgZ*4W?`LqCcgXv7q|PC8>Z3Uc8TY=$sbMNj?hi*p7i7bKU!`k|TKM-mNMW6*XJO zD-MIteFthZHnKER!xGE7Vty9Klffzhh&xfw;IzBqHIJ{g?3c*Y4LcNJs0zb0loLeo z+7PHO!}t$IoDY%D!0`q=xu4VNl}ThQfVgzx>H6Zjc|@4`b6)-@r8+hHs$o|7@NSK= z!H_|EcI$p`$77+VaLo>!>Q7CK?Bln%Z`@Wt*w}KnciqLN+@i@J(1*SI2Of4cq2?t( z>L4XNRDg}nfqx_^`9zeyaQq&mhKQ))Ux2Ae9C%Mcm0`WM1CpX6A3@5z-w?#_&C@jm z&H<~5+Jv6eRTK)!zy=^N+Y0Cy_krTZ0_1(f0n#w*Afo~wNeOe*FN8u)D6C)_CUw9F>r)?mX$lwv( zzWcm?50kdF?oPX73&ivEw52TrsVY{$h1)H1wxMDlr=`r9(r6W1<_uf#fM zxxD=WJ!4WgUfNcjZVB<(C12XaATiDt*PY6CHdWL9(4c83U8%kH(A}~Cl4Zgn_wOrq zV=$Md3#rMhe!9%%Q4*Gn!E$f(4X+k%Y}d+sbBA#WvaB7-j_fDme7dD+5Qe8wsLKye z6^=M>3PBtPLP&BurycJ%W18lq%fNDde}7KY)qFUYI@uHyt80}t{_f-)_nX1q%@L^K z2WeZ>EA}g?-vJ&As9N1k{zVF)Sbiaq??y5u(890;#EJ5^+!ra~{A`JFux9rN+%>rH z;8(aR`!@`aV#eDf>)6a`>Fue~MYBG=Oi1_)cJJx~A232KiFJ);5M5na_@Qbx_*&&~ zo?_mc9gujVdk6o6T@qtUl$Yl@=Z@md>~7*YB+HvIeH-y4R&m#^^z~4DYgDoeaUEWd zOXNrbDK14uaEc;tzEn=rNw*4%O_Z6H4sH>5eTFr$HqRfdBy=@K{eZZe#+>(4i|;xu}n`02O9xukzH*-vZN>7OEeX-ocQyweZpb2GQ^z-UuaVA1c zrjSh!gRkjMblndqJmd>%C_)p^Up^_gCb+4$39*VCM+VyvGNNXJ|KLLw@~8!@2GY(a z3ppwLG`*TX_dHOk$1kcN|j?0AR}Z}U3z4a6MHJ?w8~X^CIM{M^h=C7M%(EFXX8Y}qBM z?5cH1j?Z7?*onXB)hp!!gD{ZTqY;~0cZ|4D`ijg{2E~PJ-xMNx!-M9UAyN8iGF?9M zUQ?^g4~XykKTUeKt6EA=5`9kp=ebStvO5>b)S!(IP!vq6ar~8sZv-SUdts%PS&N{&P z0U}rE01A8nD1m3r&5;`l=jb1%KUIZ#bJeektF9jnt_;64W*g=A^jfteW#_Wz^C_9s zF4LQ(V}2%#`dS`KpVhK63dFO{rXd!hc0tagI`|i^iv!A^*riyyjQoH$qozD`p%rvF z5_rj)Jg`j4Z=Fi{;%y<+*%xNS$cwY9ND#iL)_t0k; zn=SQn`J6FJB|x1$%c#MueI$FVTLc=jKl7%z6FQ|VO;N&gDRF6Sm0;q{&7N4rHnJEy zD2ku2i&Nq_IbwCIX^=*w;<2ah=1}PyZGDR@$Tx~U9d2bNudpU)J;99iKy_BOEwWL8 zIHKvsA0j6q6uLL>FR~*=r#k%=Bh;)P&$5kWYJSm(2#+$`c-z3%5+~}>?r`)NLxFC= zTW>{87;c0broIA_d+F-jupuUPl!Q}c*__TlOWDbV7f3J89NyjDyXxj@;_#b5n;Niv zv4Pqd(uWUa$l_DR1ntkN@pn%OEzC%J0BZ>ZSp~Xau08n~U?$jT-!?YX-1eNilV#D7 z9VoZ@WE_*YJ<3mlX(nv9c`(-6j_MWbeR&K|(HM`4CQI;Ag3ZTFX-1fEJL0;&Mlt-; zNZQ(TS4FRzBD_0Hio|ROo|c?#7lb@l2yR$YpTcp3Vk748B$E|B88=Nfm=O=;-CFVr z-~}a$3V)=i%j>X zP@OJnY>CEZ$(!93bmigkq<`L+=EFM9?P3&`aG>M$^JIH9E$8JykUp{HGTAYL*wiCx z#(Hlh)R1H(*MyJf$FWE%o;ZsTtrvzGZj716Rp)n=V#o6|g2IduCi8EM@`94$7lDY- zgUlckJYS-Vrr&4-)^1V_hVMh1Yd_|Iz zzn>n=i%D?OfR;j-24Tg3<8KTJ`G&-+IlPqo7rVpX5pI8hHux7}D8B~rIA)zOt^y4{ zxscLimp>l^^y!~uQb4_wx^e$iION=KegP9upO9eJj|QQCFFOCCk_um*YVu@06B56Y zUwtQ3?e8y3F6QzakRz}l7F8ke@D6cCmX$nX@SL+~B5u&?)Wckes>-4JJtAX?c4r$} z@Oa8~#csMM3o>`PYp5tp!MpP%My%&{4#;M!R|Gi|!*5kQJmnnzo-5qWO2nAT4y(=XViBEQsmEg=qHr!WeNh+#*1swYG&g~yydDG3&y5xS1coqP z=7pFJFGP}uBHiUVIDCGnj`aY8;aq!ScG8v##o|5E1IP>&%o6?6@^A42?d2N-1ip|= zwjQY_GT;+A-k%0CVg} z#>aVH6^Jb?_O0FpB9*Ks>tr?&8>y+4*+CmjX@t`kL`7}Q?e%KDSyyAIu?1X_J<*Af zjEC4$l0sAmD{lkdNtsS2(gSn9v8fnpyq8j^Lpd)L+jyht#VHDAAZPr}v@X^XN;Jv+ zYzL_*t@X;KScqrb^p-hM$?za8EqlXBVz^|8btR64L!6fz*>2aYRVhS%DDfb#34~SN z8q|n4!4h4zd>fG>G5M%Q@$sYx9c{MdQb%H>+a-d<#2Ou!ns%2q=Wt<&GHteP=exZ> zF$qdAF3fJP{x;eDqn{1^&)!^smA^Y-$6$&BQdOOObu_GcWd!*8QXlIR5p}Lvln=~H zdsxJi1tlx?X-#pbo%;+_>RT&gH$Z_r0-iR_T9ru3k9z9DuY3uOmtBoff$#LWVN{ z%yRjI*Un_`}f z$Ee@&E}!Y*FZ()ht>$^xIiJP*BwfxK^P#d^spc@NFmww~{L7eE;G%m+yC2M1{G?bH zGL3&c{C_^*|JciZ<+Ji#3#{TFI4}Ej%u4Q`vbcYF`~S=)z#9GrW(%D7JPU##0R_+R z{Sbu7^_9B{L0T)Ohau*>c|GtB@<(>OOTf3_pQ$hirFMl}(GulX7MR^*KcNP~+Wt$V z{ok|cP8@A%V7HeGhd}BG(Z48mg?x3}g)b+SD=)DW4uM&~BV!|4i1=H~sdvmTqhgwY_BU(j9 zrY^gG%Dr!L?78L-D8{_!??)_}_(~XILs4(nSNSdDI$rnl;8&@V@79Q3iq68{(!RZ| z@q>b+j$pfgO{Mpzr4)yc^Rqy^4!1g=D)ccxakVR z&D1hq%>LpC%X7T@>sjvm1;Em?+hec}=-`WeVDYnv=ty}nSa1pxssQT}1f9xW=uvzZ(-#_-pF8^}S|JJ(= zZ(JHbtHvlior^ie9jqZnQ&%v?MUqHiCDCbH3Ix%&}1Aj@|L~3u(?yBO7 zHfP;4J=C!C+nW^5Hd%2;76TKVO2`pp5PilUK?9@R4@+sgJ-vg;confTE_y}BtO6&L zPxEI_#fMzgA>yuQY#gzQsi(2TvW|WB$p~*ci#f{+m@#UG%_nhR8vIB~LC81F*PC zdug2$x}rEmQ$zfDihlp+m5Mq`O$)_Uvv*ZFaox<;Qo(CP@Hh5H?sQR^5_i@1Bg=8i zM3+d&T8l6nV`=g6ZVcu#rDtLuX<4^$GggbRTPcyRN)8Qv*%gdWRPV^c$KQM)PLU4_ z?$zqe5f_u$V;(V=nV=HIUh)IFz*}8AY!qi9z>QbA?PcewbiwbTcx_MySHBaT{or9# z9noWJJ%i5KRU^oCeD#5ZM_Z)jrASwju8zt?ujgkjUv1W8PF!kfC`%cAuHc}T&K8rd z%rMorR*X|tmkKXnDGkxTJJ)oqzCe9T$%}kfc#L_y_BE?(j^c2(w$nA(W@()dr42Gt z4DzNLbbsmKzF{%9KNXjrD%KdjOFHS{(R`;h^^tX0rN`+dk;37(S?`-^E}WzM4xF=? zsh&4NhLt)@YRqCqb$plns0cw+Y{Tp^W;FpL&*G7Z17Df@^o$X0tL_lX=1Q=Le>h}y z2INaOO^a7QUSYwgG)r{^T;8s%66fP)x}_pI%QR@RxSXy84T12((wzQJPa30_pV!8) z@803fURf~yfpReVr#8I5Jc_EodyzGX1DP}I>9EG%~D*`hOw;8&z#UtlFvJ0+0S1`WALgn=C49dyE%`zX!)TI>rJ|lS7e0P%e4Mo)o~?cCMDI=*_2W1si{UU4D&OkKZtE~ z(cyu*g?EVsJe=_^ieQi{i&zjYj&UZd>|)7a4ZkqPeufoSOEec16RcujmDk!l6xo8i zXg}B+C=f{;VlNV*tj{bJpdr(oZy!OZrl|mYaE#uRIbJg*}dwLQ=Y4hP;R=BR(iq&L(Zgz;4EFU zsi#THjz3Tk+dbKj-HLU2vW8N_pz!hgo9-j-F={mn5UvmxUqwo8T<-W(fknLViOHPA zN8%=xHtpL0y}bQqX_a?S62@`5fy7^tbX3e=j;r>HEF_t{a`%deaKae5bnOGVb#Ly} ztLU~=f(3x$c72d!^K*tIH3Av@Ew0M*$tz4G8A^)Jue{d0{2|wo5*};Kb?6X49%8A% zaeLZl^=NnQ$1N^?d%m?L!X~Ej!lkYOJ3oH6fg;lsu5)ac8J~_|ovITryv!Nn)#eb} zKd1G!k zqu`$S`W<5NhF1@pVJ__x@g-7>9@55~xf1;#{egxLcdi2GSvY~9^QcnQKiZawUz8Hz zsNgAZ;H@6VyKVe-Z~GX=VzbOs9;-veJ0Az0?kE`>AZ0i*n_5{*E1@l09osFu#M0Vh zV`C%k><)FrdqG7H)G@l;2io>aMuf!Te?Wzf8y@plzWd^O>?0YXpBM14+UI2JZWC>;Jo#S5(!Y;^E-t;^5^2H{jyo72y&V;o_s> z;S%8$5aAX8S0fhuzejqw(@Be1I=D-Oy|lE3qO>%Xn}@57y^}Qpf?sZMvbe&R7qC7v z?Knyu2)0YLj$I-$Co2ncB#70Z{)t&F^49nYMU^xKpGf|5)9UJg#nk6M1AR1oN$eX0 zcW-wsuVmc&Onk$yer}72zULIa2MC3l=GjvOXnacolS7ejzgNdD)&7=6?_omDGU(5x zD_s@s+Q(VlF^RXm%)jpt#w|}%2lexM)B=22HkwAF{8nLPmciPIyZ8E4hGM;yMP zg^bby*7}$e_?RfGxA9%fXit`rf*nm=tAleT5J>*Act3){kTzt$nV$Rms@uBW=h>g0 zKOz?L2>)m!?SO#r)X!e$rPoVU6%k8UXAX0#h5}MyBsxBRD{gLUL4I~V8-8ncK3*Ok zc3}%%VRiv)L1Ak_0c#-(L7UWD7$qGqx48hHB|i_lr7$laJ0F*z5W9sSj{v)n6`!!B zkN~eRzYu@w3vxy}b8Aa$bADj~@M~dic0NICUUmy1ZUJ^4D;_}$b8BvXD}KJzIC3K< zVO}9YK5ju_EMEiEi~ZBmsfWwCkLxrEud z`2Oc+BHY3a1(Yu662~-g#vp!CEtKVD5Tp>a5zG^xySsxk7;Xv%o(KpWv=1AiU+Hrn za1zZ+QB@Xg8v_M_4)-22LNG#wsYQhev6T*@7yZDyV)ix5quqaJx&-6tAa1rTTN7qv z2`Vx=UB>kq;b1F?)ANPBsJ(5MycwfTu^;;(37MCb`q$zMs{+o8uawa#iA#O1U|y7%#B7R|cM?#22-pzVH-VJaPl zP0fy0?F=(v96Gg-+bV+ZqB|NdFYnDi&U>Cxtr(kzn6#84Sf9cO*4SfIPu{oG*nJy5P^xOThli^wkHBG{iNQpLV06m#r6IZR7E5oNb zb5|9S0a7dIc}kzb!w*#w%v-70XRo(&*k{kzS&-MHY@DjNZ6dGkNUya{JN5Wq&*z?{ zr$WL4DJr7bCav%&!soGXjIrigj8R_Yot~rrlgu$5^*3=PZ!~0|;FD$U>+MA{bi|?% z8_d6*%Rk!p?atv0{-ey7qbl()Ink|;K%a}BAChe{q=#FIjTO&1fqy#d?c3E1BNuZE zi*U2+CNtVtnh#SfTOKjcY3&^WXZHd1oNLq!n>^P8s!VUS2U#heb<& z5>lcb$E;4-6`)Nh4wFfEdQeBv;#$=Prr4zKU6d_*o=bKDDOUJ{~1z(gf5ghxA8iScmy{lh9^qrYW7O82(7(efY@Dw9)ct(Vn>;UPjHm6oD-pZ*!-ov0Y+?QMHi&iaxrgj4mxf&2hLB&LJihG6_wRL{+iK0OE2F+$$K*jPwgYf-6*V$U^#DnWmCo%f=u|dxq9$@#!hjbWa>-7xDzPBEFUqY(NrSk~n(D z{I}cL-!DTgytI<2 zzjY_<26|}yQ&4}EIVz?mb|mv{%Oj(X=xu(jCX22cY@q$z)OX1n9isNzO4ok~c`)@} zpi7o&eLfZor=n8T25)TT?yM#2-Mg)lRJHZV{5w9;=7Y^bd5SID=hq}%2dPY|xi+b? zGu}i095|eP4MJ~?hcX}D=FhmZGku3wGzn@er39RJCyGc}R5)}WHr82o?TXAXdT?HWf1?xn|NiZNsUSH+u$rqf1Ljvecz!-mM!H@ zWUs?~*7ST=Btuu69%&~5nJjec+@$!q^^Bk6w}=J%>qLk$3Eq6xy?#BV%usT>wXjel zyHaNnbzqdyG~rGU-=HDSQr8ho_WE{kf66U!-fekypt`@(IBj%wOC;q~^|WN`L&FhE za-S3~%}()gh}X^!SCBhfmVC&%kI1?|r|X->99##jJvIZw zA~r@lWw4$c6@_9v^PQDS9u6BA4eFHMw3aqe*|1gDcteP8m8B5o@j^7zN3n=_%_^;^ zSi8|}7sfzP-w>L;p0qxQUQu5{g@iayOF$n2wcjH=eTsru!^P4&dNJHT@dC5;w7J&m z@e9&*rhf0B#_86Yg*w$~pQkfc#0J%2%+V^0vY9KV9tqlb$F51{A|o<9-DW8a?FYUd zgYLgBojwkZN-Odj?FC{LdL69X(*N*XA#-_AlN=G!+3^^@ogc~mUI>oyh+>kLYdSl=w;qe7Av7f4QK2xL_s6YLR#;-a-Z!k3g3Go(_*rWf{6(^NRCgHA69SM3m4WGjUl2udP+V2wsJYRz>?d~MsG zQ3Y>;NGf_Vvm7{LX)*Iw#&@fOp`?TqA!Q31IweWg*tYZKKzG<{AzgGBQnx}ZDl?CH zzX&$=z76gRCASgQKhf#6w;?K*+ptN79ABkzmnQp`UU*#^nDeJn*(D{4O0!P3F3MrA z&Cm~nIQ1KyxJpKKy@WlS*2xt1qy3#W@bWK^Gfe}S^^M(8h|GexiA?<}%uYHq-3ojuk0@HQzViH<*KSIl7LcAsj zY(H<4Ik(`91D~6~d;L><<#Z+S=eu;Tz(*dk=fkneb}E-!Jk5n$6X5j=82XcJ4Z4@0 z1u|nOAx2ps{t@yIbHbihLU8edwKt)DSos_Kd5Z-BEn#LyGAWVOj5=`R->Gd*`m`s_ z0%PfK&1#4wP~HbW6#hF`ngr$50k2SS<)8Y4x$k#vTr2faUc;# z77{`<#bu#SMdc)kFKJDN?prR-H7ch(sd7owkI|L6!ncX)eRft>JLi&DH?vvXZ}{Dk&`GkmVyxe zT0dVaprdHNH*CMZDNZ!&jA|3RcDCD;vF_#pM|?q#-eu}{l*)vC!oW`kCQE8qKTyu&G7GXwN!je&4iChrx=cnv~nJ^ z;dj)M_j*I9#9pc6T|!(JWkFNfxE*ALCo(_{w=81n*#9jQiquFGE0sb=A4{V%Pu)M6 zW>NR1J{(Ql4XMPF^7vu?BEfKrE~W8(_kTsbH`{ECTUOD?qH+pTB1la|G_NlwO|vsj zUQhBWBSxy`InBo{D52^b!w;p2`@@;UM|!-wa{t7&>q2#xL%6jfq7v^=3`*iaZ~IG^ zqBw9m<8I7HT8eUg-aYcY`@}p&X0A0VZhNPpW4cwU%LTchjh{W>e*fNFJ;UrbhUuco zs|_SJ2kw!8zE$IV!WvKOt`yp$IA8*E$Px| zVeJ?8*qk*kr=xd>qSMUn+k@QbwB7{$9p@jdu`wblsUevK^ zWhMc^YB911qVL#XDLQ$D!$WtS)bv{fR1NhFJ-m{NcOfKJ1lm4`fF2lM7M;2(#ch+A6{Vq}!LFLO&&*-;kB{-)>!z71@;{3xLLl?W zk1X%)xjv4;%7E%|dy6Q!_+ z$!hhFZwrENVu2pKVNdrl}jZ=1UG>+$2CJ5R$4LesF!iP!uks`bdQ zTWel#v}Sqycm^g^Nre=1!uF+~WY6L*OtLOA2}={Uaf!IVz*sTmAY{1?Z8Nxk{rMB$ z9p-C+mQlqRe{iiV9xqd??52kB3vf71T(MnSEHNDzFX&;UF=Ygg-GXI?USIL4OI$qS z5W8n9`B5^#DCr|LDvXX;;il-OSn?tc*$T-H#*U6FTVR3I8On;^go^%NhBwT@o5-AO z|8|)+H~8}(oj3!zSMn9QP`}OS`s7vK0A67J=FRcgc4_7_1dXgg!Uz?iXT>*})BA>u zNI&w~?vIq7Bw0U;i?BGxNKT+6pVoQ&L&*qZs~$hJm{3dhVB5pL*P9yQ+9iniXvWge zQM?(|6Ul93yj;+(r5<6VX6IjXODDVX&{B-3UhT}~ka|c8kq!t=2q8);EXSoi24wmT zA*YqDcU0)JEv_pmHJk}}y?SJ&{P3MwjOAp8Re=r9hBj6*OzEn+o3DNxGEP6v1IRHN zpR#UfawCAxQspnnbjHEDG^*8%Xvab>cv@^};V?WB!>r0r`kuG#8%9r6C%FOAM{N8z z8?)0!9ZRS@rwTA=fKF+xoukhp*|0i#r(K}f738)Uv5_OdxCs~moJ?o0RQQrkwShPU z&`y!&gExyXz{K#W$}gMMVOz~XQ3Zz86Cj6vNm8;dDt-wg#C?&}fv^pR&iU-Mfx=62 z#J9Ly+)WW@y+}MUOs&j{j$yVGt3%xB3gg5dLUUlaOvo~ysh*T37wx~h7YoF&o2_Kk;Z z$oD%ac|#wI&k`^GC2{Yq7r$P)at_EcbD;dt!%7-;HWQ+S@u z+42etXCSR48Hz>hPtFEYPd0b6t^eXqvAd>vmoc2&eje z=>muy+La@pV%b?IZ6;KF<-WRauTLXM9)md8L!EUfYwDaab%t3i$=@W1jI}y-ZNys4 z9yt!-R%QI@`a~!qSw3Pn<Sdq7T>Vl?u;%?XHVELtHk~G#;Ebnv<-Apo9heN zl9~=3-G1G)U|9C*;pL>*q3YQG{B;3OAg2~LZVn3EuWjhQ*SqGWudJh>*vR*O`?x+S zT+$%&`>oBM<)LVYF(8-#SYK#%9!7spS2t&q;QS%&8xPK7^K0kSz3DWAj-`HY6R|NP z+)NHtUcou0Sf}X)y-L|a4IxYm?nH<|qnJ-Jz$L@k(D9R$o`gAW1nr(o&5{c)G6jMn zE)Ft<=1)0v5%&vxsh+Pgb9W3mBK8^v#+5yu7)Gkb%x^8JP6^nQ|r${*)IUW_;p ziBx>!))zXR&Uyz};5xPDW6bN+yOlsG+-V2=;FzD>k<=*0H1Gc1oOg{LKZ91rP!6t8 zmfg;*N90hl)v`*2q8J`O-;P0kKM57_!4ar&u_=(4gsDYbHXK#C-z3|sR3Twingks! zs2_J;Q*5m)9{O;Ic;b@us!n zt8mtku!y_dy)pHy%Fwi;9IBFO=*XnmbP>ly6qip07p` znZEQ`B+xhh%=|t#MgJw7JyEZ+DG~u+$_;Q`9!Gsd+SPg(;Rw``-t5rTA9d)PZ}+tc zsImij_f0dVmNqugdkZZK80+fL)ueSYVzo_1zavB^Phw1cJ@Q32oYoY*V3_rd# zg)~U8klziF>j0i%5)yHiu+E9r_rf^f<{N&H@^#hJbEC4Jy{{Adn3Bb!`5%<2+UI>7 za8*DHOdJgwji8kl$bC0x1?^+ATD2Z6d?OD9R*Da5OMK&=5239v`~k}Vd<-Bkm$0y6 z$5NSIB{x66Z1Fg8YM%h8?7|+OObcZu)`IY*>>~9PEMNqD>jkK7-yb$-{0nXjtVO?IOT1x`BOX) z4z7M*$XajD7UsI4`xMXp<<6Xkb^_23VUYA|tfR#Hi^SczhSySsJz&VS_q*>yjAypM zO{f)d^8)@>6b8rXW&1R!&i=&?+e-P~>|G^UQx`}s_axr)Bj1b(E204WVu5LEMUVa7 ze2c@opRhtAbJhH8!sEO<@2blDR`y)t8Y*=9?2Q+te3p|v;W6l)agkDG)ZMWOk&L+Z zW9P6sc=K4y8r*7yC*YS#v+~%?ov*i_^=5Nci?%)xIc=+i0DBw~6!Xp1GAY2n~n-!oo8w9PT!O zp4^!H?oUvK*k`E*#q6?+5X7|6pNDXD(fDMo^>&vrV*CE(gkP@zwwhG=?m80MLpFYr zmQ<~rCh{4WV@FfF=YF&uqkxO)#>nE0BE`Kkjl^ckWb^rtk_1I$Xp z2w!3b28P!C_MNx+`P-M<`D129FnRIi(;8p*&nQIk!Os#!_l0V@?*i`j5ANV{#pdUo zr`4`oe=BSIkHvbqHjP%U2%m`^z8JgKtk7w_{FNho^c4@$Z@+E(qL=8rwO$7SfeJxU zMpCDXYeH7VQMF@=!Rd7x^i3ACuNNl~=Pm7k;Xtiqa~Sn2(}Rs`!{L8+ZKEDe-3hHV zS!qnA0La=^V|-zgjEu2-pSHfee8PZ5DKP>vf4WkXVYM9S8fv-?9oL2W-C->0m|$!y z>IRPnD91!sF4fe;Pjx2vw!n|uubJZ}Jc@-{1#`L9Yf+i#-?U45Wy{sxqNvWSQyBQ6P`@be z<8mhTB6QC5S*M}RYQq&JVZ^`S&|rwA(R#(dFvd|`y~BGWW(y`?ROmd++1oHt;q=v!YkDoH+aLHUG`2|Y&)&6VNTcT)Ashe zfV=_lmZ$4B^CjDie`Pz?tR3gy2boF=!yR&c=IxDxj{1i0ZZQ~hd%$IST}KJ7vj-fV zCt6~haS%Q-Vy3YN#(yK^4rX2<{XN3+ML#6y0Y;ZibAo3#xK_}fe<5vHHk?Yx=1or% zOGk8q<(Kt^Ki;Hh*R{O+K^6&P4VoPNWTjPjmU&}-L1mV^^cijU-mZ*Kv=GmB%}5{r zxDg2CjJMD@3wA9&d(!wyzoWN1mpFcDNKbF+xq^M_%hnoY*Z`oG9%zWz4WULui2dwf zMZH2t3SIJlK-cA^zcQ&0>%B@!9Bb{Tl=Dy9;y}*~%<~)*&y3?}e`Dm*7WF$(a@X$T$M6SF60vlt!ILoHL z!{pTX@VaklLzL86OgaY8%eV47_?&LzC5;nrby!n$)(DipDisq5O%%Ump|zAHo}R07 z7H#r*G`JWy$wy13ilo(W*ql5_5NgSQM@F7wmuB=$DWrSim{RdefHvdcqrC1VtN|N& zBaE}%Im?Z7cB1#W9+&_f{d?g%2#^z@vHwO)*m9e*sJ z+0m)g1)`=sl!V?^spky9X^7JY#Yl%-SS`-zqCON@gPkGmMZVCW8*gFyg?! zRjWAa#ZDTZ+TpHHg~(YN#z*IVO~)8S5u?!bI3EMYU)Xq2NFmfzYIyVR@st+|Kz|x& zoM%Q?EaeG<6C8W;D}^O)qrgPG(7TO8)m{=w-#{4D0l1FN<+m{!Pv+@{mryJJMN-t{ z^5*=$QNx6Hn%I|ES@t{^YLzovp!B|F6ecJ7?;s(MtnV(r&yh)o*MZgYlr; zpnpS^JRdke9rk`A8gtjNJl|hpl_)c;KBN0I*Wp{cw=ltHf@_4g8pD|PTT#s8_YZ1{ z;FEDkcgZumjE(Il(AkXi%D%EYBmX&_i%yvQ(qFvqZeNW&Ti_1;xDfo?S~$RIX}nbR zkMfi);+qkNr3$$%BSx?ngYldYlB&7hUBf{`H(L5}>!xGBZuvlz9R^`-i{|D^`@-Nm zMa2}lX?h`5ZH}%Bi;zE=Au)he07El5ISJh?j@`oU&sOe?9>!Jr+fwU37{I|+7&8TtE@-Tsi|zreN|Ihr=!Eg z#YjeduTdt%9d)Ag_1hx@nPHmm*jii3EWYl_oq6nvBfg0Cq}Y{C%3fhI!qDzKcN8uD z={(H51CeQdPvG$>s(sPXW7+TmnaXj77>jRxxFU+0G}h-B18YLP{rBf$&zMW;@8QS- z#)xk(b+!|{XE)JG13&ShlK4efAk6ut4|6RGoYF5@p;+9XrqWR>66&X)?=1`fB01pO z{i5gCUd3ymo|%GVm3RCJy7@PJc3=5a&!k%KIFo8%d;wBaEY&iYs_XDQXrBAn4U*X5 zgfgf5)t~nb0Hc6*fVr%!4D>0_03H;(`dz%c)RB3A^!yIZHRL!)F(s1n?(nrE{8>63 zv!^FuvE_lH>U~Tn7SYd?p14A0Lzdto4{D z6Lt2A?e-d7BoX+RiuZX+)q2G`EQr_x+=?l{V*yC_Cx(t{qI-?j*3cN}5#X>|PA0^d zK?(i>C`vB5z{w3i;Xt@VF^3LE0a-*vf~zzfXgnH*5wyutGz2AvR0K?=$x0gvF-n#&BC)Zx!;{JjNt{wb`2&}&{B3_a z#ri_)R=&p{k6ia}_TG#mr~Iw5`9@8UM({Tj@}ryPN)czBSgL!Jndi7=5Szex$|dcp zSd~eze*pOuz7E2A`_dazjCWmyq%iP6|4-=vrn&m>_(}2)sy>LzH_R`_%=Vb}FRyb} zZfwR@B(dE<#Xk$uubjI0m)R}$>5cx-G$nJ!b9s0|kD-OmSNDU!i?4XC;*-#yfXP zs#mLK7Yn(8^{9=vL-A>$UwmD;HK!uel~d;3bt5-o z$LNCVrj3Bn{BhSgo<&jq8$dxSt<9@tEC7HJL7%UidgY;?3kKs!m|I zM>U{l^;v>#squ{ZWyeMuo$pO>)Xl0w(G069|`(TQ)w^V;!|0&~f@ zlK_x00I}L;46s&V+PbQ10G^>^&Muph^?Tdvnucko#_s~ za|0uW{J$%uKNROHr@h6A65r-*^E-=d?OULQn@z^&L)V8ed4A9dje#DuyU6mvv~_ZF z^5eEzFOE)ecN4E*+zwVJQKaWO?jW!8O*Xt|Wn$~O4~$eANoj5ev*Nm6rTEED>H5=} zsar3=veS>T>gjQO5qm7~;btJ^qhlj9@|MZ-+-{6&2l9a&ONSK7(cKr?F=eA$f`6(@FRoqxR!QO zJo)IRgbWLG+Sz)&N|mxf3zo8-4~58{|GU^oq~z8DX*i*G3G!X}esgn!xh&Zie7+?3 zPq4`_0gW;!iB9o{q37YMBuHj?v{%N8DLjH^wmvRvZ^vYWofZvZ$dfEnpS0egeCAJL z5a;+Ztdm5M@Uoh`sfv#3QI`?tvp07;zWK%Xp0`d6)*JUIlCMlgp_5`t5Z}(QZYp%( zLjYPGKroO>h0c0U*du{IuVWs)rNihtC~o?-_L}n7*!|6z|IN_=5Y=83t%eG7$~LH|9gcJKR}kgC+Y<3$Cp9 z>U`vy^R0uPyA(AL68ipZLm~5Bnj!Q0(68oD#Iz19f`2Y+n&PWI0}m4sC18EBva?A= zhXTOVd@wN_V-C6hq=j4Vcb=O*AV>hM2ha8KbVDNOY`qs>qkWCC`0w$umnvkjS2q*vcjBk%>!i`gR*{+#PLo z_itNPm^2w)5741-EA@o=EWJG@cl_IJUWb6z2OGJo6>*RlKfhh|>Xecg&kr&KsaXZO5RhM$7dmu{M&>tcH8RXun@ zDoXS8qlC`=;L3Na>@zP+YwN`)X3U0eONY6bnx@50e`>RKjRIcx@@l4gRCM_W3;z7> zB1}dyg--1L<~Zm$XJupx5{8qA-5fqx?|^k_=;zO$J-+QrE)Bj1rT}s5Tc4AF#*@}0 zDsl5d6LM^}kHW#*q6s^o)TSHiakmu?Zdq=PYJ;&-u4LrQ`B-m@$^F7~RMFsBmi0UI zma|2qa9pl_vuotozb*;%F(8uVvPrjZ`va8bvp4@Riv}EPJNvCOP5J=^C|NmWaesHa z2u_P#?=}c1zR}bm^EGFt?>d;Y<9kyCy>VV>o+A$1@|cSb{b9J_Z+e#k$b0ZQC759A z@fQ(n_4zMhMt{n0NA5eOg2kWY?mzVt4A5-Lls@!2Q)NM{&?uMX(U6M&s#)ZzT6~lG z?wD!TioU?{i3FK)nQN)$vqF}R1n9Gyl(wPzZt2`CYGmt<+Tf?nfRFz$23}f1&QUtM z$y$psI9v1YvNRjoke370Uct{C#lh1)SiV;4hdDN?8b*tBSPUKon45=zN)BkRM7E*O zY}$cwI2wgtDDcuG13%GQK>)dtB}1{pnPP>4F1QlK&xqBAfw9or^*Zqts+4 zYKe@S_d{9u_iy#A{16SY@)w_IVmO)4Q{p)G>m1su&G0v4ZGs3bkz$j#t^;V?&~U9x zP`HzR<1q|38vCqF1(Mf|WK)LJSnrudv3D%)yng`F7a@etnoMg!L5$gW3v8-xz?yvc z{C@ksaW3?ISs9pWXMP)Jtfhdt0vR2UDWA^f9d%d;NTCE&b7CH0;i1Jv7y_U=BhCFr zUYq?l05I0q#h(@HFCfwwsU%Dy_L42x39Qf4Lk)2S8$637GX(Wx1g$F@y58g~q9Kpj zdG%ktV3A#nIj~h2)Crxe+k(*tJ{9d$Fmhp7oFb(tOR}>4_iFIN-Prz$!}v9GN62I? z1HL?DH&NI(z9qNd8u<&eXKTeQ0NMDVnOeEOJHIr$-+?q)(htQFxt?Th$>oCg9Up=A z>|cUw(4zr&rgS9*X8R^X8BH%0aa-a(7#xqKFVv%=wZfst{q~?90~s}u)qioW`f&xd z?1Ju3mzL=xWF2^w*JifUTk{X1Mnq%*2T-T8r0>Fff0?O;*>GP>LdNi4GqEvgbV(o^ zxWfSXM~|p#vkEM$umQdA`oI;QIXoRD^q*ncn8Qea?V+~=WasOKF^Qa*7z`lBgSquE z6&FDvq*F{ee-4D~oZ^|XR7zoZm|h7o`W(rSb;hE!)rviL>pNzsP6*SXF0F-UU#fhd_A28WS)VFFPY+k4R>fwK-NASIwpkO4+nKJA`Ps5rGWsnx z(oz%2k@W}f#AoLg&|!bgP6F49fN-0*waPz59r5jgFUmiEp5ZT$qbue*XD%_FY__5G zQc2o5eG)Eyf%`r07T>Vl)1ACPQqZEd+X!$<8>*=0W+_yJN)6#iFxeOtr70Ly#F zI4>i6V0f)x-Eh?Wyr(XrUvlBrOJJNNQ`VNiTcfvRi$VP_)D?2>j}mv=yf<)tMn)7{ z`Z+t3Xhz+aB*<}Y+ECw1TzQN^$87g4&Nd%vm0`R)Yw1Wc&K6DN?-w78fp?B<+loi&^h;VEnsSPDJdLiOLmA{1`I6F|IrnKdim|+8--E;GNT<=^*ZRcZm z+WAPID*w#3V*f>2wwpjY*=p-py?2pE7b8B+B%3+uLq1Tq&A&Ihn0g6yEH%@4!3GqV z0}0DVyeIkS(UGn@ne{zufBBDep0yOyDJo3jGt&cMqct5YE;!EF7-=BFM7f5S*h8_~ z{sQjsopy9;r&dbOXPWpp1KjY9lh!_Pf#Y-U*Be}?4&Ro_JvxTJ+^qb}#E|5xxdSu- zPy#?>Dps2FQSjWhW+9kD8_>acH1s}LuKR&?oT+x~KQN7F%nYIFJ^ zB*_%UYHYkllNO3!l5y4n%dsa>z-3m9l%{E*qcmi~CY6k(>Z9)ebF=c{USxp9>h)_) z^rbrX%-I*Xh1Q7)uLKh4l9Ln^WaT62Yi*1bj(e=uc*4+?}q`)&E(!$lq5 z`IY_^s>lpEtGv3Zox06&p>&qauXlU2@igK1t7$d)r;JSsVS!%K;kXCO@6KyF-I0+g zCITM*f{nqM?+s+M_CH%c8Ah$wy$$o>)k!a|@vpMEG*P?$TR#z<5rrMcchGaI<=T2H zv3vLQC~jI@p#6vPUhYUVw6A}~q3#c8dcb%5TKyaWun#u!Mb{X}$AF`La9$jbiY88~ zKYz#d#2@r2en6S=`!~eszZOMQuEP>(ZWr?wBp!*oqeRa|_W}2dLv!HHhj07)K(K*G zbA?i^wFg^4a+HtNkF1&uo-!LO#-0^ z?x0pY-g6$Tyq_^_Z6p_c0o?pjg+EeDO0_CpTJ$G5Mep)_O{TU~F4ao(n8bbv2Y?Ac z_5%x8uDgO!J@XY)G%9pLVEC+yFPo-RnzsSO7ayOD2j|VNU+CdI64HW-R_Lu=jjwzQcEF>h#|3JMCo-u6#ZqEFKr85=V~n!Z^UQ!do___dxnQS(%r zL;a$3_S+D5#)V_nWQnR#dTb&Z!*bdz)1~<6|622e>B!Z=u;hBOLpd>zdy-UKwL1 zSu53EpFQpG9t*D<8)@(TOZ zDdD=yRli~8u{#XBj)%@$rNK;<^n`3;6#54*U#a6;!=X_qPYzKrk#_a-ofU1-fL=1^p>W722t4 zc%bXXB(B-a?*3)En`BBAz5prf?;*LU5A-qc+$Hc6_0uPFG^JJ!4zb|e;qrM`CUmzA zHRE{|=ZUZwW@-uSuWfG9nG?_3*63BrD3<+j5XgBjs{r-TAmwusU-T}QRTTAX<23NM zuC{YL*n{HSVW7hVozH&;fpB^tIJp7J5ReSqpxwE-5z|s?c3yx`d%)9!#^>zXqD}!{ z76gs5Aps^}G#CK69zKMfT2|A4D!fFehc7FzUyL0N=9+Q}NvSC|vN+xMleI7S%Jg6r z44`Gf`WJjz1!m@SB~S^M52FPiXiJV>khB&{>68XwqekG7GUn@^pbW5;iEa~qXZ zq)SL3sUO&;QBt2%+M=Wo$BqFDB47a5w4jg(BCf_4_1!C)asZf-UDFZWV8$CzBRxeQ z8Vsuqk^Eg;;C!m!M1~IUmd!4gXX28}b@AUZroIH0@wAS#6d=$Iy^^NNS+TFKj=L|v z0{k}6>5OL%lGb9Gu#SxLJxXO3vFsrMvNMYB#|q5Cmz$~1*B?MkSLzzMM!ypHYE2yd zVEK)vmc*tiwK6v^e0|7Qh&JL4y0bniqo6mvU%QCp)|g8h|Ejn-712!Ah>y|ICu(MI zF#VGG@P2<{$>+_aE%zIVZCl~UHJEJ@R`v_80coA0F$MmVC9E%%*EPHx*U;g|Yfb4V zUURmu&c$r?CldCzrwutZu}F0zb?>u|&&j*XeRMY1&Y$KL!nRn6EiThsD#>~DT{*7L zANbxj(8g9Pe+f{-%m>79*T3}I*#n8QxOof-iK*6}?s6OH}5 z0bR5Y%mj3Q4ngV-bBp!X<_yR+JBG!LtJgP+B+uoW3}qcfqAe{L-K%X<=X=GffBmvW zqLfF-J{srvajN?JgtXyp78m{(i-m98Px=juUsk2#P7d1*6u&m(o)016k$6@g!P6`M zgpo|%*wy&X`hJ04M_ZN9GRi znsg3DWd_;04i>hX=wjU1vcR&zIUp-n>s89TPn+aL-|)KRKSVUoQc3YnyRpX*{mtmX z4Oag3;`YVedAB;4`Z6Y!Q}wYNE9Y$x5Q^J?=zK9{cB>2c5GP%e=t$mgV#V>8UjPZi zL{i{jFN+@I9V^Zf6#K7Mvf@+Q+Y&fH_=p zF{1GX6~wWoEm}R~U+T?V{w>Kf@slKa{ZzWTZ*4XWLqI9B=Og9Ifxc7dmT9IE!~xaw zm5FyH)W z?D*p6dkNXPmz$~iHznS|{M!ia=#?snh7~$;(`LlcBsfc&ilpJdKM&H5{R*cvJnM{1 zHd%M_bXt8;`#JQmRIcrG+URm3^DYeNBA8KXPQPt(bm&#|7~T{$JCF58-2j7uk7Q4| z3LxJ=;%)h+P|cq{!~$FzWdMWJPkl0qo@Gl{%J|6t!MuUr;n?72qI@Q}qXWi!?J{1y z)EdiL>VfhKUQFt8Z)MYTSL3LQWD~tUvEh6ZD*;6;n7T$8>~CT|y-1h14`3w}2${NL z(noQ6b(tHu-?me8$14Qj%;o${SB7odu)EI2hsqiY2L}h~(ctwM#s~HIu$c1JZdd|p zQO=Njda(k0p~80;8}bwj(bu6V|4NxF;oAJ~IPjD<4 zR=N&6LI3pNg-kdIJ_wwag-*uhDYrn?vIafGbeCXcICV+@r@subDr_%i zytDH2Ta-AYTS@9!;FdcS_HUj@o7z=_0?`bA|NedT&}7RIa{ zOR?kwg-b4z4ov`~1BN}TXaHw~p)EcpXYX19RId!HKN9lT$YAL)a6W)_YlhX+#m2nW zmd457FGr3Ab5sMzY{p03VIW6?30l{xRWXYqUaZ9D+%ax9yDj*)1yGLyIZUN*v$Km# zYm3v0AY%@&7&Sqzm~k{;vzTOJ>}R2-2hsyO09JL*zl>Ol48oV)U2IDQpM-Z^BZ_KO zs6lK5^{3C4laeK%mTC>ztO=Z2{w$W`wpSQZD&?sUv9YnKGIFVVDT8j0F(p-}WLX&f z+bi5Y`(KT3XWT)VQ{4ixjB{_{!vf}@EmpI&1K>hot8-Y)0(q%*>!5pZuVrO)E&hq_9I9{Ig6d0s?2eN(|9ziG=elJix-6++h=Pik7+eM;(a zwr9gR2T)P71SYO?5k}nkEIX&_ZVv{>v401%kJ;)_M-Y+{T3UcbKSpe9BfPxg-*AC9YBaD;HF$T*nY9L^w27^_zGw;;Xzw|M`io0Z)f5!NWWukch-R(7?RYAK>5p*c| z;Jh)c&Y%|``MNkOw=~4f(YlJ%b$428VR-s0VJkBi0~@X5vI;F@oOf1TW3o)?~sj*Q?a)wAN9Nmf@+pWME`uUxRFLbx&9_jg?VJbyJ6CbIU4p4@jc2 znf0adv#Z9IhQPwzvg%4cRbVT~5aUx4YJgxW^~w9QP_^E0Gx^4{{UYa6b6bl;-AY}s zM9)4OtfF*{`A&t-7Pjcn-hcT zq>=tu<(7=6868$1>==-#mS#68miY^w~3l?4h9krr@)&0%<&t z(UKUlL?%+PBv*k)>Eg@t8)?YrU|BW0?B?+G|Dox+*WQX5#KHt~x^-_P-8|OUddG7nVuj{(+BlCXlxd(`7 zpv`})`v+`%17QttOqf&03$32)(6Jb#P$(pb1Pb=n_rqW@H|T2VVn;>hxmkf`pz^FM zib+P)zuf%%((kVJb<|{)fLH zcs&nMy(iq!Y+kn2sY>l+GnB($i5C}3Sjrm=X z5+ZFNO;mK8|MUE4;^dQN4iLXS+l3L`tKsCgMF+z#3{W-d`~2fCCI@hTZ>4jID;X$1?`?ca7OLV9NuVGKyuDi8 z2J%G{u3(NL>gcc1{7C?>YYtYYs>!rJ;x+(k5;PO?kUZr|+bgW|h~9@@Vv5F+;^Wwl zycgE23MgfsZ^L0$_~U(M9OdQQ_!NYRM+wxnd98WO{)rNk9Lk#iq&6uf@!9I)Ia2`? zZ$Gkq6QgWvhn>^fD!wb&SzG&T?$!4!a{?TRhY!bo{CE#QBr7v03zdszQr}>L&O}|_ z3~_+W^-4=X479R@EPL&82>PdoMugPF@e727gsc?`Sk?NLmrV@_kB@WRKdTv3IV_uM zUSw|zJ|8@g0BIseAKO3AK6VpqVgWHRdoynWIny#d9!uMA{U0uc4%U)3YpsU~L1AcP zmQN7NRs7z(#^<&Fn+LIWlPPoBs%3iSk7c4X%o1uc?vp}sO7gFHc`5&0*4#>f_Lu(* zv<>;!JV1wnnehN&wdWe9U54glzks9xZWHfxb75x1DnNqowT2tp!aji2Hf5eWw* z`tx#k>l3%<#k2n;C8HX-_M$ydH37Jc%=JE9R5U!W1W?M*JQz4agI|n&Kp!5GrF~ue z^(F}4w%n|y#d}xo)J0i&V}j>6t!YMXVBhZJlI`aGRG2KJE3rCQxm>iXJ;qmdpNA{b%JFB9qMU5=z`%3V=m3#hv3l! zyXwCXgkah39|C#~0d$j@r^kJGeC}u8eUA6tpr!I}_;Be85r{4K0MjubGS=a?dW)-X z^S{hRsqcN5?vRA^YG*o~ zGr?wF&|@!sObtHBo9DZM^WPFI+Gc=yzej;cYQW<5#KPp{_x%jhzEYWM$6!$ER)Gc-o;vkXhQ@UD{qE9nPr@;^8ZLgNI085aLG?zSFwUHNW5 zTpdjmTLFC{pvpdi$@wi{I%{OjX8s0b4!xUx=$t6m>$4gP2$51SA{SMQ;!O28nSb7R zet3J`?FpVV{D}B&|M0=Rs4CKbNFZADKk!eQfVH9moi*?%Hcyw_w(|lG>J+a(Tmtzs z-DcuTt^jCB#TJ|^{t^dMVK9ogI%rRpxjmL?#xVQopEEbQw^ykzO!KEvk4wO!h9rvl z;mf_X_SI`%#>?qwcuoCDcQUIUDux;_@Ke9mOy~v0IG7FqNv|MJcR`p3Y%A)H!o9v{{24n z;;&PYS`SZIG-wFMzz!uRUAMt8{m202?^ugpKLTxbQd7iXQE#ZcYm^&5I?eL}Pq?P^ zjDv7xj(Ib;S#unoppRY=o1n+1ag3D#g{4nBFOBb1RyYC}uuLYEfzaT*i~bq$W9ZCm z>SW$xiYWL9JumnV&ZPJ@T`;K75}BzuYHsjiSanO951=v8|_C3MiLmwyyw#(5H$^7H5;vY!U zIcl(e&04tDzU;f17`TScxHc?vBe42US=rePx;`;=UlISe&UnL&MM%eVg#O?6r)o*W zK0NG5?T;pcJcp?5-S_7Vk#kF~gPs@wQ$KHfE>x4f;3u~1jxM$uD1=4C@WaGsRS8&b zw*f09e!IN7>6w2;2M?EU$3T$Y{CENlz?lw|nq-7@h#uI0TNAi<@4ZKi;yQLBW%dh! zHErn=6^>%xqfkZxA)$WEyQV#$i%)#w5<*k^GUSQTwVT2!5PC>9oE0{8Vy5@{K515V z?z7;R*^hYJLXx4ss~~0Zc}$6Q_o}hK9hfuh#4LCseYH4cha>e#>G^Sk9`}7ol~+T| zq1*3XV;4N3E0DWS9(Y%;&u=?-kl-+gDB$d1YbF}%Gg6iw z%F^pjglu7di4_wolO+y@Jf*zEBe%K6d9$g*5N9vY&dOx)*L(_C(pAco?Lf7#=sv zM4=>$2w_rh4LMZQubE9?zh|KhW5Txb!3*>JIXReHPSp~$B-(K?B;owM-cLPI@g-L3 zTP3knp|ufgn@hT6YYHazifAV5N|s%b7M5gzKSM?Y@)4mC#888xUCPdb`EsK4or2_2{b5-xrz!pL;d6jjXV^ zR!_h1&x0jUS~z{eW@09CT;>$jH2_+Bz{vm}1!VHHH+^Skr%{J*o$!s0K1H(34Z^j3 z9}%P|~^TjdcjxHZVNms23YC6XjwPih zih$M~B&m%*V|>G)cuWR7h05t@0O0&;ZAMAq=9BY_DTuJ2-+h2YWo;$ka1f-#2vycF zkOAdS-yBO4#VX26AmTsIDL_P0Dc++= zR1YVb=@+1kXMU^I?`<}oFT)70UY+xzAlN~bMs;XljuJO-+YSrNcl8V<(gKCN)^?22 z)N?`vu@Q5x*7du@k7K_q62D=3pJyk(=fGm0EqfBVw*6v*P13$o0W0C%|F-xLf2S2Z zpcTLEb#PJ{yl-P)fAc&3xeg|-NW-gunIPLA7Zt_zx3Y>}|UlAKkxHm}ri;_v&A3OJCS@oNvUR zgI|+aUCk?b{dcwnBz=-|g&-m)v)3+D#qmsXXwSGaR%iUtlTHqr9+Mr3dO!Hl|F^U4 zq@vh3zsHPV$Vn!8TmiUtW2DqrNqgIWPV|Rj(MPPTm{&s9^eg_>5SU>_yL!?-dh)Vd zaM3s(q0nVTWMRWze_dotlyqMxbt8UMa_#8?Z;>(7i9AE8)H^T`!XEa^X>sI}#Yig( z*ALiA7$tt+=$o#=X8Ws*|7ygpp_b=SoMaKGG;FhK+_CmSKydmjLsOV<%t8J-iWqb% z#&+&_iP zIUt~&Ty8LvBMwbp-xj}5N5Ng|zAitoiHxIG5a9;K zQf8XO4KP2ducrrv!S4qN=QM`gU<*u&^^&O2G1Eit`uc+)VTAyH9BFuXIEByG@)Sh` zUAokt2=~_-Y#x0$>`bYnYan@tTT+c|y?A_deNqD@f^v{Dp7KE-Xax&}do;Y4nEkst zftmX=zrvd+UZYwRr}^qZ@F_M;1Q_AASvM8n77Cv$NsD z3<3D*w#?XQfZd=kFSok*v$arfizi<|Osu`)5zH!Gl zIEmXm0>Lt}C`kerUJuZeOQfW0Q!^AAit1=^&p&er+VpT7CyiBnx65TEfCnBxrkEQ; zlGLe@^FpoV$iBE9HQCRK&nK;VTyiraZ7)e#QM3%EEq4D_96dJ@3Vry!=sUGO>UmMb z41Vf7-YX`s(@!z1`ex_mBKwmOq)z7l@KzQ(QtsFh#YZPn5D%D_aEt!B-@=^*-Rms< zv+%_hI-$hH{8h*~AUan6ZrA_qFF7H^c8qcncsSUR@}D{6sZ_qu#3n@Swmp8&MUKm1 z+o1aLB?}#4^B$iV{aS8OO$`z7`|%mnn44|ea1WHZBN9NLpLQIbRjy!G!rpeqgk3L{ zf+T~An;Rbt26Mf?8pkNI09l%fit3ed7DP`^FKF_Zsu~2hi_G$J-{T1}kI!oC%vl=h z=!7Ws>}$)do`9c3GTtH?AD_n!S_z{61w}~6wqGp|9t_byNp*uG1F%r7$EGqUU!Wc^ zZZF{dV@?}E>a2qD@^Y_?{olXkLE88^kz-3;>Q1y$Yl#jv%Ii^9{C-GQMFs0?jRl|3 ztRfL&W<~am(NvZYR(bi?SX(*b9%0SJhuVlcM1TLTm6hfzx0{4zj3i59j1VqT_HfGD|=W9zMA(qkE$) zoTYK+?c28_HiC~4V)*Fb%HfLma+dl-d0bu|1Mnz8Z{cd!ReJ)E8sljUTM_-ZfxNRpUhCibKT>6@SZc&eSTv57(2Qj+;=*H2p|7l zRTTm7v42Kg*$0X8!bP#6%tNr* zf!hIP0~yeBU0hs1#02N4KDy|Os;YSfS4kH%+P+UMw>AF!#S(INCr;X2yWFiA z2G;LnP?_Kunt4;etcGd8eq;aT!>F45Fc zCYnTfef_|TF^hC%?zlpxERBt=42$%>r*%dVhgY8~OTbNJX(-3N=xa56QykCWm9(iU zr~PIEFXi#Shvc|W0XV@}^j@#Ljwv?Y6N@W}*YjV$$W@2rQwX?*!RHNJ-KYiYHwJXTs_Ctv4hyK$k)vI6! z84siM`;YunM_DhguE04?(JCdujU-MUP8qd%PuA)V&1kWbB96y}EC)&9Odw1XS>m_@ z&a0bSjqnwSLV|c1o=LsEav(lL^jj*jka&4}t1(Nbs6HD`ayeh#mJXlQx%|&PDj}pF6xCxARS6C)}#?1+G!gUT#0N;`>a-uJ+gIbcOkeWQx8}Ygp4J zVlDSxnz~xE_S7+e!Qj>kb}v61-lcZz6IP7jEc7wtM%DkTGiKKS!X4ljriBTRzI3go zuFBXU@{$zP{@_`W#;QWhY6+GgHdt8XkMn|Zqs|CFQeMu^B1)c_4)ntKf@XOn%;iuF zKQ$Y^JlXXCwjU6F4cT61vwhc$%qiy>7QshXVGI%$%)$pb?7b?c>?!#&&||@1L-k@( zQP*XTWXqIRj(RbI{#UNkpg#@`PHAVIhe0jtQM6F4FVaf#d@US1EKV(v_?3vJ0*KV$ zXAw&5FgdSu7{?ivmv2lf5JkEyfsk+{NpyXEjR59iTRVE4QM&ly&1VUtCwGmV5B=xc zZH5X)eB+giYF$k10vel}t*x^KQP-Y`MVmsFpmNixFe#f50S&L_B6gD^C zO}9lw0T=daRNtRB&SjzAk!+)kJaAP7f@DBG`8oNPJ9!pB+3sl1pW|{C7L1}U-y)27 z?kqJ~8dJs%#;s45>sQoz{dx>5SiN9|Y@8beLZ!9s)AIh;Q;~`(;Pux+_*lZ`N^*neWD>N^^zymW;1E7G1 zfvXvX6Alsb%G&}Uq*qi52w^o|^2r>~0*l=Ov zw&5on#`J26Hi~)vewGGH>gY2tjj3g?e$0LJe#ROM5Nw<7q41b@P~~k~!i5hxCJefI zu^1DGK;3H8=Ecb~CXb*S0%IxAP(D)uxHb=&2N0+*_^~EQUpjOEL+6XV2*>EdD-g4H z4i3O-zmhtP;5W#z-D#^{76YNv6E7u}b70`+CfMucODoXh3-)^Ycs_JhmFeU}mWQhHbvqxQLokU~l55hMQJBRy&mMV0i+CjgcfZS-*9W3?d zLek4DBZ|M!)ST+IO@lG#f{%|+zwpE-k9AD1DptEL6poLLA#)jJe<*u{b?%LbbosFK zx|{L!==SkY9<`~(Sx_LS81a8})C z+zM4XvJQjf`N6bIQ$)@p@~dzC1r2@3@YM~8j7iL9pm53y;JHg0Q3ZkMbD}V5>_=f# zoG{tb$742vf&nTv=uMy2$gZcJ2i)MC=`*uv*(*Y=pnsO&_~%4wmbKM#6QmUwF#G>z z0+*#Iuor+6sIaO^Grj!hi&jgPSJk_!rL#s2b_8%vhN}&eCgxUfpmHRlgjlO5x-W_Y53#9Ba{mUKx0Z0uq1?>&>A=0GJW$5o9{N8AyN?6!i7M zx-UND%bDoo$9G<8BJf!a5~JiZPXL9MSsMx8(~sETEN;u(w(=wf1Ioc2s3RNjsD?01 zJUFgC19cbzG|%cnTxriQKMDK;1l=Ie0YY=!x~RDgs}(`?S^@m#r%h#>)!5C}iVD16 zUQbVP$yt(#QwGjKlY=4)bEFLO4s}#00Fshmr=5ciB|;@#n2N}Sk3*#awo`%tNZWtU z&gM^E0*o93V6wA8WjNwSNx5)`adB}m7J@D^pk6SxC^k?!St*`!eWxgS2=vEsX)40O zb89SPvmZ3{ez@WT#0XMPU*GttjL(57EHe(n9CP4lL;fI$sy2@MfM+5ct9J)X649#6 zadPiNlhyPqc5E~Z=_p&&xK$QfBuqHN-~c{(urm<`iI5}Je+D9VI;@(`Q<{~)V@=1e zpI26rTps$rDv$eM^>E;Vla6rn=IU@54kWa$EvT@fU}LZb>St7aeV8x80=6ogu{dV( z&mLnBR%P7|wK^_-n!p^#LCfa!*RN4?6^4&8c9N0^v#!M{@7)i}Y(jfhbzXdwq6GD{ zZ96Ty2I*!k^9e&NEP?biEkG>3bH3*g;l;&57xSCvEzQ z54&*I6p1p1TsD_!o80u2FtI9Pu$@0T@H@2L^a;z`7{}rSRB?_tU>7_>_4kVo-y4uYNyBGW>bIcp##Uhvfl0umO&Es<1qJznQ6)z|7;hp1>+ti1WM@#6 z!VBfni|^x4;HBO;DL1=w||K?suFWZ zWaYYyhJe~99(V&C=MS%%U*h~dJ>8g_|GGZkeIp|3wg?zL5?mAa4ir09+)2GRd+O;M zx02_?f~{zvja%6C51c}njsRcwIe4Nr4 zyc3vHxz{ShAo>imm(*I~TjX>iw^9o&9`{@?hQ#2ZEKDJ2^V>(xv~mzl{>|PtT%1_; z3gD3tMk9ZuYHz@M7F?UPdr6^Ay)EB>7aNw89%p!?86&fjLJ#$yEV^-3sQ9CB2pTt~ z0@P;piBvY!QhCkuPQK6^=esgj7kutu$*qus8Nfxo0i?dc?(e5T%YnxeBX`I6?@^GY zTP>DlK1QS+rcgY3*9@VgIMDfX;4ht!G+DVnx__Q4H>Z$xZJ9~oG0E>S1+8F#;|a*_ zqJ{=CxG6OG;2%yN1ZC7>wwbv z{PS%m4m`9t>1vnked`d+bOm0#ARi;7dXh3m;eI?}F${Xn!oXl53OFVMVicJORqo{* z@q}8{@myJa4Nlgd2-)@Huzhhp>C+ueriDN)1;3w*lCD9A=Fauurnc3u&&3N}K>@|& zi%8-`c7e=^F#|MtjvA3glKV3-?K&xWtO47chl8*7qnWV(d%l1?9cuI5?J4NteHc=> zmUr7h1f=delZfMcwJx+$Qc~yhP&oPStKRu_dIBMz640R0zFPag{T(epZOTL{aV62)GkPgdSWFZPf6dhWwH z?BmzqAW}tz`bzSS0nQL}n+|YFzCde!=a@ZCOs-7a81yr6HQJKuv#vo513Ef-{aG%c zvm8k7m#(`*RGays z4BNJic9weX*Kb?6mrub zJ9c$RXHBpt@zn(b#Gd40eVQx&KX&F&zmt@nAS)gRXuw; zQ!#ZGWE{n98eTNR0bE&h!zsH@c{2(eX#}&zKU^gXur+K9oB{kia*Yk?C4_)V5qyeg z?6uTiFEh7R2n7zfQV)GtveZWfpX+|(y;Q~Rtv($BoSQqHifeu$2OtP3#EzaD92re! zYy@oj!%Swa zL<-=hCp;-U%)^CVlqtr8;`{6w)t#SwzC`B!zii*Q1mrITo9RzyY2cHgs&K8cmKav@ zf3^;om8Pxf8Ko*JkCsZ<_5&xieb3Sea4FNw<2r5a?O^-8cQEH52XtBAy8EC*0hch~ z3po8_gTnxg10Wr)MTz4la~~l7;L?Go1d>Y~WTeqn7B??2L>xb8qt?VB2FUjYyNTCZ z74pw8m@*u*+4g>Wv!tl1hN|#Xq@iT0vi69p@KB$+%$<+ z?(RCczJpe}G!S-p^i$;Dw-Y%jzjt04rzhwO+TlgY7YqBII&-@IJoam?1}eZ5h3f=V zUjC^oe#U`3&k^*|XZ3ys%q=J`eDm0wAH^i~(J9)@$1z0w@}dD%aF)i`*oxxEYIa?? zVwPZ2x>4Rg&h^}BTX}KY*w_g4Hq~`agm?VzTd>ug*|Ri0-NZBoai}cGSOq);noXir zhY_%wUUu;ftF_pa&N_;phKEDppC1^1B%w+Sl;%^?)tm(!{2GGr^77KA^A=&6{=&yVXU0D zJM_L~Ic%x2=RdPXjL2BjMRSw{v))WLXP1mRi_d#+3A`TLdS6SCs4T$GAELv7p``ns z%6NB(M}utxiSQ2yDCSfJpAX93q6eVizB8Oq{1m(nSoAMCl6e`#`6-b>adrZwgmAa!^mx& z2z&^ZC4h#rnEGWlHcd1(DNPtrs97;;C8JiB+sPtD0)V+^E^fIX4yIuScGQ4=U{ zT-KRmhThT4DWjL>EA(-mbXI1k6o(eD4Tvx_&3?;h`X+7M5DbB?bL&E6WPN=8L%j{D zJl~jwPHgMgrz)np2V8>t`2DX~-f{$N<=#tC)f=dgM}^OrX_iJwwVqL?!H06L@6RLG zzO1e$pPs%1p^Eb$##w}U1xG*udgF>K@EBXd>MhW17PaL+l7wjk^=XW(xS(+{UNiaT zx8Pv6l5rLBQp5;ZtfGx*0f%LxB1;-WRKY7c3ufZj`Q0|cVGsU*Njg9XIT9A^N`TG# zZdXEMRuQOe)MkZ08k_VaNn=O;m@4UlAOrsQcfE*H1HDpr3lJ+HKn`(A;uJha>~qD1 zDYArWu-zNej6^&vElDu4F$R35S$b`pwc4!k#l8ARN_OupsfrG|Td^2ooJF}G^$r^0 z3K$?<<9WoHTX1?cY~G44$*{CPNk|!~B^%;k$8WYLS{R#RINz8*`rpv;Ep_fvYsa0xQQp_Jka^=Xw`x8UJ8nfr!?;aEOS98ml zL~K+}8qxUjchvvna9T0Q5n~9Pu%?c--B}tqI*-mY7naaGsPK5aVLaOHR?zcY?gZ8X&D}7_oazx0z%lX(nL7-HMX6>NADGrg8nOCg=QnKBF^RSl4n}9n$7Hxgr_MS{&+C;6ndSD{Q_Za6(88a;(m-R14H1Im{#HmC z?oC`pO-2jF0|+B@toSmI_>CeTBIN{AmWFeoDG{6*u(we@jG1z$sRJq*EF$O*0Oh5i zr{)`Kv2^Ll?`|^S6TmDB7}3$gn2|q#{@~Xn8?||iYq{=jWLGwq{uDP<8}Sei`Tnc> zeeAHdgpV`TLL1kG5UT;UDL^*30*$sGSe$0gf&dZ7c@*RQ;X%r&q_)J3L{d076#!vz zev0h3#3v+V$)9?J`lLf9e`;);FQtr4ahVDG$8llw+l6G7S*=VD>{;xYCzHaOj`Gav z&4Z$E)G5xs>)Dr*Zz@iyq+H3qg z|7)K1;AI?-xJRJylFWuDg;JR(+TFg2h1FcoR{T2Y>Rt74YUDL>@pk~x%Jj-@iF&?y zmroouxX?Da0CJggJnpj-Gn#)M`@F7m&EXXcS;*wZZJsjpPM{GQlgac_WV%yYillWe zPC4{JC4yQiE1e?ma!00Yf^PIM+r4+ydxhkP6?b=E5P!_=*yo-fMw&OrNU`71YC0F2 zLlWVH1>9cTOC6Qf*UzeA_5X=uiHag$@JiS&X3z?-{H>;1>5N~qeZV@hlHwTOaMZs6 zbUKx_;B3{`UIQDvICE$Db27A7VC;gm<{OAS{)AL&CE~o7Dpf2xqBnv#I%0=KUDZTO zwxTeOI3+@;dSsk*_1Du-O&hDZxEL06&CF*p<1N5fk->?fZTS&qw1F_5s9e0mnr^tn zs1*}E>?gc<5;4cT52*AtB$f%owv~qbD@grKvhL!5}2LI z#(5!+f82{|P(&`e+ZxU01DWSwaMY`;8KF1*enDQ_)-${re1c@#V6nbuhxBM=P&lll zz#A-*^S*u)RXp+{m71I`S=bNB&aqxjhA&Htxz@HBdYq0vUVir(ZWNByVeS{Qc1B%7 z#bL($>44H4RpCY3`ojE$yvxaYwIrFL&SdtMo8K)NJ<%=@F-97d8Kk>*HOIpdX;ttm zgjh=v>KHmKJg>$k0k749^OPd$E89l-7Sii6#nU(ZhC2HCd7g-EZ#(kDtmQ1|ydUtqp*OPU{*Y=%&C7?8ss_6Z+^1PaO;p|AcvP7G#qK)= zy18&V?R#1U$P)Z^qP~nrccBAS0z+=k zMHRKym`@g2I*w1ru=Vwp{>$n~y3{|K716whHQu3K6K39JKj+yJMSfk(pZir>V&>dG zu4kj=e}V}qWWPyxo5pSUwe^iA=vlJ<-sUab%48=A6QRI<@p|;fd+m<)_IBha?wV<) zL%TazSarbRL=)p-Zul!uOUD_sLjkPfrINFUDSC(b2?=@po{YgoPZUMZ8%P>6`sxnM z*8)d}0~2;37_YP$ipB6Qbm0+p7>ZjL@($GTf`NY2VR!eFaCb*jH9ytOrCF!JYwQ*u z{?t>;O##DzJ%hZWYc=?nC`xH!_6(VaBxZ$#8Sr*j>XS8_dHJQuY4;yb2vQ9{KbqOQ zPMn&0(^76cm0y6&StM)8YJi#T60xObmwu<3ju;(HD1SyrVAhRi|0y1+)u9d4ah2n# zjKqVNkyr@VWylS6yC_=c&AF(RtU4L&@skH;yl8gDE2(9H7BSL^ni?;cw`B#ajHicL zkHu#)FaEoKjF|*Wv}X-7&^pc18+XCnw$n zj-fd5#r)#r&y@g}6p9B1I*T?-5vimKLStG{XIY82C8781BmqONprWGkwqjism<51v zF>fA_NM~0{lIyv*T!)@~8!Y~*7>!_r)zn@MBX@B%n%bu$ag9&|z7Grm($sWRDt_7Z zz~#_kU$YRWTLT2~7YymZH(Nq0rWm<@P&EHl8_Pxh#DW3{4uj9yJ-!;-GczP7`-RN_ zbI;XbcfO*mm9i($F<|o&Xdbin?>xN91S%3}=e0;t6(j&kWR9roz$Xgr)bnQU`BOHl zRU^TD3TbJfkDV{^RjBi&GTL5TzDoB~3z||BTMo#C#+2o^o8OQM|0G?WY#>Kx1iAcX7#td905U z^^_)&sflt(BR1lDv6TwRnl)n`%st140V#F+R%vx8Hopx1P9ZmcKpP$wA8QET4=ycq_6BZsWP;6PrJw(~d zI>=Y~^W2uI+`%!yboh$%#eR#0SYBe9AcGsvYL(dEX9TN!CHS{=3>YYgOsEgAk=R4N zrd$En;-Db%&BIaxA+f7+5-f0A^%FJ;c)hd*%cM}m*=~a!_UTWDWYYu;>{&kVC0#mL zI*-YgjUE?uChN~fCKHb|2vpw{Xb2lOV1jUvxZ*p~l;$1HO!YfnhqK zpBko<;-EIH1HMd}p6|fyvCvdS%O~AU6D0L;=x-im_Y*Df?zbQiL6i(K@s+%)SSE(< zfAlreAgM_n=Ws=*8?sxXjoylK)$td3rg}Ad;5Ae#;O2f!u+13 zDftKm4)A^eLhC!AsL1jA|AMkg z908a!YJIWwVU@({53p1jalC5};9l{)_TF1wxy+sA_^bKzB%P_G-=`3{8YmWYaP(E+ zZ)djpKq99=^if3>q3JyErmt-MCIHtJmN_a+fnw5nrX@_KWByLy&-O>$ix``okC|%nGW9Jx%8az2tPDwfv)Z?3Zi!HNQj)9RC5m7;L}? z>2Wbj@rOVM2USMht`F#z@{a1&uvt$}4=1rxtnRy7Pi)BB4)`|f$~-<_5JVmAus(ao z5w{EmoQ_aF=sJAl^9)|?h55`=>X0ha5OxUxM{g{C?DG$ytl#BW;h>K@DK0r5x!DHv z{SjN(<~Vrj@_pxfolCP>^k5r^ljCnZ?V(M-cmcLYp}zu^!N2Uq{n=jm$CJ{Lx}>L(vA!R zRdOnPx9!OEu5EjQRY6RkbTlx}{HUkK|9euPH$HEX|McgO*ds@p^`MRuB@YXz`@4R? zY>F$2R?W5GW798Gj3PHx98ZHBkEVsZlkqFO*43{KBlS0AEr!ZYwu(Elkz$f$wJQ## zGaM0##M)n7xCfyJo7PIY#2Bc0`jO@3M?n^CsBn}W=!$7-c3boH|ChLZJ$v8RVc$wa zju8C&fh7Cc=_wO77uf{U8nh;fLWKC&6TRE!PJcJay1&i^Ow=HMA>L9AMtrR;*e-05 z60pkc6`Z?Qdd$A^75n4KO8j3C7x+%qjK}XrGL;~5PiK0es3xub$k#EAxrO_!(ev>B zp%e0@hfn&tVyoHn*Ym+l9RcGAFM_g`$TaQS&^zHwFUbgp%*;0r-eTBR!XJZzP{>zIbDb#zNx;oT$m)}`7FuQ&5SRG7 znp>c$T5A)x%lEK}byZ+{PGpFsKPPaM7YrMjKbMrfe*xzlxdcYdi)=s9zTABsAC~BNWFvvSAsGZz#81CoP{8_C0bqYI0z*fS&^18#cx%ueb zyS?r#8Dls!;Nxp5u~3EkA?670P6xkN2>xvInpd(Y{X$F2+oWNsCg3*0@Tk z;lX+0RsJBxTg%ccDJ4_TY zC6Y-7c2?``czdi9X>z){3brj87#13lE`yzh9aC=gNmY3;AOhDyQ(}9ENu?O_vJ(7c zFTVXi@cQLePd>fwKxQ(vREtQNj8oa@LG_`(>e{&G1@Z;8Qfi0 z&R9dn1y4xZe_(%fd;`rv*wue7?|5*$N+u#=*iGLAhLE7%;iLWMfn9U8T6!4zX)|X> zc`uQJuKxm+m{DIb#UQO6xJV5!N-XuNae9q%jt(ygN-XE^n?n;U*>@vP{@+XRQYNv4 zutV*iY~RtjRg0a+GnbA5D%x^AN2}^}YgJ$C1ZM20pn@L|ft|EGAWx6o5V&$khStsu>rdOYSG&a`0X8p;b~<(-aS;|<>hmm0wKF?&k}n1@z14ex|-`5 zU8XR%A-WRBFo8SySRmc!3hlHJP8}1UO9M9(2qS$Z&u=jw+zheO%cSiK;iJnD%j%M! zcM)j{0g6~p-v#N%8bXRuSL)D_L@2}PpNFM6yXME>Wd;-&ppgh``kIG4E{pJ$B-Gaj zZa=`ug`gyD_BiX>3eV(}&bDDa>p}5`!e|jQs5D=V{1^TvDHdmi^^x-{AKQm94f#(# zQ+;Zw2o#)1wlNHW)OCXHaGVG?c{+TWzMHbC=XCp$14Bo7zu!k<_N(W{B0(EOkC9R? z@X!_LU}u*AS__or*>`|@mg0IdkDN8P|DXvdOW0(i2PYP^2_p4Mcr zzJMM9B$cKRztdNq_p^?^dC`DOR?-EIZZIr-v99#x_30e_Y-XL<&*^w=Nq5%MX&ZC> zo}L~#TqZeUsK`3fi?!%0wlOx?s!z~Q&~uH+B$>_i1^?1vgksGKb5REkl7d3$Ur*%r z!x$3}jL3Yb`Zv$ z^5(DOX_3m&H3SL}2bjqQV)V3kr=V*ScLSP=IV3|`jT4r+0&oo=Ec}G}k4=Hia^WRT zwUffSETqj)LQ!THir#9fZ0AcGr5lS=!a=vNJ5`tf6fV+A@KOj&VbIQfp*02jTu0H#UMc+?=yKLlhQy7?guzEI8L$7{I?^WN^0 zTfUrea7!x|6)2w40ejZaH@3wBY=9^|-eyMWkxbaIo^d4&7HGH4!O1DyWeNP9bGbXe zB6;3_iI$`QVbGNYsDJN4jtX#Q?LG5awifQq&x&%~@iJh+wxY@|d?#gBAe0tj{5D4O zLZtlXojFw*5L_2CpMT7c9kNIz=_d(DdeMqK8RU?SHp~gG3M`+EpyR-%?)z_~(_WZu z)%{H_Z`3i345;?^Ic*xkv8iba4XEhfx&TBFz6-S-1>Ie0b{_hpl3_JrkvAM0pQLg) zxj!w8>EfdH0sqJpt4)F$(^RfsyJ5RDN`X)uV+y>!1V)jIPejOZLp>RhzFzpdnjpbR zAti~x0rPeNmjF0Yqw%mp4gfy>DC@G?6u#jD!#doV> zaO2hi&&qIC0SFEc9Ce}tCKWs0{q~5n`geFRlYyEjEizy~3!vPwRm81z(F7CpR)A^^AV2@VJ ziH$~-w=Fy0_`6#wv&8t<^>(`sc~8?Au<`C39%c~sCT+^f6+0sevld!|Xn~%fJF-QR zXgn=Kk+qA}Fo`(Ry5w&Z2m;!k~wZ1d3HeL4n_N9(;biL>#LKgsT`3u~*OPp3iZC z69o79Kz=uaOY#YgRqVJ4+YG^*gs@)3Gt^)3M!1GIde5K#{}U&nI}$;AQANXw7`7or+h`ISO4wl z0MXpJx?PWc0Av`~9T_9=W%l*hyys-W2eMnjuV%x>o12xtcEz_t3PsKAiaQLcJ#rO) z@mb@4FwS!p=;wy!vy0%KM~OpR{YhNhle8Hk zaK~)Ew}tOfIQ=jvSU~PQmIV28GOAwObTz!NY~(H;G)++S#|*6}90eC$J}Rl_tOt0@ zZEzn31pKFB*Zv!4`aq}MbvQQ6eK3$mPWGsnR9G|hzFn>-=VP{1H86yF@x6Iwj$V?j zQg9N#L>c1r+aFW7O$ziUE}5I<>u^NK7oZHmR2pI%1Otj@>+iDCsgbr(y=a6}4FymI ze#-OHct(sD#^4CMlo8rAEEc%1Jb^bT&W0(k4TWaK08!}fdZnjy7|>jonvkHGf{fcx z8jst#01Q{u<62{vX(h`pS$DfVbwl>ft`aFcAwHKc?H!Sr!zlP0je{}L7)mGLIju(_ zFe{txk4icq9a>PPYGj!iCD#Wr$`QBwSWOdid=`)Spwg@Q3lqrlD6nY`Oz*l=n)^>0 zSUa3WlYxb9^Y+>S2bfQtm!fQqL1{T}0ZB=pwN9-Av(pf8svv>6m?oMr7y&d~cpd^= z;-#sBuXQjHt+!Aq9%c%9n_Qf~eZ&FNz=*-iCOn|U9`ku`z;FSi0(;_NHx}ri>r*eW zl1K!85V+%U2#k6$;U9X(p>B4_HFbOxN)(9lpA$*)k1Ehyz?bAv4?5`A>hzMNL@Oxd zP%RY#Eew6|e1=@#jo3^fdjOXPFU7>xK%Z}K=h+G#C=IognK0xupw-=A(VW3OakDvE zR_61rxb1ukf`0yKAeH(`Cdrq@0P&~NxZzIz`2ai0yB7^5h(?-0^9LwNfok;rgbsPq zd`?xgjBd-lvo4a|a_B$Bnq5cdyagpDUBKU{P-aEzd z?bz1QR};4auyx1<-S-5BL7#T_yDBNnrKXga!IUwZx@WyJ{F>-a^Ib_+bsW~1k%mGz z#+%;uR+oe3yJ#IXrj$`>{7>A)MJ;9ZUnOMjOx<WM0EtAxE#!k)!u>$AeJfg@a9dZN2L0wiv10XK!1xxl*@{k;0}|6x zZz71MX|fzFr)=qAXMhVwSQ`k9?Lp%N$nwAU!6g@WbaxE+wP;QE%`73LJh z3L5KE7r&*0k|y=~V=ytBsKo+P5zZ0mW#ZHI_E4sqnF5_YqvaZYeo~||UXmjUC+-$U z1Et_5w1o~qYjC+16pfP3ot}C6}DF$P`L`247IN3@eMX?BRINK%x}bVY2-?~QHW z0B{`87fKf;5Iy65hd@o5cz7mms4VBsVHfkVC0ytqKs@1}eAladg?WLHUF;;r8G9#p z_4|{#WV>5k3z7C-MQm$N5AoMQmoLZ2&oMIk_Lqxkk*M5itOLLfp z;87IUffQ@Bs-YVA#v*+Vn=N2p!kp>2*mm-#8v$1zBB*`gyC~%a`?Zm$Xaku9^vy%7 z(xI(Ucs_BaXUKxS0l?-%85-Ko)(=gdz)_a~I(IyP$`#Q)k%ZeM^w{tdp&il|^6h78 z91kHPAf>)u^1TpRt_b*JM4snyf;h&wILSYTJ4vncXdjYxa$_e06s1%2l zo6mKkEilSVw#5mfnFaWbkh;6Y#>qi#?;0#aXA~Z)7bUgFCtF^1>f8>Io-6QW3fdV! zxbMc2_f%MYc-v(?O{{!nF_k^;r-*3L9HK^r$+MvS#x-2oS8~}HTh)ky($})Tp>0nP z)~eu0SG&{3-ce@ivjvNx1^LY!GMI+6UI0P9_$^=8qKLRoH$17)K<~KQ2MbagQ(Cky zXt9ORGK7t?0Nf5Uv@~-J@=w%^W;@qKsONBB-UW>v z?{q8V_Mi`QQiwT($_tbrW(cxCi#Da#(D9ZOsr?k#7{?%tT+6>^lVm?z?*ZdRx9q07 zx*TiPN=KfgM^{%g5nAW51OYIRL92r3>XLy~Rp+(qZ|;c+-&gwdZqkL0<~8D_gXKFPjx zVyQot6Zgzn1Z(P5D3Ev3$Ea}#_>;Uo6eU4r$G6kf$RI(C?+N%Ij{iv+|N4f-kJJ97 zOld@q*R8w>%ZF5rgEkQFm?Fi@ON$hUNkEQ3Yuw`1Y;rGQhm)z>iV(B>{EM=ff@=Ex zgOeC=ItCml-`t@D`r~310Vpo3Q=g{xCSH(%frRf>8Q?+}y?skf>&mNY5= z_o68}2(=2(yg&e_PzbUcs5&qerT^&!0aZ8%&Q|WjFDMDXkMpp!gzr*d?0tP?<^Gs2 z$!xvvWy)$eVIYdbdI8d;daCYS9}73*lN#TMDc7Fpv$SE}vB*+D;ODFQ@% z9pXJ}{=U5{j7s@tQ@yv|d-m?@=OIhp*4RM|hNC)Eic!d<=MzO!V zmRrIO2^*5C83an?J)W`NNyT_M5%H40ScA4?Cz%lTFTP6;x*bNDrR(}tQC!aa-4HQA zBlW@Hje*f1AA{f9w}7OG^g%;FrZWLa**iy=l=0Z~w*gKm8K-w|PP#vXyM`en+gx;) z5$qSy=ORx+g32YbK57#q?3EglKKg%Xj;hSlm}Ti3oCeSY9-_IyWFh~lRtX?P=umgK zzj}OzHFwlvieO{}50i}zHLy!~sguVS%e@BW-X_T~Z+ZR8p*c;*xxOJ??(^XYzhP~?VV5k<_IRl7spQ%!F%Ds*}WIMKbum3Mxu6^3mqUz z3j4+G3Ec~X!W!a1VMGUF>{4vjnH}q^KiK@ezp-P&WTpWE{+ z&3l)F(g~vCuDCX62DhK)`X_t|=3W;h`4zb~;Ibg`l?5tkglI5sr8$1-k>Yi|??%Ap>(RWx zjE@O4{>~b^g>To3^)T0zc&Nep2ni;~!ioe8;_(gYmHQM4C9vZX>3iG7Tyz%~d>cTD z11J+9Xh!8`(aDG|%ka0q$jYw7-mz z!;6pWZaT|hiWE`RpQfR8zrnVLqqSeKaXxDpm%QbZN%Q_er0$^Q_$ZuBSv&HkfPIw; zZkZQ?-}G{v1U6@8eR)YF@lL}9Ys;tk-W4oizeuJv#AHS&c)~t%U&3@_WXF4U%i;!p z{_}wW9sowrqRz#HunI5GLV0mwzua<_v?55r1i~?F_c}y~P0iMugqZs=)|i_rj8pR1 z;pqn0?bl(cVUPMK$eh)s-3@OB_#(hE8OoJ_J4T_5LZ4RAD(W_}tJ%^1kp1-HKRDL` z*92}dY$4^b5_>r++KyeJ?w}=Ku#gvo%_f}6a5h7I{-;f+Dm=snDl`TP_;hHk9$;ZG zdZ3j{g68!%E~kU1AgpEwHoBl~0hgH9$lC;H5#aLxPC5(T1D$A%@vKL~BE)2Nv|pUG zFGsSSvZHyt;hh33kub5`gvJhjAo5R!UL)W*0Ef{7ODJAxQnK0;Wd)I{FqpxaC6e@} z&|GIVBBO61k;`>L#)V6RmT5_!s5Z?zO5F9aCV3E{rQtYjGGcSf9P^DowZ`5pMrgeB z29)j`@<_?_>ZY&Jkn%jkxBW;T>9BoG^^O5^UO}Zvm9b+irA`W)I=CglI9>0NV~B?W zu-VrVgl15Z;bx4C-1@I61z-*@Y2t~pzw1<8g_|b;-hd5G!CG;9I~&Sj)e?AP2_ZBX zIunRE+^!G(&@iB#_5i!(uV_?Z{FenV2nOikusnl11jXX7`#kA{xahq(IjA3K_Yq+I zCisEI9Jtx=I{?l(+Hg`sYMTv!QfMy{=rPcyt*9p(ZBVs00QL|8r8a0y-O-+>sDCmA zsDFeX9Qay^fI{W?hFOn7WcS~Ed)MFsJ%I}^ojx6K;CsW?a3|4*xToFP@^_8VhT<&Df;sxX%i z9c2#8wX>9Q8eP8WT!80UBJmzP`dBc@#rXLuWt<@c*zZo??CrWJzCWCPDJT`y?KvckPh9i(3Q<9U68)tp zktKo|{IT=QlRvF>POI1EPaoejbTowg;j-UzCub@zc0C=9gq+Vp=8rNj;t$%8gH~zq zIfBnJDu2KnJl_xABKZB@RjsYbaHbiRM#F5{ine_8UhKkw^a=NS_nw)NSo$dbEhbw$ z++OxyH=j>H3-r-ZTR9E_=P#^}ue6+e-@Nth2M}V@+<&0oA9S`DIuR7V_O7Rg$D(oQ ziZSJS&t~wNEb^%n8?*CkiNTwqsdZa7HzG7TCS=tT2T?PG=s(Z>eJQmF8+!FRvtH0Q z#X$G^{jhcZL|zW!wL#S0i#9z55REWTT3;jS{g38+c7c*_RAseF`n*|sLht`OQuz9v zRmKYC(c`PZo=qq4La#>f&ft{{3%pE+T^a-*Ta2A0(aS+F^`k%&gE>a-=K}vO`(=}` zH}oOTzGZRmnqO7*KF5xY7D(AN?W2doTx5Mr(0Jyx(XT3&Cp>Mr!w*XqzzalLZnS%* zc5VE3pr*UrJemF4d(Uh7lhNMi(gd9IzaIj_HpAfc7uWyx0^yfPAJ}l@%leU~Aj{}L z-VSuy-7-1`9t4yP=&4_$^h*~wZh3t%G*)&aDVIQ7Aj69Ymme(%6#?fkNegi3{@2NX z`vEoh^r9!9&Y{D^_Cuo6>C`iNv}I@Tf2wRHN{C{{BOp!J6^k57Rks zD4s7X#bZHAA)?a26k!PGj$SW<4{x=QyQ)?Q7>=UCbulRs^a|Qe;TK zV_T$%EL!2Z?Nt64aunBVea9%f^H0?tN$H2iTmeN^K6)n}4j%QYCn^Kgi!u+hDfBWz zF~e<0cegpGzP2pyo`0AAL&Hz{WNWJX(zZ?<)_>BZXo^6L_kIMswg|G;B-FNd=r{KB zFXg%-jOH%lW4|9(S67Fx$!`Y04U5+^fxdpd8N8<*a7iN|GC(jM!(~?Tp`YL4i9(X7 zwD%G=+6w2hYn@vBey?yS1==kJ9s0KjDS@4uWwj{??qil8y ze8-#HueP2k9zSaY(L4Mk5az6Z3LyeC=p>wfkO&AClYQ1kY<+yFD%O``L;s82x~#Z! z+5bza)ssf`hFOn%8DByKoimbBiJYw$`?AOTg(sO?u5xO<7%b_h){88J zt}}!-Z#7xA{K$3N`h0sd>>0@q&jzB|di~Nba56uA7gT4alM$%J3;8pR6--W_9iMUz zN(bz4juglf%^z$7kj4fU5U>#PCi=4%2m=k|Z9=pPda`*SGiJuUjUhjJ^B2#bEN^7h z3Z=P2(eIrmMM9G=1O=~QR{hy{n4{@`51TcUIz1cmdVXIn`LI(RPsM$U_`Uo=p!~a8 z4~xK`FEp7uv|gmiSvSl^i{gH9rD?Sy%Td1(GDnlVvHMH_Yh(0MJ+; z9v&mWEGf%sK5I^tS3yH4{qW5r$HHTL&zpB+OPaq%DEA)t+j_<4(86kl z9i^$Je8)iXRvB(8KIw9EjLJQXOTX}WQ8@69&+XnbikbJ1c={kX_ll-}noWD0AZl>s zDj3ScVQW-6Jw4r#Bj&94CW?W5_O~Rk(ts+~{QXu$H$M$uNyxme9S+i$PzP16#w{|g zs;({?a&#H<^73q)EL!|U|61^70okPlTM&G1+&gc*N^hL(tA164qG&c}c#m@1d*%BJGbZ%Wc z^sRB6H0!QG1U7#ER_1-usN^TZ(zx>W!PRi3s!XtdOxD0!8!h}-@)GHUHH6yXBe5qI zQ(rOt1|Q|!q|K_=^6O@k)c0MqVRuW^-Xyti{~2#g{@TgnDAD)np5B;_IrcuzA~R9< zWlxOki@ZIq`pD65+`n5k_j9EZOxWeU_q!e&pb$w$l8rxqSl{}+ePWR>U9bQ8LPX~t zL+i$QUTn#zBW+oSz;W<$na?L9nRU5~vY?alle7N1J+Hh(9Gl>$Tg{7&{=4r`%q{IS zQ|a**4xjy5>2_PpnS?F%R&^aL%HkV^LZrMnUI(#K5 zp`Ged+0j)a+K<7!!a5j5{HojscLT9|Z~?J>=$uN-SR6Wl6%J7f|% zYy6kN!8G}gvyVlU>rift(Xc6ghIk`NE7c0^diHD1tOKUiW>*%zZL*r*eNkSw z?|9Uo#;GG>bx0Ym%}y?$(^;kA*&eY{A#dj!df2xWIx<^naEm(V-;atvNr1ek4SOGT zC6Y||>yGew>zH@Al9vg4W*DC8UG{{c%hHlBhNqC0)vd11?C8j4*=-YsDs1MdRX!O7 zN~%X`EgjvvyN_0^FZ``G6wx0Z9~%$vZYag~7iL z{XCrPXgmUu5L%mRdKzZx8~fJbHX(gWZ~LANVI}r+Cbr+LMNqoOAuSkUCL?aQom?Fk zICs*S#deF%{&#(7`-!t2a?d(EBH=%lozc`WKV}&DW{K0sy@^jFeV1~mJHug1`jTum zQSvq9oBZue@ieh@FY?2vs9hz$g^hV<-i(QF*VbE1Om3z|%B59YYwnL9*&+Ws%3M50 zKT$}9cU*$x7=)_!$>k2c3Mi(AgvC#1PjZxTCIqTW;*AypXqTcl*~~Ydm%D1Gh5A@8 zhul4LjydA~p1JFy_*9HYu<1g3wvCFlN@PYWwZC&)-GKy}q&<%dpHscOQU9~EN$Z`E z&f}YviINofSeXx)Td9%*oLO10eje*6S}N9Co#G(Lm1~^!g(GiLiQVLk^e=9W>bw3} zOXw=)!Q`o%6D!@y)lcepOyd3QWKRiuYe5Y4oQr?`28LGjdeS00bEU{Yli2%<6@JWk zd^mmq!Zs=ePsqQ4@*taH$A^hF!(dBJID<)msPFH5-Na8D=&*x=f}ju#1luKrPs$)i zV6#6UkSEzUgxIuMh(U5=PQfCIGX5ibOCZTte@F$dtx)t=3?=4a?)}r97Tsl44 zjlWts@*%T9#5R3#HUXc4NKp&H&mMOBGkLt96lqlgd}0Y+0rf1=RHfZjhbJ z4*t>?(S4n7sQ&Amox@{1_aH=4)L4vaUvu%^q_04~uU!&`*fWDSO1wDp2gMl?lZYXJ&#g=%#Yz)J^K(!t=kcypR9hwP^IZq~Fn})y<9%gi*QC zbmT|}{no~2rEYPazxSWs^~ZR5Mnx|XE;Fzr{Nc)LoDXJ0fmD`qmQWH_wI_-Gu0)K*X7ou;X3ZEdlh|7x^l0;go)MV)xTF zr5d7N-+aO5Q=P>T7jer=8n;G?eG<8`?m{3f{R&U{QO{Z;Rq$b3 zs$a`8%VC}Mi3HmB7^b6t_)ESb!iwI^!tR~#2_Yne0($6&9QbwDl=WQIb4#XcS-|lJ z+n!V~OQM0w4j+*YDj9;l`$Q3j3f0?ru8;h^GJ>U^^bTvyXn~${`)I(KooK7^2eIoy zFl&{k-p_TfUnhr~pZ97c*OzxE^yu5j=;p>p2fA{y9KHKWUx_7*s0fff8_V9)&|W5E z{Dl`>NF612?H)U>@pO0>XzgB|^B3(-yZNPlcDT*=WNdwp6!{|izCPOZy*T5|t8S;` z?#u13J96pVT&;Y%LZ2Y&o1>+MqMwW`ykqc)bNzb~i4NCK^X`P!#S?vs0*$k~9U`$lShq76=2TwI zVtrt>YdvebYT0@%1&c%LTPUd5*8&>-&*h+nL8mPowr!p$Y;0^StOz65U{>0Oo$(@& zHN@t?mzrzxBXq>}_E>OCszqhBZaA`YK-S?mpC-~AZ!|*+z6B9q19uS(V*!hA$H1qnJY1dd*}G?ZgPz0OO*(LwHv5};WlcMd zd#8NcMypDIE8tYXg-+3dVIskz-N0j^K37-tG&&*(;dcXhO`748MF7eKol{J7FqWd9j&|N+c*a zB`Bq2*>BQ6#B}$L-B0Nno7ID?cW#SgyF+8hCtn)AcSMt9qm?Hw<~?L^UVJpAt6@|l z;SI#bOAkxe^ci}PIluUB@h8drxK2U%e|1F-NLueDupt-b{=X7vJZ9msTr4 zr2HHfLWpzcaiZ9nObXTuYMG~`V2SwVP8Vd6V9eH24JwsO0ICHL25h^sdiF}E)}dIB znL+gfMm4-JQ1^IV09*oP(9slq-yBWR1V<&cF*7JhkRXT(+dPRk5W$b=DbYnAbP)j@ zRu@U4HclCXD-Nwh?rZSa`Y(~8#girhMegO#jnula`w zeA}v*7wx94^5cYbW{f4+=)=`dZPlu{(q7lGD!i<;*(bpQ*T(5knQYIM=7}pokgTjr{li5QS|!|i0}q#$8K?*M#C%)x)_&yCl#B|8eEBPFMP_y5m|i@gAb;GubDlc)x_y!^3%GMHG3@` z%XX&OeQ*^yWBwG|PdU+DA~#%Xa}_6bSK*f~c2Ky@-fmap2g`{ll*!*yX=%mmyEPef z{u!E+Nb!pwomP0bvwY_VH>Q!88CQX|m@b|a+@a}nC2=e7!tYZwydxKie^V%#UYJ{R6$_C=smSyA7aeBZ6^3K$zGKs`q3K*| zC!h2i;UX7ctmVm>inrdx)8Yl_>p}DPo@2oJVP^+T2GHr~(Y)K-VJEw7XH#0p#RUxE z)kJ4sA1*vG0@$)oNukY(p+=xpfnUnx3y-g}YGRkek%NVT#x!gU$6s%TzJH$zK6SWF zLEKcO|AgzwjI;jySHk5vy5pETaw&#bBm6SQkXbc^$1}Ezh6NmfG?z zFr|i2%^y!Tcy_Z-K#AdWw759m*DY8Nurc1F{7Ty1H_)#GxDl<$~JCt+Wm~5}CEn)<)W*;R{8> zc+pXdH0F& zVkMPZEP#0Ro1Y~xe&AL5UvCfInGc(xJ%arVSZRTkrbr(MU(~|9(axv;n1KBX`-rF< z#4rGtLpWgICYYE>EdzuqioV*N5Sv7~-Ug8qUk)K=E*IYr1{dkjQ|Vy9ZJZLJcY+gz zHXZ~BfKinNaSOFGqrA>f1DF--F=ne#&$h6n+9}XbYLaow(#qv`le?JMDBx#DHy*#G zi(oi>?AL~+)`6TITu{L}B9sO#xQ-(+!3bxInF*`0DWcMJ6UfSLz_F^jBY{KeypDYvJ5JBMHj(?potrWs47))&Fm@_O@kjdG*NwSai_ z0#RJFtvuK#l8T4{fQ0ATpG#?R3jH%_ovQtmdiO zLwc>hGOrG5uVa5r;y3?b^ov9xpHceWBXLJdzi$WBQ)8SZ?{x1A3w2+&XcYcXEmr#= zp(-aatL^U#|8Azas-3m}kiDkE4aM`z1;>+8VwMUof+3gTf|pdSFRsefG=P``2e)^nZm}0h+%)dYB-g^Fu#&0G%Ul+g)^RBAQnzn<=ax&N%*S z(V?`Vue7YJEIM$5iobi>K*Wgt-&j=jdXG%0<`2}i{E!{Z;M4TN4&<irE;K^3EYZ0y7COjI=uM3;Y*1W*zif0v!SrjgD@@M@O=ehb?4UV9OwaGxTbv zGOIhDw;eG?IJoaFXUX|3nZ}LUa7jLC!t>6Eb0FP}=I3O4lA1>O zAcYkzYNq9VV<%?>#M`+9QIlZ^Iz@WBCaaYgHb_u!y_o@121v5gJD*ee1KQ=?sxsE8 zHXh!wMvBefzl;C1!V7WQPlVDj zsfuiDyiqu8gbGR+D6C$>1i`TK|17XNI_x@@0%9KN8z5S|FER{9^kQ~vKU%PP%;GZO z;xRs*Wc^qXLD1yCP>km_D0q+FtaSN@@pjXjx?ATSv-=-%8T)D=pOmSBX z&D^ZgFOOOjS|Sg4Q+^S^P&4DLoh-f>ckyD$rr&->#O86ryEJBA?B%DQ7uUz=KlKa| z++`rh%ty5LMGrTl9*Cwo;N6<%4*5&CIBWrX+QM{3hxToLnr!QwemxyA1pZ3>`A0cV$w3dq6CI^ zTs7Lx9Eh1G+C{0}@~QL=?ATeV+uC5n(_G3?Nd^{%AfxR5r zE|D=DpHzozRJS5gDID&f-?FbPv;8?1w>nLuQ+KlAj$(DYT?C(a-w@&Tx%b}^ANde+CUu)<`lX?5Wt!?E z+o(W}TuY2zS0w@-6SCJx=q$0Q52L=*t;p$wOLv4~R7k|QJ$3r%;WdOE1VzOYSrPl(mRgb-C}m-QIwiM-dqzg;IK-Bz9P?`j4( ziia?gXXBUgylcFSNgNqpB!T6i%)0+ib^J~#Lc`wS;0$h^9MR6g> z0diw|V=wxD`|q01C6t#7DqZ`}YNP9+hje|iXQfn$mqh}mS3ZYl=B~5JNT$XrHzK$> zGnMmOlbhh}a;4(=d970&^+s*wkE9LX)N5Sy&=L2p-6~XBh}~vRN$_1Qncw9xdtY(& z_Lly@QYiV8i0dzOQwwC>O(h@58L2r^&>CIVdUNSK-Or**uV>P~-h-Pqr1}fYG=S5= zWrB-k<_3)&^qgpQ1uWf>izIgZs`u}6OwK$!JxgZXLC~=XEr@qbjJt@q3N&Wc z6Hhfg2l=qe{odP4>M*`=?s~b?OnWvgHvRcEX@txCY{7*olc1=G{820hB4i|%vO;+UPqqjXw;*LaRB-!}Ljx$@| z-c-BHbD1_wbL^VLKs^S)n`+E4Mau9YvCcYG@w8j9NBFc$xMONjV`g=d0DV;N2E(`l z00ni^#p7}7p~QE>X{TgXL8hV`*b&{NI8rZV7ebrX?)!+SY8X6r;^5!#{ zJ8nn*Y-awsP)`|MKK^t;mpXmc0Bgwap3CUau&K6v2{cIoG7F&02EZF$%+xT6PE9~X zsobobQY|-9EuGO`I;)%YK!*2$a*cFSYJ3cZe>0*=7QWmADzjOOJrLpb&!L!CNJ_4WZ+lqTs zjXz)J0X-gi0gTbzj-i2<=K9lT(E{IYzX)#+4;@OU7;GmdLriixW;d1a+qG}L4KQsS zW(||qeOBt!cHuxL!59?EY|pdq8GmH~gU*F;zP;*_l7_Yf>qT%(N{W)d$yWJ2`IK0; zU>gTHLZ`w}4exgX>Z5PrHa|p$A}gzXNNz_4AVSqFP8RY$c!5IcDlTL zFFwvP|>0rDtG`5UpI}B$-ula2NsAueNs2_8! z)2p1??vrYxK!>Zspp{_&y&XFTfK|i*mbUMix8uB^2JpG*yAb>y!5-!@8&6Jc&R#)^ z`}L{_v5R9Td3OUbCJn0?yR>AaNw*OOawCAod<-GnagAN`cfrmYj(RaX(X`I0ay=5d{x81F+|}*} zA<1YV(P1Zo@LNpjNOs?W|(WtdOQ5N?3&xPN+Z);SR(MnbKEWXY4AtA36BH6dp zUF0|t64VrY`J`eNO;aMSvj5^Zdb4HzwMnNcZRSE=t0sM-#K+UXETeog)T+XMX)qf_b736K^* z-9?4D))6{sAkE)n3zu!u{t?_|PRaIwTmKM>k{AYEbqzak`5oRsd!tNK`?Fk;b5Oe{{I`3cUq z<9eyZ)jTUA)b#I-68$%wR~a2e{B@_ zM4Y5am=)O4L#g)wv~IyWuM0jf;E_Vx`%1xwOs0Hw{P)y*(?GhA5C-XzsI)*uZA7bM zpgmr3CNp^G8NcU6zBI4aWc`+lwo>QPSkkwL@%Vl8aJX>BD`YdE14q$>K47|VvKD#F zHu`UKWEf0;{o18_+M~-^jU7H#K3yY#u39O}DEx9ZyPF;8cpSL6=p$ij9k%L?UKnQU zJ;T%9%r51|O>I0;NDc>0?58+_x{@(>WmSv~rQ0MLykS~4{$pDh({+Nw`v18Eb&T&YGB6cD#fIitA-5#kEgCX7NBEwjEUvs~AJMI^Qs_b0 zOn2az)+(y&izExC7?s;&d6QIEVDm<=CH5ORQArrJz>RIG;p6s^vs7|NF|I|)hz;L& zXifl{G!@vI0`0QHMeF(TI@MtGbVB79*G^aiJe7J#mQsrgi~j#%3+(t~kpG>&(S`DY z(2Aw>!c&K(bMwlnbnPxPH@H5zr}hqK3B%;l241R{iYSRaP=7STw898cE3s@wTfWz* znFp|V@Tnp@ZlHT;^rb2O!WP!i!<0bV!Dz!K(kyLsZ(&8Mg;YZJ$IYkgN&^pz1+eCK z7aDu1z2qxn)67*`tq@&*Ix+6zs~MPb!X3U-mQ)q5fGziw^rK;r5bahyBPTKxVk(gE zy=#1l1yQ806|(qzS>83~W}oy-iV~`~{nd|9Z|&@|`71^~*E&mL3x!iZq|mX(3lNw6 zvS-y$4_$EL*dATF*sk~$HXI<@3q?c1NMf& z+f|p({r(cGVGmzIQZaHk)dUVhH*UGmthJZ7r(kFWm@GQhnJd4lrZWEDoG@y+S$P5ce# zL~irA*NsYJLU{7VR8jNZ7;Dn!%fhGD)4jW6HBw`>VI7319c7WK2fRa92fWES|J{Dx zEK^3R81E`+?GB0#%yrhXIU(29-9s|r6Dia+5rv{S{c0BRSp~_rq|C1x&n$R9p66{n z=0U0r%gC`&{nefL<*50+_A`Nh6d8>-cRoHzy+3szZpw5C3HafX;D1Y7g>LMFEadyVvH)*p>h9XeBGJKjJh52RVj?XnasGmxUz#R+&tSFRkoN;&!)6JM z|5BHH*528NNs<*FGK&7Q?(@SH&gkcVk@;x;j$~1^Be<+AG_PQ}2PF+n+(D~Po*9)E zPy7UCsCrjIIwuyS%Wj1z4*XsJhcXEaVp3dDNdkmfeE}dyy*GEZCL7w&IL>rJda(BG zi-(^`L-dDmKPVf2srI;M+dro^`;gp(ScQxbx4s^iIxOTK4=;j#b9m{j`_`Fw?w}Z5 z{uYH5#nwEu2`0;G-(oZ|%BIQ23=`<^?XIHv`d@T^vBXPZOpDN?6#!IVAPOIc?YcM9 zTmz=aB`q%D*;%hgs}A$I{Z#MDEFBq-g6{mJGshX(>9VhDbkcSp;2_RF zSgG+#M(+C_!p%#VDj72?Hp=l>?6}oN9^iEEi?vPF()avNM8BxjeiP^j(8N^3sI$P? zRc=Sd*JRz9#1p-O3i$|b4CUEe{iFN7C4qsxH+}65#?5Z=${AgAp{66c@}eZm7}~y+ z3>Bb?*`4#!m^sy(34-7ecYarXK5}??Sgn)Je6JVTKD9Gml%BNC*M4qRUIGWzK?uD) zC4VPipCGSVhV(%I!PVc~F|%I%fKulgi;>8%B^NIu38v~piCcun;s!cgLfI4(^Z#kb zCO5&pkA4mYneaQC+58qUVpt;pH97hZ1Y85Z5VK!_G zG-0zz2ryf_9pG>VpDwfY&(5DC5AT$I;k50COd3b%txvH$8y9g~mS`$FVC@0tATa6@ zffA1i6fRU4obAnCtp5@7Wo^C#C;88`kZ?;CG@5q zO4XH}D3*xT)UdXHRsMH{b5wJk3* zm_|8YYDDnHdjlCF2t;g-vSvsleEaQFL$Js~eilx?OozM ziW6|^bdTA`i<4{N@S(7bqRsGHssK^PkCO;tlgcTi8kpv0<)D_Ss;$KutbvAo?B|26 zEXc+2@0Xnm{|En0D4yMxJM6$ZlrpyX7sPbf+LRe&&Jx`Ys?xbXn z62K}aen+@`Zt8`?b>F{Cq=Xm4!B1`Q*fimPP4OTP4T^|(7P~5mjH;J+QYOq^TwvMW z4U2~t^%l>B5f?)`;ppRYC8By%(xNn)y8^U!&dmLSwJVu>Cq}3dw}ZwHn@;+N$vj*8 zEBl@NSKJekvU$z5_~_(?swMPj{q4=DmU>w=N9lj3SIC*gafX|A6Ab0%#c5r)kK!>Y zx+jr%%|AT$8L2)+M7iAF&YNW6Oed0kJdcxoM{-G8wdf5)$nyGz1W445Nm z4(FiT>??aF%82kw9Xw2Za`K;@T*UJzg3Ov+d-hrTZTWBUJ7XMuSyW zT%lL$Rw2h+h>O)}!5lA2zfN z@y#6{D(QNc3CPvwb8sHN+-^6#{INH5RkkingQdKA8dGQ6y?r*7(u=icPsG;Xn_~5; zIr2b;CHT)@$P z+MgXH+DqhBYrX#|6^Cf9MiQWB)qfBHkf8Wz$n`0kE3GQI8G>)VFtE!}QbRYoarm$T zG;NML&i{1w3KYqT{WufVV% z{T`A|e_Q$UZxVV!1ydW8B-Yy-e7;x7L4SbRxp5z{p%RZ4P-Ez*!ki|Qa?XhSZD~_V zYBFSFe`WW5?+XQAQ}$?fLaJ~VEOdKaZp5!Wgfpa0;i7wI-*$TTVC?bUH1mVUqxrYn z`F_PE93U41zom{2m5m=U9|{yksEnB{7Yi%KITxIcsQe20UW=*>J>(n8#dpX=j@Cbn zlBMDJx6V3;?=~0j`Cqj7hn!q1sNcHTe>m8j`FKwvHwdrxhm-p;*F|Bgsv+MKPAV8! z03L%`A21c@bikh0iijrhS&KBMwk@0MX~gH0>IY)PVe zT2Ny^aaB%N=;)0IsW4(?>f6|NcMvAyW-o&O-gj)O1qPxiFC=Muz)7 zm^46dkWUtkG4OjBuPm!F#0)D;`+I2)Z0itZ~Ll|d!eDspBmCAX{AmvxbAZ(|R|t~l#*JrL)}&?ChV z?@MacVC~eUcFCZ6#yzU?sD{Z*{f?37qY$Pim+$GlBvzrmHli&(5<^4~Tx#^_2Z7Dc z?q^0sPOtyeMhWRJmvgrrRbC`i!f{22)5&p=95thvN1h=yw{xtxlGM(@#EzTGO_ce} zRDJ@>;CGM$5+}&*qQav6Fez~|kHF_pg~ld+4_9~gw0n~_c2TfM0Y#sl*%*INSaQMo z0B8C6`7`=nny}|Jv#GJO*CzNLK5E|n>vo^sO?WeZgSC_R^#>A(MXERSA2R~y*kx|1 z>5Y3cE|ZiD{H@(2NqO|cN`I3{>>-}DqOeAhkn5#J6+5KJ2E~QlhHJ|A;LcTCZQQSa zl@cx!Y1VB9aIB81VcT~Rk3P}5;y%bue}DXonX39-qPj_T4m6_kO1E&t9c%soiYmBG zeOC*2nce^DL&nv?XFfeMr}4Yzh0A^*k8x`8>^jCON6IosnI{v8p^6e|()}(;J=*MU z`mi1he1vPG|6S_8TU&cu*eHy2<-%X=stDtTpY4$>&k5bw_Zf}NPgGtkII(wna51qn zF~f^(XtXfwv|syF9a9dN%-QiSyk9FIqVf)4efvcwFcj5L6!~kKK8%o>qH^5wah!yUx;Ab**Jc=a4&Hrit&yM ziNXg$6;*wA+Bsx5@0%pJfLS(G5Yxxh(nQ`azaV-&f45I$&7C=^2gSVXIrL$)Ju5G2 z1%;TCyXuKw=qo-LQL>Yh-?}Gu@x}~Q^a|_FFNrDw8#%UHyh_jJI{wV8d7WIGF)6*L z=);a;$w`_Xe@@{{Jh`Yngf;l)f7K~b(EeQ4NScibH5v8?qX`RJCu1~(r1Ij~JO*Y= z4awJG%g?`ju@{ZF?v!Bhmo�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 &eT2lom&&#r3o53jHbT1x;;P+)`3+WQ0 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, content: , icon: ico, onDismount() { backend.log(backend.LogLevel.Debug, "PowerTools shutting down"); - clearInterval(periodicHook!); - periodicHook = null; - lifetimeHook?.unregister(); - startHook?.unregister(); - endHook?.unregister(); + clearHooks(); //serverApi.routerHook.removeRoute("/decky-plugin-test"); - backend.log(backend.LogLevel.Debug, "Unregistered PowerTools callbacks, so long and thanks for all the fish."); }, }; }); From 9e1f7c06204ece22913aac2bf76e33f8d47efb1a Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 17 Dec 2023 17:12:34 -0500 Subject: [PATCH 19/56] Fix game callbacks not activating correctly, fix UI not updating correctly when new config loaded --- src/index.tsx | 51 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 1ba2b15..e65bfbe 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -77,6 +77,8 @@ var startHook: any = null; var endHook: any = null; var usdplReady = false; +var tryNotifyProfileChange = function() {}; + type MinMax = { min: number | null; max: number | null; @@ -170,7 +172,7 @@ const clearHooks = function() { startHook?.unregister(); endHook?.unregister(); - backend.log(backend.LogLevel.Debug, "Unregistered PowerTools callbacks, so long and thanks for all the fish."); + backend.log(backend.LogLevel.Info, "Unregistered PowerTools callbacks, so long and thanks for all the fish."); }; const registerCallbacks = function(autoclear: boolean) { @@ -185,9 +187,14 @@ const registerCallbacks = function(autoclear: boolean) { //backend.log(backend.LogLevel.Debug, "AppID " + update.unAppID.toString() + " is now running"); } else { //backend.log(backend.LogLevel.Debug, "AppID " + update.unAppID.toString() + " is no longer running"); - backend.resolve( - backend.loadGeneralDefaultSettings(), - (ok: boolean) => {backend.log(backend.LogLevel.Debug, "Loading default settings ok? " + ok)} + backend.resolve(backend.loadGeneralDefaultSettings(), (ok: boolean) => { + backend.log(backend.LogLevel.Debug, "Loading default settings ok? " + ok); + reload(); + backend.resolve(backend.waitForComplete(), (_) => { + backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to game exit"); + tryNotifyProfileChange(); + }); + } ); } }); @@ -200,12 +207,26 @@ const registerCallbacks = function(autoclear: boolean) { // don't use gameInfo.appid, haha backend.resolve( backend.loadGeneralSettings(id.toString(), gameInfo.display_name, 0, undefined), - (ok: boolean) => {backend.log(backend.LogLevel.Debug, "Loading settings ok? " + ok)} + (ok: boolean) => { + backend.log(backend.LogLevel.Debug, "Loading settings ok? " + ok); + reload(); + backend.resolve(backend.waitForComplete(), (_) => { + backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new game launch"); + tryNotifyProfileChange(); + }); + } ); }); + // this fires immediately, so let's ignore that callback + let hasFiredImmediately = false; //@ts-ignore endHook = SteamClient.Apps.RegisterForGameActionEnd((actionType) => { + if (!hasFiredImmediately) { + hasFiredImmediately = true; + backend.log(backend.LogLevel.Debug, "RegisterForGameActionEnd immediately fired callback(" + actionType + ")"); + return; + } backend.log(backend.LogLevel.Info, "RegisterForGameActionEnd callback(" + actionType + ")"); setTimeout(() => backend.forceApplySettings(), AUTOMATIC_REAPPLY_WAIT); }); @@ -233,16 +254,13 @@ const periodicals = function() { const oldValue = get_value(PATH_GEN); set_value(PATH_GEN, path); if (path != oldValue) { - backend.log(backend.LogLevel.Info, "Frontend values reload triggered by path change: " + oldValue + " -> " + path); + backend.log(backend.LogLevel.Debug, "Frontend values reload triggered by path change: " + oldValue + " -> " + path); reload(); } }) }; -const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { - - const [idc, reloadGUI] = useState("/shrug"); - +const periodicalsSetup = function(reloadGUI: (s: string) => void) { if (periodicHook != null) { clearInterval(periodicHook); periodicHook = null; @@ -252,6 +270,14 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { periodicals(); reloadGUI("periodic" + (new Date()).getTime().toString()); }, PERIODICAL_BACKEND_PERIOD); +}; + +const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { + + const [idc, reloadGUI] = useState("/shrug"); + tryNotifyProfileChange = function() { reloadGUI("ProfileChangeByNotification") }; + + periodicalsSetup(reloadGUI); if (!usdplReady || !get_value(LIMITS_INFO)) { // Not translated on purpose (to avoid USDPL issues) @@ -263,6 +289,8 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { onClick={(_: MouseEvent) => { console.log("POWERTOOLS: manual reload after startup failure"); reload(); + // try to reload GUI too + backend.resolve(backend.waitForComplete(), (_) => {reloadGUI("LoadSystemDefaults")}); }} > Reload @@ -351,7 +379,7 @@ export default definePlugin((serverApi: ServerAPI) => { if (now.getDate() == 1 && now.getMonth() == 3) { ico = ; } - registerCallbacks(false); + //registerCallbacks(false); return { title:
PowerTools
, content: , @@ -359,6 +387,7 @@ export default definePlugin((serverApi: ServerAPI) => { onDismount() { backend.log(backend.LogLevel.Debug, "PowerTools shutting down"); clearHooks(); + tryNotifyProfileChange = function() {}; //serverApi.routerHook.removeRoute("/decky-plugin-test"); }, }; From 508c6ceb9e95f41a8684536d99d0c511bf73e37f Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Fri, 22 Dec 2023 16:26:50 -0500 Subject: [PATCH 20/56] Community settings WIP --- backend/community_settings_core/Cargo.lock | 65 + backend/community_settings_core/Cargo.toml | 9 + backend/community_settings_core/src/lib.rs | 1 + .../src/v1/metadata.rs | 22 + backend/community_settings_core/src/v1/mod.rs | 5 + .../community_settings_core/src/v1/setting.rs | 47 + backend/community_settings_srv/Cargo.lock | 1538 +++++++++++++++++ backend/community_settings_srv/Cargo.toml | 21 + backend/community_settings_srv/README.md | 7 + .../src/api/get_setting.rs | 120 ++ backend/community_settings_srv/src/api/mod.rs | 10 + .../src/api/save_setting.rs | 68 + backend/community_settings_srv/src/cli.rs | 23 + .../community_settings_srv/src/file_util.rs | 34 + backend/community_settings_srv/src/main.rs | 40 + 15 files changed, 2010 insertions(+) create mode 100644 backend/community_settings_core/Cargo.lock create mode 100644 backend/community_settings_core/Cargo.toml create mode 100644 backend/community_settings_core/src/lib.rs create mode 100644 backend/community_settings_core/src/v1/metadata.rs create mode 100644 backend/community_settings_core/src/v1/mod.rs create mode 100644 backend/community_settings_core/src/v1/setting.rs create mode 100644 backend/community_settings_srv/Cargo.lock create mode 100644 backend/community_settings_srv/Cargo.toml create mode 100644 backend/community_settings_srv/README.md create mode 100644 backend/community_settings_srv/src/api/get_setting.rs create mode 100644 backend/community_settings_srv/src/api/mod.rs create mode 100644 backend/community_settings_srv/src/api/save_setting.rs create mode 100644 backend/community_settings_srv/src/cli.rs create mode 100644 backend/community_settings_srv/src/file_util.rs create mode 100644 backend/community_settings_srv/src/main.rs diff --git a/backend/community_settings_core/Cargo.lock b/backend/community_settings_core/Cargo.lock new file mode 100644 index 0000000..4873981 --- /dev/null +++ b/backend/community_settings_core/Cargo.lock @@ -0,0 +1,65 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "community_settings_core" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/backend/community_settings_core/Cargo.toml b/backend/community_settings_core/Cargo.toml new file mode 100644 index 0000000..d4f6632 --- /dev/null +++ b/backend/community_settings_core/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "community_settings_core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } diff --git a/backend/community_settings_core/src/lib.rs b/backend/community_settings_core/src/lib.rs new file mode 100644 index 0000000..a3a6d96 --- /dev/null +++ b/backend/community_settings_core/src/lib.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/backend/community_settings_core/src/v1/metadata.rs b/backend/community_settings_core/src/v1/metadata.rs new file mode 100644 index 0000000..0208658 --- /dev/null +++ b/backend/community_settings_core/src/v1/metadata.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Metadata { + pub name: String, + pub steam_app_id: u32, + pub steam_user_id: u64, + pub stream_username: String, + /// Should always be a valid u128, but some parsers do not support that + pub id: String, + pub config: super::Config, +} + +impl Metadata { + pub fn set_id(&mut self, id: u128) { + self.id = id.to_string() + } + + pub fn get_id(&self) -> u128 { + self.id.parse().expect("metadata id must be u128") + } +} diff --git a/backend/community_settings_core/src/v1/mod.rs b/backend/community_settings_core/src/v1/mod.rs new file mode 100644 index 0000000..7519798 --- /dev/null +++ b/backend/community_settings_core/src/v1/mod.rs @@ -0,0 +1,5 @@ +mod metadata; +mod setting; + +pub use metadata::Metadata; +pub use setting::*; diff --git a/backend/community_settings_core/src/v1/setting.rs b/backend/community_settings_core/src/v1/setting.rs new file mode 100644 index 0000000..259a7b5 --- /dev/null +++ b/backend/community_settings_core/src/v1/setting.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; + +/// Base setting file containing all information for all components +#[derive(Serialize, Deserialize, Clone)] +pub struct Config { + pub cpus: Vec, + pub gpu: Gpu, + pub battery: Battery, +} + +#[derive(Serialize, Deserialize, Clone, Copy)] +pub struct MinMax { + pub max: Option, + pub min: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Battery { + pub charge_rate: Option, + pub charge_mode: Option, + #[serde(default)] + pub events: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct BatteryEvent { + pub trigger: String, + pub charge_rate: Option, + pub charge_mode: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Cpu { + pub online: bool, + pub clock_limits: Option>, + pub governor: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Gpu { + pub fast_ppt: Option, + pub slow_ppt: Option, + pub tdp: Option, + pub tdp_boost: Option, + pub clock_limits: Option>, + pub slow_memory: bool, +} diff --git a/backend/community_settings_srv/Cargo.lock b/backend/community_settings_srv/Cargo.lock new file mode 100644 index 0000000..1cf7fd3 --- /dev/null +++ b/backend/community_settings_srv/Cargo.lock @@ -0,0 +1,1538 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags 2.4.1", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.41", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "anstream" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "community_settings_core" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "community_settings_srv" +version = "0.1.0" +dependencies = [ + "actix-web", + "clap", + "community_settings_core", + "log", + "mime", + "ron", + "serde", + "serde_json", + "simplelog", + "tokio", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64", + "bitflags 2.4.1", + "serde", + "serde_derive", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simplelog" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "zerocopy" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/backend/community_settings_srv/Cargo.toml b/backend/community_settings_srv/Cargo.toml new file mode 100644 index 0000000..37cb95c --- /dev/null +++ b/backend/community_settings_srv/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "community_settings_srv" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +community_settings_core = { version = "0.1", path = "../community_settings_core" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +ron = "0.8" + +clap = { version = "4", features = ["derive", "std", "color"], default-features = false } +tokio = { version = "1", features = ["full"] } +actix-web = { version = "4.4" } +mime = { version = "0.3.17" } + +# logging +log = "0.4" +simplelog = "0.12" diff --git a/backend/community_settings_srv/README.md b/backend/community_settings_srv/README.md new file mode 100644 index 0000000..c02999d --- /dev/null +++ b/backend/community_settings_srv/README.md @@ -0,0 +1,7 @@ +# community_settings_srv + +Back-end for browsing community-contributed settings files for games. + +### Technical + +This does not use a database because I'm trying to speedrun the destruction of any semblance of performance for my filesystem. Everything is stored as a file, with symlinks to make it possible to find files multiple ways. diff --git a/backend/community_settings_srv/src/api/get_setting.rs b/backend/community_settings_srv/src/api/get_setting.rs new file mode 100644 index 0000000..9ee986d --- /dev/null +++ b/backend/community_settings_srv/src/api/get_setting.rs @@ -0,0 +1,120 @@ +use actix_web::{get, web, Responder, http::header}; + +use crate::cli::Cli; +use crate::file_util; + +fn special_settings() -> community_settings_core::v1::Metadata { + community_settings_core::v1::Metadata { + name: "Zeroth the Least".to_owned(), + steam_app_id: 1675200, + steam_user_id: 76561198116690523, + stream_username: "NGnius".to_owned(), + id: 0.to_string(), + config: community_settings_core::v1::Config { + cpus: vec![ + community_settings_core::v1::Cpu { + online: true, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + governor: "Michaëlle Jean".to_owned(), + }, + community_settings_core::v1::Cpu { + online: false, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + governor: "Adrienne Clarkson".to_owned(), + }, + community_settings_core::v1::Cpu { + online: true, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + governor: "Michael Collins".to_owned(), + } + ], + gpu: community_settings_core::v1::Gpu { + fast_ppt: Some(1), + slow_ppt: Some(1), + tdp: None, + tdp_boost: None, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + slow_memory: false, + }, + battery: community_settings_core::v1::Battery { + charge_rate: Some(42), + charge_mode: Some("nuclear fusion".to_owned()), + events: vec![ + community_settings_core::v1::BatteryEvent { + trigger: "anything but one on a gun".to_owned(), + charge_rate: Some(42), + charge_mode: Some("neutral".to_owned()), + } + ], + } + } + } +} + +#[get("/api/setting/{id}")] +pub async fn get_setting_handler( + id: web::Path, + accept: web::Header, + cli: web::Data<&'static Cli>, +) -> std::io::Result { + println!("Accept: {}", accept.to_string()); + let id: u128 = match id.parse() { + Ok(x) => x, + Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("invalid setting id `{}` (should be u128): {}", id, e))), + }; + let preferred = accept.preference(); + if super::is_mime_type_ron_capable(&preferred) { + // Send RON + let ron = if id != 0 { + let path = file_util::setting_path_by_id(&cli.folder, id, file_util::RON_EXTENSION); + if !path.exists() { + return Err(std::io::Error::new(std::io::ErrorKind::NotFound, format!("setting id {} does not exist", id))); + } + // TODO? cache this instead of always loading it from file + let reader = std::io::BufReader::new(std::fs::File::open(path)?); + match ron::de::from_reader(reader) { + Ok(x) => x, + Err(e) => { + let e_msg = format!("{}", e); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); + } + } + } else { + special_settings() + }; + // TODO don't dump to string + let result_body = ron::ser::to_string(&ron).unwrap(); + Ok(actix_web::HttpResponse::Ok() + //.insert_header(header::ContentType("application/ron".parse().unwrap())) + .insert_header(header::ContentType(mime::STAR_STAR)) + .body(actix_web::body::BoxBody::new(result_body)) + ) + } else { + // Send JSON (fallback) + let json = if id != 0 { + let path = file_util::setting_path_by_id(&cli.folder, id, file_util::JSON_EXTENSION); + // TODO? cache this instead of always loading it from file + let reader = std::io::BufReader::new(std::fs::File::open(path)?); + match serde_json::from_reader(reader) { + Ok(x) => x, + Err(e) => { + let e_msg = format!("{}", e); + if let Some(io_e) = e.io_error_kind() { + return Err(std::io::Error::new(io_e, e_msg)); + } else { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); + } + + } + } + } else { + special_settings() + }; + // TODO don't dump to string + let result_body = serde_json::to_string(&json).unwrap(); + Ok(actix_web::HttpResponse::Ok() + .insert_header(header::ContentType::json()) + .body(actix_web::body::BoxBody::new(result_body)) + ) + } +} diff --git a/backend/community_settings_srv/src/api/mod.rs b/backend/community_settings_srv/src/api/mod.rs new file mode 100644 index 0000000..7581e25 --- /dev/null +++ b/backend/community_settings_srv/src/api/mod.rs @@ -0,0 +1,10 @@ +mod get_setting; +mod save_setting; + +pub use get_setting::get_setting_handler as get_setting_by_id; +pub use save_setting::save_setting_handler as save_setting_with_new_id; + +pub(self) fn is_mime_type_ron_capable(mimetype: &mime::Mime) -> bool { + (mimetype.type_() == "application" || mimetype.type_() == mime::STAR) + && (mimetype.subtype() == "ron" || mimetype.subtype() == "cc.replicated.ron" || mimetype.subtype() == "w-ron") +} diff --git a/backend/community_settings_srv/src/api/save_setting.rs b/backend/community_settings_srv/src/api/save_setting.rs new file mode 100644 index 0000000..5e84d8e --- /dev/null +++ b/backend/community_settings_srv/src/api/save_setting.rs @@ -0,0 +1,68 @@ +use actix_web::{post, web, Responder, http::header}; + +use crate::cli::Cli; +use crate::file_util; + +const PAYLOAD_LIMIT: usize = 10_000_000; // 10 Megabyte + +#[post("/api/setting")] +pub async fn save_setting_handler( + data: web::Payload, + content_type: web::Header, + cli: web::Data<&'static Cli>, +) -> std::io::Result { + println!("Content-Type: {}", content_type.to_string()); + let bytes = match data.to_bytes_limited(PAYLOAD_LIMIT).await { + Ok(Ok(x)) => x, + Ok(Err(e)) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("wut: {}", e))), + Err(_e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "too many bytes in payload")), + }; + let next_id = file_util::next_setting_id(&cli.folder); + let parsed_data = if super::is_mime_type_ron_capable(&content_type) { + // Parse as RON + match ron::de::from_reader(bytes.as_ref()) { + Ok(x) => x, + Err(e) => { + let e_msg = format!("{}", e); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); + } + } + } else { + // Parse JSON (fallback) + match serde_json::from_reader(bytes.as_ref()) { + Ok(x) => x, + Err(e) => { + let e_msg = format!("{}", e); + if let Some(io_e) = e.io_error_kind() { + return Err(std::io::Error::new(io_e, e_msg)); + } else { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); + } + + } + } + }; + // TODO validate user and app id + // Reject blocked users and apps + let path = file_util::setting_path_by_id(&cli.folder, next_id, file_util::RON_EXTENSION); + let writer = std::io::BufWriter::new(std::fs::File::create(path)?); + if let Err(e) = ron::ser::to_writer(writer, &parsed_data) { + let e_msg = format!("{}", e); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); + } + + let path = file_util::setting_path_by_id(&cli.folder, next_id, file_util::JSON_EXTENSION); + let writer = std::io::BufWriter::new(std::fs::File::create(path)?); + if let Err(e) = serde_json::to_writer(writer, &parsed_data) { + let e_msg = format!("{}", e); + if let Some(io_e) = e.io_error_kind() { + return Err(std::io::Error::new(io_e, e_msg)); + } else { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); + } + } + + // TODO create symlinks for other ways of looking up these settings files + + Ok(actix_web::HttpResponse::NoContent()) +} diff --git a/backend/community_settings_srv/src/cli.rs b/backend/community_settings_srv/src/cli.rs new file mode 100644 index 0000000..b787355 --- /dev/null +++ b/backend/community_settings_srv/src/cli.rs @@ -0,0 +1,23 @@ +use clap::Parser; + +#[derive(Parser, Debug, Clone)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + /// Root folder to store contributed setting files + #[arg(short, long, default_value = "./community_settings")] + pub folder: std::path::PathBuf, + + /// Server port + #[arg(short, long, default_value_t = 8080)] + pub port: u16, + + /// Log file location + #[arg(short, long, default_value = "/tmp/powertools_community_settings_srv.log")] + pub log: std::path::PathBuf, +} + +impl Cli { + pub fn get() -> Self { + Self::parse() + } +} diff --git a/backend/community_settings_srv/src/file_util.rs b/backend/community_settings_srv/src/file_util.rs new file mode 100644 index 0000000..467e304 --- /dev/null +++ b/backend/community_settings_srv/src/file_util.rs @@ -0,0 +1,34 @@ +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +pub const RON_EXTENSION: &'static str = "ron"; +pub const JSON_EXTENSION: &'static str = "json"; + +const SETTING_FOLDER: &'static str = "settings"; +const ID_FOLDER: &'static str = "by_id"; + +static LAST_SETTING_ID: Mutex = Mutex::new(0); + +pub fn setting_path_by_id(root: impl AsRef, id: u128, ext: &str) -> PathBuf { + root.as_ref() + .join(SETTING_FOLDER) + .join(ID_FOLDER) + .join(format!("{}.{}", id, ext)) +} + +pub fn next_setting_id(root: impl AsRef) -> u128 { + let mut lock = LAST_SETTING_ID.lock().unwrap(); + let mut last_id = *lock; + if last_id == 0 { + // needs init + let mut path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION); + while path.exists() { + last_id += 1; + path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION); + } + *lock = last_id; + println!("setting id initialized to {}", last_id); + } + *lock += 1; + *lock +} diff --git a/backend/community_settings_srv/src/main.rs b/backend/community_settings_srv/src/main.rs new file mode 100644 index 0000000..70ba717 --- /dev/null +++ b/backend/community_settings_srv/src/main.rs @@ -0,0 +1,40 @@ +mod api; +mod cli; +mod file_util; + +use actix_web::{web, App, HttpServer}; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let args = cli::Cli::get(); + println!("cli: {:?}", args); + + simplelog::WriteLogger::init( + #[cfg(debug_assertions)] + { + log::LevelFilter::Debug + }, + #[cfg(not(debug_assertions))] + { + log::LevelFilter::Info + }, + Default::default(), + std::fs::File::create(&args.log).expect("Failed to create log file"), + //std::fs::File::create("/home/deck/powertools-rs.log").unwrap(), + ) + .unwrap(); + log::debug!("Logging to: {}", args.log.display()); + + let leaked_args: &'static cli::Cli = Box::leak::<'static>(Box::new(args)); + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(leaked_args)) + //.app_data(web::Data::new(IndexPage::load("dist/index.html").unwrap())) + //.app_data(basic::Config::default().realm("Restricted area")) + .service(api::get_setting_by_id) + .service(api::save_setting_with_new_id) + }) + .bind(("0.0.0.0", leaked_args.port))? + .run() + .await +} From 437f5beb71cb9041a2b360c4b57a0a5515299f83 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Fri, 29 Dec 2023 00:34:02 -0500 Subject: [PATCH 21/56] Refactor SD LED functionality even more; move to different project and expand scope --- backend/Cargo.lock | 729 +++++++++++---------- backend/Cargo.toml | 4 +- backend/sd_led/Cargo.lock | 16 - backend/sd_led/Cargo.toml | 10 - backend/sd_led/src/lib.rs | 193 ------ backend/sd_led/src/raw_io.rs | 51 -- backend/src/settings/steam_deck/battery.rs | 8 +- backend/src/settings/steam_deck/util.rs | 15 +- main.py | 4 +- 9 files changed, 409 insertions(+), 621 deletions(-) delete mode 100644 backend/sd_led/Cargo.lock delete mode 100644 backend/sd_led/Cargo.toml delete mode 100644 backend/sd_led/src/lib.rs delete mode 100644 backend/sd_led/src/raw_io.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 408422c..719de2a 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -94,24 +94,24 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn", ] [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn", ] [[package]] @@ -122,9 +122,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -143,38 +143,33 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "bindgen" -version = "0.64.0" +version = "0.66.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" dependencies = [ - "bitflags 1.3.2", + "bitflags", "cexpr", "clang-sys", "lazy_static", "lazycell", "log", "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 1.0.109", + "syn", "which", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.4.1" @@ -195,9 +190,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.3.4" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -205,33 +200,33 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytecount" -version = "0.6.3" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] @@ -253,17 +248,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.48.5", ] [[package]] @@ -297,15 +291,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -339,10 +333,19 @@ dependencies = [ ] [[package]] -name = "deranged" -version = "0.3.7" +name = "data-encoding" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", +] [[package]] name = "digest" @@ -426,9 +429,9 @@ checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -441,30 +444,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.2.8" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -478,18 +470,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -497,27 +489,27 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", "futures-sink", @@ -539,13 +531,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -560,9 +552,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -572,9 +564,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -582,7 +574,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap", "slab", "tokio", "tokio-util", @@ -591,24 +583,17 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "headers" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", + "base64 0.21.5", "bytes", "headers-core", "http", @@ -628,9 +613,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -639,10 +624,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "http" -version = "0.2.9" +name = "home" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -651,9 +645,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -668,9 +662,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" @@ -689,7 +683,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -698,16 +692,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -721,9 +715,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -731,35 +725,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -778,9 +762,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libloading" @@ -794,9 +778,7 @@ dependencies = [ [[package]] name = "libryzenadj" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a805571abbf0729e3641b825b734948ce84ccbb54b9f08afae0b860bb12af971" +version = "0.14.0" dependencies = [ "errno", "libryzenadj-sys", @@ -806,9 +788,7 @@ dependencies = [ [[package]] name = "libryzenadj-sys" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b72f39d32e246ce0db7b4dd2430ddb5bb4dfa27681bce696a8746ac88892de1" +version = "0.14.0" dependencies = [ "bindgen", "cmake", @@ -823,16 +803,22 @@ dependencies = [ ] [[package]] -name = "log" -version = "0.4.19" +name = "linux-raw-sys" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "mime" @@ -867,13 +853,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "wasi", + "windows-sys 0.48.0", ] [[package]] @@ -906,9 +892,9 @@ dependencies = [ [[package]] name = "nom_locate" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e299bf5ea7b212e811e71174c5d1a5d065c4c0ad0c8691ecb1f97e3e66025e" +checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" dependencies = [ "bytecount", "memchr", @@ -917,9 +903,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -936,23 +922,23 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.11" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.11" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -972,18 +958,18 @@ checksum = "7b2b2cbbfd8defa51ff24450a61d73b3ff3e158484ddd274a883e886e6fbaa78" [[package]] name = "object" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -1010,9 +996,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" @@ -1031,14 +1017,14 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn", ] [[package]] name = "pin-project-lite" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1058,6 +1044,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "powertools" version = "1.5.0-ng1" @@ -1068,10 +1060,10 @@ dependencies = [ "log", "regex", "ron", - "sd_led", "serde", "serde_json", "simplelog", + "smokepatio", "sysfuss", "tokio", "ureq", @@ -1084,6 +1076,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1096,18 +1098,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1144,9 +1146,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.3" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", @@ -1156,9 +1158,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", @@ -1167,9 +1169,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ron" @@ -1177,8 +1179,8 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64 0.21.2", - "bitflags 2.4.1", + "base64 0.21.5", + "bitflags", "serde", "serde_derive", ] @@ -1196,19 +1198,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "rustls-pemfile" -version = "1.0.3" +name = "rustix" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ - "base64 0.21.2", + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.5", ] [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "scoped-tls" @@ -1216,38 +1231,31 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" -[[package]] -name = "sd_led" -version = "0.1.0" -dependencies = [ - "log", -] - [[package]] name = "serde" -version = "1.0.183" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn", ] [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -1268,9 +1276,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -1279,9 +1287,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" [[package]] name = "simplelog" @@ -1291,28 +1299,45 @@ checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" dependencies = [ "log", "termcolor", - "time 0.3.25", + "time", ] [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] +[[package]] +name = "smokepatio" +version = "0.1.0" +dependencies = [ + "log", +] + [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -1327,20 +1352,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e" dependencies = [ "proc-macro2", "quote", @@ -1364,45 +1378,35 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn", ] [[package]] name = "time" -version = "0.1.45" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", "libc", "num_threads", + "powerfmt", "serde", "time-core", "time-macros", @@ -1410,15 +1414,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -1440,19 +1444,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", - "socket2", - "windows-sys", + "socket2 0.5.5", + "windows-sys 0.48.0", ] [[package]] @@ -1468,9 +1471,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", @@ -1480,9 +1483,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -1494,17 +1497,17 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.0", + "indexmap", "toml_datetime", "winnow", ] @@ -1517,11 +1520,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-core", @@ -1529,28 +1531,28 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ - "base64 0.13.1", "byteorder", "bytes", + "data-encoding", "http", "httparse", "log", @@ -1563,30 +1565,30 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -1609,11 +1611,11 @@ dependencies = [ [[package]] name = "ureq" -version = "2.7.1" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", "brotli-decompressor", "encoding_rs", "flate2", @@ -1626,9 +1628,9 @@ dependencies = [ [[package]] name = "url" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -1686,9 +1688,9 @@ dependencies = [ [[package]] name = "warp" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba431ef570df1287f7f8b07e376491ad54f84d26ac473489427231e1718e1f69" +checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" dependencies = [ "bytes", "futures-channel", @@ -1715,12 +1717,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1729,9 +1725,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1739,24 +1735,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1764,32 +1760,33 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix", ] [[package]] @@ -1810,9 +1807,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -1824,12 +1821,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1838,71 +1835,137 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.4" +version = "0.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acaaa1190073b2b101e15083c38ee8ec891b5e05cbee516521e94ec008f61e64" +checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff" dependencies = [ "memchr", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7b00fd3..660416e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -29,8 +29,8 @@ simplelog = "0.12" # limits & driver functionality limits_core = { version = "3", path = "./limits_core" } regex = "1" -libryzenadj = { version = "0.12" } -sd_led = { version = "*", path = "./sd_led" } +smokepatio = { version = "*", path = "../../smokepatio" } +libryzenadj = { version = "0.14", path = "../../libryzenadj-rs-14" } # ureq's tls feature does not like musl targets ureq = { version = "2", features = ["json", "gzip", "brotli", "charset"], default-features = false, optional = true } diff --git a/backend/sd_led/Cargo.lock b/backend/sd_led/Cargo.lock deleted file mode 100644 index 0a12fba..0000000 --- a/backend/sd_led/Cargo.lock +++ /dev/null @@ -1,16 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "sd_led" -version = "0.1.0" -dependencies = [ - "log", -] diff --git a/backend/sd_led/Cargo.toml b/backend/sd_led/Cargo.toml deleted file mode 100644 index 0ee424a..0000000 --- a/backend/sd_led/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "sd_led" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# logging -log = "0.4" diff --git a/backend/sd_led/src/lib.rs b/backend/sd_led/src/lib.rs deleted file mode 100644 index aae4209..0000000 --- a/backend/sd_led/src/lib.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Rough Rust port of some BatCtrl functionality -//! Original: /usr/share/jupiter_controller_fw_updater/RA_bootloader_updater/linux_host_tools/BatCtrl -//! I do not have access to the source code, so this is my own interpretation of what it does. -//! -//! But also Quanta is based in a place with some questionable copyright practices, so... -pub mod raw_io; - -use std::io::Error; - -pub fn set_led(red_unused: bool, green_aka_white: bool, blue_unused: bool) -> Result { - let payload: u8 = 0x80 - | (red_unused as u8 & 1) - | ((green_aka_white as u8 & 1) << 1) - | ((blue_unused as u8 & 1) << 2); - //log::info!("Payload: {:b}", payload); - raw_io::write2(Setting::LEDStatus as _, payload) -} - -pub fn set(setting: Setting, mode: u8) -> Result { - raw_io::write2(setting as u8, mode) -} - -#[derive(Copy, Clone)] -#[repr(u8)] -pub enum Setting { - CycleCount = 0x32, - ControlBoard = 0x6C, - Charge = 0xA6, - ChargeMode = 0x76, - LEDStatus = 199, - LEDBreathing = 0x63, - FanSpeed = 0x2c, // lower 3 bits seem to not do everything, every other bit increases speed -- 5 total steps, 0xf4 seems to do something similar too - // 0x40 write 0x08 makes LED red + green turn on - // 0x58 write 0x80 shuts off battery power (bms?) - // 0x63 makes blue (0x02) or white (0x01) LED breathing effect - // 0x7a write 0x01, 0x02, or 0x03 turns off display -} - -#[derive(Copy, Clone, Debug)] -#[repr(u8)] -pub enum ControlBoard { - Enable = 0xAA, - Disable = 0xAB, -} - -#[derive(Copy, Clone, Debug)] -#[repr(u8)] -pub enum ChargeMode { - Normal = 0, - Discharge = 0x42, - Idle = 0x45, -} - -#[derive(Copy, Clone)] -#[repr(u8)] -pub enum Charge { - Enable = 0, - Disable = 4, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[allow(dead_code)] - fn led_all_experiment_test() -> Result<(), Error> { - let original = raw_io::write_read(Setting::LEDStatus as _)?; - let sleep_dur = std::time::Duration::from_millis(1000); - for b in 0..0x7F { - let actual = 0x80 | b; - raw_io::write2(Setting::LEDStatus as _, actual)?; - println!("Wrote {actual:#b} to LED byte"); - std::thread::sleep(sleep_dur); - } - raw_io::write2(Setting::LEDStatus as _, original)?; - Ok(()) - } - - #[test] - #[allow(dead_code)] - fn led_singles_experiment_test() -> Result<(), Error> { - let original = raw_io::write_read(Setting::LEDStatus as _)?; - let sleep_dur = std::time::Duration::from_millis(1000); - let mut value = 1; - for _ in 0..std::mem::size_of::()*8 { - let actual = 0x80 | value; - raw_io::write2(Setting::LEDStatus as _, actual)?; - println!("Wrote {actual:#b} to LED byte"); - value = value << 1; - std::thread::sleep(sleep_dur); - } - raw_io::write2(Setting::LEDStatus as _, original)?; - Ok(()) - } - - #[test] - #[allow(dead_code)] - fn led_specify_experiment_test() -> Result<(), Error> { - let mut buffer = String::new(); - println!("LED number(s) to display?"); - std::io::stdin().read_line(&mut buffer)?; - - let mut resultant = 0; - let original = raw_io::write_read(Setting::LEDStatus as _)?; - for word in buffer.split(' ') { - let trimmed_word = word.trim(); - if !trimmed_word.is_empty() { - let value: u8 = trimmed_word.parse().expect("Invalid u8 number"); - let actual = 0x80 | value; - raw_io::wait_ready_for_write()?; - raw_io::write2(Setting::LEDStatus as _, actual)?; - println!("Wrote {actual:#b} to LED byte"); - resultant |= actual; - } - } - println!("Effectively wrote {resultant:#b} to LED byte"); - - println!("Press enter to return to normal"); - std::io::stdin().read_line(&mut buffer)?; - raw_io::write2(Setting::LEDStatus as _, original)?; - Ok(()) - } - - #[test] - #[allow(dead_code)] - fn breath_specify_experiment_test() -> Result<(), Error> { - let mut buffer = String::new(); - println!("LED number(s) to display?"); - std::io::stdin().read_line(&mut buffer)?; - - for word in buffer.split(' ') { - let trimmed_word = word.trim(); - if !trimmed_word.is_empty() { - let value: u8 = trimmed_word.parse().expect("Invalid u8 number"); - let actual = 0x20 | value; - raw_io::wait_ready_for_write()?; - raw_io::write2(0x63, actual)?; - println!("Wrote {actual:#b} to LED breathing byte"); - } - } - - println!("Press enter to return to normal"); - std::io::stdin().read_line(&mut buffer)?; - raw_io::write2(0x63, 0)?; - Ok(()) - } - - #[test] - #[allow(dead_code)] - fn unmapped_ports_experiment_test() -> Result<(), Error> { - let sleep_dur = std::time::Duration::from_millis(10000); - let value = 0xaa; - for addr in 0x63..0x64 { - //raw_io::wait_ready_for_read()?; - //let read = raw_io::write_read(addr)?; - raw_io::wait_ready_for_write()?; - raw_io::write2(addr, value)?; - println!("wrote {value:#b} for {addr:#x} port"); - std::thread::sleep(sleep_dur); - } - //raw_io::write2(Setting::LEDStatus as _, 0)?; - Ok(()) - } - - #[test] - #[allow(dead_code)] - fn write_specify_experiment_test() -> Result<(), Error> { - let mut buffer = String::new(); - println!("Register?"); - std::io::stdin().read_line(&mut buffer)?; - let register: u8 = buffer.trim().parse().expect("Invalid u8 number"); - buffer.clear(); - - println!("Value(s)?"); - std::io::stdin().read_line(&mut buffer)?; - - for word in buffer.split(' ') { - let trimmed_word = word.trim(); - if !trimmed_word.is_empty() { - let value: u8 = trimmed_word.parse().expect("Invalid u8 number"); - raw_io::wait_ready_for_write()?; - raw_io::write2(register, value)?; - println!("Wrote {value:#09b} to {register:#02x} register"); - } - } - - println!("Press enter to clear register"); - std::io::stdin().read_line(&mut buffer)?; - raw_io::write2(register, 0)?; - Ok(()) - } -} diff --git a/backend/sd_led/src/raw_io.rs b/backend/sd_led/src/raw_io.rs deleted file mode 100644 index 07d6568..0000000 --- a/backend/sd_led/src/raw_io.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::fs::OpenOptions; -use std::io::{Error, Read, Seek, SeekFrom, Write}; - -#[inline] -pub fn write2(p0: u8, p1: u8) -> Result { - write_to(0x6c, 0x81)?; - wait_ready_for_write()?; - let count0 = write_to(0x68, p0)?; - wait_ready_for_write()?; - let count1 = write_to(0x68, p1)?; - Ok(count0 + count1) -} - -#[inline] -pub fn write_read(p0: u8) -> Result { - write_to(0x6c, 0x81)?; - wait_ready_for_write()?; - write_to(0x68, p0)?; - wait_ready_for_read()?; - read_from(0x68) -} - -pub fn write_to(location: u64, value: u8) -> Result { - let mut file = OpenOptions::new().write(true).open("/dev/port")?; - file.seek(SeekFrom::Start(location))?; - file.write(&[value]) -} - -pub fn read_from(location: u64) -> Result { - let mut file = OpenOptions::new().read(true).open("/dev/port")?; - file.seek(SeekFrom::Start(location))?; - let mut buffer = [0]; - file.read(&mut buffer)?; - Ok(buffer[0]) -} - -pub fn wait_ready_for_write() -> Result<(), Error> { - let mut count = 0; - while count < 0x1ffff && (read_from(0x6c)? & 2) != 0 { - count += 1; - } - Ok(()) -} - -pub fn wait_ready_for_read() -> Result<(), Error> { - let mut count = 0; - while count < 0x1ffff && (read_from(0x6c)? & 1) == 0 { - count += 1; - } - Ok(()) -} diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index db152be..1dea06c 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -6,7 +6,7 @@ use sysfuss::capability::attributes; use limits_core::json_v2::GenericBatteryLimit; -use sd_led::ChargeMode; +use smokepatio::ec::ChargeMode; use crate::api::RangeLimit; use crate::persist::{BatteryEventJson, BatteryJson}; use crate::settings::{TBattery, ProviderBuilder}; @@ -131,7 +131,7 @@ impl EventInstruction { fn set_charge_mode(&self) -> Result<(), SettingError> { if let Some(charge_mode) = self.charge_mode { - sd_led::set(sd_led::Setting::ChargeMode, charge_mode as _) + smokepatio::ec::set(smokepatio::ec::Setting::ChargeMode, charge_mode as _) .map_err(|e| SettingError { msg: format!("Failed to set charge mode: {}", e), setting: crate::settings::SettingVariant::Battery, @@ -329,7 +329,7 @@ impl Battery { fn set_charge_mode(&mut self) -> Result<(), SettingError> { if let Some(charge_mode) = self.charge_mode { self.state.charge_mode_set = true; - sd_led::set(sd_led::Setting::ChargeMode, charge_mode as _) + smokepatio::ec::set(smokepatio::ec::Setting::ChargeMode, charge_mode as _) .map_err(|e| SettingError { msg: format!("Failed to set charge mode: {}", e), setting: crate::settings::SettingVariant::Battery, @@ -337,7 +337,7 @@ impl Battery { .map(|_| ()) } else if self.state.charge_mode_set { self.state.charge_mode_set = false; - sd_led::set(sd_led::Setting::ChargeMode, ChargeMode::Normal as _) + smokepatio::ec::set(smokepatio::ec::Setting::ChargeMode, ChargeMode::Normal as _) .map_err(|e| SettingError { msg: format!("Failed to set charge mode: {}", e), setting: crate::settings::SettingVariant::Battery, diff --git a/backend/src/settings/steam_deck/util.rs b/backend/src/settings/steam_deck/util.rs index 4362bb2..c57c70a 100644 --- a/backend/src/settings/steam_deck/util.rs +++ b/backend/src/settings/steam_deck/util.rs @@ -27,20 +27,15 @@ const THINGS: &[u8] = &[ const TIME_UNIT: std::time::Duration = std::time::Duration::from_millis(200); pub fn flash_led() { - let led_status = sd_led::Setting::LEDStatus; - let old_led_state = sd_led::raw_io::write_read(led_status as _) - .map_err(|e| log::error!("Failed to read LED status: {}", e)); for &code in THINGS { let on = code != 0; - if let Err(e) = sd_led::set_led(on, on, false) { + if let Err(e) = smokepatio::ec::led::constant::set(if on { smokepatio::ec::led::constant::Colour::White } else { smokepatio::ec::led::constant::Colour::Off }) { log::error!("Thing err: {}", e); } std::thread::sleep(TIME_UNIT); } - if let Ok(old_led_state) = old_led_state { - log::debug!("Restoring LED state to {:#02b}", old_led_state); - sd_led::raw_io::write2(led_status as _, old_led_state) - .map_err(|e| log::error!("Failed to restore LED status: {}", e)) - .unwrap(); - } + log::debug!("Restoring LED state"); + smokepatio::ec::led::constant::set(smokepatio::ec::led::constant::Colour::Off) + .map_err(|e| log::error!("Failed to restore LED status: {}", e)) + .unwrap(); } diff --git a/main.py b/main.py index 9f7916b..018ac69 100644 --- a/main.py +++ b/main.py @@ -17,9 +17,9 @@ class Plugin: env_proc["LD_LIBRARY_PATH"] += ":"+PARENT_DIR+"/bin" else: env_proc["LD_LIBRARY_PATH"] = ":"+PARENT_DIR+"/bin" - self.backend_proc = subprocess.Popen( + '''self.backend_proc = subprocess.Popen( [PARENT_DIR + "/bin/backend"], - env = env_proc) + env = env_proc)''' while True: await asyncio.sleep(1) From 4eaf6fae2bba0219a316d37499ee6e1549645862 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 6 Jan 2024 13:26:35 -0500 Subject: [PATCH 22/56] Add all back-end functionality for interacting with variants on the front-end --- backend/Cargo.lock | 16 +- backend/Cargo.toml | 4 +- .../src/v1/metadata.rs | 3 +- .../src/api/get_game.rs | 121 +++++++++++++++ .../src/api/get_setting.rs | 5 +- backend/community_settings_srv/src/api/mod.rs | 2 + .../src/api/save_setting.rs | 64 +++++++- .../community_settings_srv/src/file_util.rs | 54 ++++++- backend/community_settings_srv/src/main.rs | 5 + backend/src/api/api_types.rs | 6 + backend/src/api/general.rs | 58 +++++++ backend/src/api/handler.rs | 14 +- backend/src/api/mod.rs | 1 + backend/src/api/web.rs | 142 ++++++++++++++++++ backend/src/main.rs | 18 ++- backend/src/persist/file.rs | 17 ++- backend/src/persist/mod.rs | 2 + backend/src/settings/general.rs | 42 +++++- backend/src/settings/traits.rs | 12 +- src/backend.ts | 31 ++++ 20 files changed, 590 insertions(+), 27 deletions(-) create mode 100644 backend/community_settings_srv/src/api/get_game.rs create mode 100644 backend/src/api/web.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 719de2a..0daaacc 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -289,6 +289,13 @@ dependencies = [ "cc", ] +[[package]] +name = "community_settings_core" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -778,7 +785,9 @@ dependencies = [ [[package]] name = "libryzenadj" -version = "0.14.0" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5bccdf07c3234c06c435648a53d8cb369f76d20e03bb8d2f8c24fb2330efc32" dependencies = [ "errno", "libryzenadj-sys", @@ -788,7 +797,9 @@ dependencies = [ [[package]] name = "libryzenadj-sys" -version = "0.14.0" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de3621be974e892e12d4a07a6a2e32b6a05950759b062d94f5b54f78fabc3a" dependencies = [ "bindgen", "cmake", @@ -1055,6 +1066,7 @@ name = "powertools" version = "1.5.0-ng1" dependencies = [ "async-trait", + "community_settings_core", "libryzenadj", "limits_core", "log", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 660416e..15a72f7 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -28,9 +28,11 @@ simplelog = "0.12" # limits & driver functionality limits_core = { version = "3", path = "./limits_core" } +community_settings_core = { version = "0.1", path = "./community_settings_core" } regex = "1" smokepatio = { version = "*", path = "../../smokepatio" } -libryzenadj = { version = "0.14", path = "../../libryzenadj-rs-14" } +#libryzenadj = { version = "0.14", path = "../../libryzenadj-rs-14" } +libryzenadj = { version = "0.13" } # ureq's tls feature does not like musl targets ureq = { version = "2", features = ["json", "gzip", "brotli", "charset"], default-features = false, optional = true } diff --git a/backend/community_settings_core/src/v1/metadata.rs b/backend/community_settings_core/src/v1/metadata.rs index 0208658..009baa6 100644 --- a/backend/community_settings_core/src/v1/metadata.rs +++ b/backend/community_settings_core/src/v1/metadata.rs @@ -5,7 +5,8 @@ pub struct Metadata { pub name: String, pub steam_app_id: u32, pub steam_user_id: u64, - pub stream_username: String, + pub steam_username: String, + pub tags: Vec, /// Should always be a valid u128, but some parsers do not support that pub id: String, pub config: super::Config, diff --git a/backend/community_settings_srv/src/api/get_game.rs b/backend/community_settings_srv/src/api/get_game.rs new file mode 100644 index 0000000..c7d1966 --- /dev/null +++ b/backend/community_settings_srv/src/api/get_game.rs @@ -0,0 +1,121 @@ +use actix_web::{get, web, Responder, http::header}; + +use crate::cli::Cli; +use crate::file_util; + +const MAX_RESULTS: usize = 50; + +fn special_settings() -> Vec { + vec![ + community_settings_core::v1::Metadata { + name: "Zeroth the Least".to_owned(), + steam_app_id: 0, + steam_user_id: 76561198116690523, + steam_username: "NGnius".to_owned(), + tags: vec!["0".to_owned(), "gr8".to_owned()], + id: 0.to_string(), + config: community_settings_core::v1::Config { + cpus: vec![ + community_settings_core::v1::Cpu { + online: true, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + governor: "Michaëlle Jean".to_owned(), + }, + community_settings_core::v1::Cpu { + online: false, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + governor: "Adrienne Clarkson".to_owned(), + }, + community_settings_core::v1::Cpu { + online: true, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + governor: "Michael Collins".to_owned(), + } + ], + gpu: community_settings_core::v1::Gpu { + fast_ppt: Some(1), + slow_ppt: Some(1), + tdp: None, + tdp_boost: None, + clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), + slow_memory: false, + }, + battery: community_settings_core::v1::Battery { + charge_rate: Some(42), + charge_mode: Some("nuclear fusion".to_owned()), + events: vec![ + community_settings_core::v1::BatteryEvent { + trigger: "anything but one on a gun".to_owned(), + charge_rate: Some(42), + charge_mode: Some("neutral".to_owned()), + } + ], + } + } + } + ] +} + +fn get_some_settings_by_app_id(steam_app_id: u32, cli: &'static Cli) -> std::io::Result> { + let app_id_folder = file_util::setting_folder_by_app_id(&cli.folder, steam_app_id); + let mut files: Vec<_> = app_id_folder.read_dir()? + .filter_map(|res| res.ok()) + .filter(|f| f.path().extension().map(|ext| ext == file_util::RON_EXTENSION).unwrap_or(false)) + .filter_map(|f| f.metadata().ok().map(|meta| (f, meta))) + .filter_map(|(f, meta)| meta.created().ok().map(|time| (f, meta, time))) + .collect(); + files.sort_by(|(_, _, a_created), (_, _, b_created)| a_created.cmp(b_created)); + + let mut results = Vec::with_capacity(MAX_RESULTS); + for (_, (f, _, _)) in files.into_iter().enumerate().take_while(|(i, _)| *i < MAX_RESULTS) { + let reader = std::io::BufReader::new(std::fs::File::open(f.path())?); + let setting = match ron::de::from_reader(reader) { + Ok(x) => x, + Err(e) => { + let e_msg = format!("{}", e); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); + } + }; + results.push(setting); + } + Ok(results) +} + +#[get("/api/setting/by_app_id/{id}")] +pub async fn get_setting_by_app_id_handler( + id: web::Path, + accept: web::Header, + cli: web::Data<&'static Cli>, +) -> std::io::Result { + let id: u32 = *id; + println!("Accept: {}", accept.to_string()); + let preferred = accept.preference(); + if super::is_mime_type_ron_capable(&preferred) { + // Send RON + let ron = if id != 0 { + get_some_settings_by_app_id(id, &*cli)? + } else { + special_settings() + }; + // TODO don't dump to string + let result_body = ron::ser::to_string(&ron).unwrap(); + Ok(actix_web::HttpResponse::Ok() + //.insert_header(header::ContentType("application/ron".parse().unwrap())) + .insert_header(header::ContentType(mime::STAR_STAR)) + .body(actix_web::body::BoxBody::new(result_body)) + ) + } else { + // Send JSON (fallback) + let json = if id != 0 { + get_some_settings_by_app_id(id, &*cli)? + } else { + special_settings() + }; + // TODO don't dump to string + let result_body = serde_json::to_string(&json).unwrap(); + Ok(actix_web::HttpResponse::Ok() + .insert_header(header::ContentType::json()) + .body(actix_web::body::BoxBody::new(result_body)) + ) + } +} diff --git a/backend/community_settings_srv/src/api/get_setting.rs b/backend/community_settings_srv/src/api/get_setting.rs index 9ee986d..47f9cc4 100644 --- a/backend/community_settings_srv/src/api/get_setting.rs +++ b/backend/community_settings_srv/src/api/get_setting.rs @@ -8,7 +8,8 @@ fn special_settings() -> community_settings_core::v1::Metadata { name: "Zeroth the Least".to_owned(), steam_app_id: 1675200, steam_user_id: 76561198116690523, - stream_username: "NGnius".to_owned(), + steam_username: "NGnius".to_owned(), + tags: vec!["0".to_owned(), "gr8".to_owned()], id: 0.to_string(), config: community_settings_core::v1::Config { cpus: vec![ @@ -51,7 +52,7 @@ fn special_settings() -> community_settings_core::v1::Metadata { } } -#[get("/api/setting/{id}")] +#[get("/api/setting/by_id/{id}")] pub async fn get_setting_handler( id: web::Path, accept: web::Header, diff --git a/backend/community_settings_srv/src/api/mod.rs b/backend/community_settings_srv/src/api/mod.rs index 7581e25..ca947e3 100644 --- a/backend/community_settings_srv/src/api/mod.rs +++ b/backend/community_settings_srv/src/api/mod.rs @@ -1,6 +1,8 @@ +mod get_game; mod get_setting; mod save_setting; +pub use get_game::get_setting_by_app_id_handler as get_setting_by_steam_app_id; pub use get_setting::get_setting_handler as get_setting_by_id; pub use save_setting::save_setting_handler as save_setting_with_new_id; diff --git a/backend/community_settings_srv/src/api/save_setting.rs b/backend/community_settings_srv/src/api/save_setting.rs index 5e84d8e..39d610e 100644 --- a/backend/community_settings_srv/src/api/save_setting.rs +++ b/backend/community_settings_srv/src/api/save_setting.rs @@ -18,7 +18,7 @@ pub async fn save_setting_handler( Err(_e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "too many bytes in payload")), }; let next_id = file_util::next_setting_id(&cli.folder); - let parsed_data = if super::is_mime_type_ron_capable(&content_type) { + let parsed_data: community_settings_core::v1::Metadata = if super::is_mime_type_ron_capable(&content_type) { // Parse as RON match ron::de::from_reader(bytes.as_ref()) { Ok(x) => x, @@ -44,15 +44,15 @@ pub async fn save_setting_handler( }; // TODO validate user and app id // Reject blocked users and apps - let path = file_util::setting_path_by_id(&cli.folder, next_id, file_util::RON_EXTENSION); - let writer = std::io::BufWriter::new(std::fs::File::create(path)?); + let path_ron = file_util::setting_path_by_id(&cli.folder, next_id, file_util::RON_EXTENSION); + let writer = std::io::BufWriter::new(std::fs::File::create(&path_ron)?); if let Err(e) = ron::ser::to_writer(writer, &parsed_data) { let e_msg = format!("{}", e); return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); } - let path = file_util::setting_path_by_id(&cli.folder, next_id, file_util::JSON_EXTENSION); - let writer = std::io::BufWriter::new(std::fs::File::create(path)?); + let path_json = file_util::setting_path_by_id(&cli.folder, next_id, file_util::JSON_EXTENSION); + let writer = std::io::BufWriter::new(std::fs::File::create(&path_json)?); if let Err(e) = serde_json::to_writer(writer, &parsed_data) { let e_msg = format!("{}", e); if let Some(io_e) = e.io_error_kind() { @@ -62,7 +62,59 @@ pub async fn save_setting_handler( } } - // TODO create symlinks for other ways of looking up these settings files + // create symlinks for other ways of looking up these settings files + let filename_ron = file_util::filename(next_id, file_util::RON_EXTENSION); + let filename_json = file_util::filename(next_id, file_util::JSON_EXTENSION); + + // create symlinks to app id folder + let app_id_folder = file_util::setting_folder_by_app_id(&cli.folder, parsed_data.steam_app_id); + if !app_id_folder.exists() { + std::fs::create_dir(&app_id_folder)?; + } + #[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained + { + std::os::windows::fs::symlink_file(&path_ron, app_id_folder.join(&filename_ron))?; + std::os::windows::fs::symlink_file(&path_json, app_id_folder.join(&filename_json))?; + } + #[cfg(target_family = "unix")] + { + std::os::unix::fs::symlink(&path_ron, app_id_folder.join(&filename_ron))?; + std::os::unix::fs::symlink(&path_json, app_id_folder.join(&filename_json))?; + } + + // create symlinks for user id folder + let user_id_folder = file_util::setting_folder_by_user_id(&cli.folder, parsed_data.steam_user_id); + if !user_id_folder.exists() { + std::fs::create_dir(&user_id_folder)?; + } + #[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained + { + std::os::windows::fs::symlink_file(&path_ron, user_id_folder.join(&filename_ron))?; + std::os::windows::fs::symlink_file(&path_json, user_id_folder.join(&filename_json))?; + } + #[cfg(target_family = "unix")] + { + std::os::unix::fs::symlink(&path_ron, user_id_folder.join(&filename_ron))?; + std::os::unix::fs::symlink(&path_json, user_id_folder.join(&filename_json))?; + } + + // create symlinks for each tag + for tag in parsed_data.tags.iter() { + let tag_folder = file_util::setting_folder_by_tag(&cli.folder, tag); + if !tag_folder.exists() { + std::fs::create_dir(&tag_folder)?; + } + #[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained + { + std::os::windows::fs::symlink_file(&path_ron, tag_folder.join(&filename_ron))?; + std::os::windows::fs::symlink_file(&path_json, tag_folder.join(&filename_json))?; + } + #[cfg(target_family = "unix")] + { + std::os::unix::fs::symlink(&path_ron, tag_folder.join(&filename_ron))?; + std::os::unix::fs::symlink(&path_json, tag_folder.join(&filename_json))?; + } + } Ok(actix_web::HttpResponse::NoContent()) } diff --git a/backend/community_settings_srv/src/file_util.rs b/backend/community_settings_srv/src/file_util.rs index 467e304..670c608 100644 --- a/backend/community_settings_srv/src/file_util.rs +++ b/backend/community_settings_srv/src/file_util.rs @@ -6,14 +6,66 @@ pub const JSON_EXTENSION: &'static str = "json"; const SETTING_FOLDER: &'static str = "settings"; const ID_FOLDER: &'static str = "by_id"; +const APP_ID_FOLDER: &'static str = "by_app_id"; +const USER_ID_FOLDER: &'static str = "by_user_id"; +const TAG_FOLDER: &'static str = "by_tag"; static LAST_SETTING_ID: Mutex = Mutex::new(0); +pub fn build_folder_layout(root: impl AsRef) -> std::io::Result<()> { + std::fs::create_dir_all( + root.as_ref() + .join(SETTING_FOLDER) + .join(ID_FOLDER) + )?; + std::fs::create_dir_all( + root.as_ref() + .join(SETTING_FOLDER) + .join(APP_ID_FOLDER) + )?; + std::fs::create_dir_all( + root.as_ref() + .join(SETTING_FOLDER) + .join(USER_ID_FOLDER) + )?; + std::fs::create_dir_all( + root.as_ref() + .join(SETTING_FOLDER) + .join(TAG_FOLDER) + )?; + Ok(()) +} + +pub fn filename(id: u128, ext: &str) -> String { + format!("{}.{}", id, ext) +} + pub fn setting_path_by_id(root: impl AsRef, id: u128, ext: &str) -> PathBuf { root.as_ref() .join(SETTING_FOLDER) .join(ID_FOLDER) - .join(format!("{}.{}", id, ext)) + .join(filename(id, ext)) +} + +pub fn setting_folder_by_app_id(root: impl AsRef, steam_app_id: u32) -> PathBuf { + root.as_ref() + .join(SETTING_FOLDER) + .join(APP_ID_FOLDER) + .join(steam_app_id.to_string()) +} + +pub fn setting_folder_by_user_id(root: impl AsRef, steam_user_id: u64) -> PathBuf { + root.as_ref() + .join(SETTING_FOLDER) + .join(USER_ID_FOLDER) + .join(steam_user_id.to_string()) +} + +pub fn setting_folder_by_tag(root: impl AsRef, tag: &str) -> PathBuf { + root.as_ref() + .join(SETTING_FOLDER) + .join(TAG_FOLDER) + .join(tag) } pub fn next_setting_id(root: impl AsRef) -> u128 { diff --git a/backend/community_settings_srv/src/main.rs b/backend/community_settings_srv/src/main.rs index 70ba717..2753bfa 100644 --- a/backend/community_settings_srv/src/main.rs +++ b/backend/community_settings_srv/src/main.rs @@ -25,6 +25,10 @@ async fn main() -> std::io::Result<()> { .unwrap(); log::debug!("Logging to: {}", args.log.display()); + // setup + log::debug!("Building folder layout (if not exists) at: {}", &args.folder.display()); + file_util::build_folder_layout(&args.folder)?; + let leaked_args: &'static cli::Cli = Box::leak::<'static>(Box::new(args)); HttpServer::new(move || { App::new() @@ -32,6 +36,7 @@ async fn main() -> std::io::Result<()> { //.app_data(web::Data::new(IndexPage::load("dist/index.html").unwrap())) //.app_data(basic::Config::default().realm("Restricted area")) .service(api::get_setting_by_id) + .service(api::get_setting_by_steam_app_id) .service(api::save_setting_with_new_id) }) .bind(("0.0.0.0", leaked_args.port))? diff --git a/backend/src/api/api_types.rs b/backend/src/api/api_types.rs index 8d72107..cf889d3 100644 --- a/backend/src/api/api_types.rs +++ b/backend/src/api/api_types.rs @@ -61,3 +61,9 @@ pub struct GpuLimits { pub clock_step: u64, pub memory_control_capable: bool, } + +#[derive(Serialize, Deserialize)] +pub struct VariantInfo { + pub id: String, + pub name: String, +} diff --git a/backend/src/api/general.rs b/backend/src/api/general.rs index 73cb706..a534d22 100644 --- a/backend/src/api/general.rs +++ b/backend/src/api/general.rs @@ -391,3 +391,61 @@ fn wait_for_response(sender: &Sender, rx: mpsc::Receiver, api_ sender.send(api_msg).expect(&format!("{} send failed", op)); rx.recv().expect(&format!("{} callback recv failed", op)) } + +/// Generate get variants +pub fn get_all_variants(sender: Sender) -> impl AsyncCallable { + let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety + let getter = move || { + let sender2 = sender.clone(); + move || { + let (tx, rx) = mpsc::channel(); + let callback = + move |variants: Vec| tx.send(variants).expect("get_all_variants callback send failed"); + sender2 + .lock() + .unwrap() + .send(ApiMessage::General(GeneralMessage::GetAllVariants( + Box::new(callback), + ))) + .expect("get_all_variants send failed"); + rx.recv().expect("get_all_variants callback recv failed") + } + }; + super::async_utils::AsyncIshGetter { + set_get: getter, + trans_getter: |result| { + let mut output = Vec::with_capacity(result.len()); + for status in result.iter() { + output.push(Primitive::Json(serde_json::to_string(status).expect("Failed to serialize variant info to JSON"))); + } + output + }, + } +} + +/// Generate get current variant +pub fn get_current_variant(sender: Sender) -> impl AsyncCallable { + let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety + let getter = move || { + let sender2 = sender.clone(); + move || { + let (tx, rx) = mpsc::channel(); + let callback = + move |variant: super::VariantInfo| tx.send(variant).expect("get_all_variants callback send failed"); + sender2 + .lock() + .unwrap() + .send(ApiMessage::General(GeneralMessage::GetCurrentVariant( + Box::new(callback), + ))) + .expect("get_current_variant send failed"); + rx.recv().expect("get_current_variant callback recv failed") + } + }; + super::async_utils::AsyncIshGetter { + set_get: getter, + trans_getter: |result| { + vec![Primitive::Json(serde_json::to_string(&result).expect("Failed to serialize variant info to JSON"))] + }, + } +} diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index 69234c6..a00507e 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -227,6 +227,9 @@ pub enum GeneralMessage { GetPersistent(Callback), GetCurrentProfileName(Callback), GetPath(Callback), + GetCurrentVariant(Callback), + GetAllVariants(Callback>), + AddVariant(crate::persist::SettingsJson, Callback>), ApplyNow, } @@ -238,6 +241,15 @@ impl GeneralMessage { Self::GetPersistent(cb) => cb(*settings.persistent()), Self::GetCurrentProfileName(cb) => cb(settings.get_name().to_owned()), Self::GetPath(cb) => cb(settings.get_path().to_owned()), + Self::GetCurrentVariant(cb) => cb(settings.get_variant_info()), + Self::GetAllVariants(cb) => cb(settings.get_variants()), + Self::AddVariant(variant, cb) => match settings.add_variant(variant) { + Ok(variants) => cb(variants), + Err(e) => { + print_errors("GeneralMessage::AddVariant => TGeneral::add_variant", vec![e]); + cb(Vec::with_capacity(0)) + }, + }, Self::ApplyNow => {} } dirty @@ -389,7 +401,7 @@ impl ApiMessageHandler { true } ApiMessage::LoadSystemSettings => { - settings.load_system_default(settings.general.get_name().to_owned(), settings.general.get_variant_id(), settings.general.get_variant_name().to_owned()); + settings.load_system_default(settings.general.get_name().to_owned(), settings.general.get_variant_id(), settings.general.get_variant_info().name); true } ApiMessage::GetLimits(cb) => { diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs index 8dc8add..488faf3 100644 --- a/backend/src/api/mod.rs +++ b/backend/src/api/mod.rs @@ -7,6 +7,7 @@ pub mod gpu; pub mod handler; pub mod message; mod utility; +pub mod web; pub(super) type ApiParameterType = Vec; diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs new file mode 100644 index 0000000..a7b5f0d --- /dev/null +++ b/backend/src/api/web.rs @@ -0,0 +1,142 @@ +use std::sync::mpsc::{self, Sender}; +use std::sync::{Arc, Mutex}; +use usdpl_back::core::serdes::Primitive; +use usdpl_back::AsyncCallable; + +use super::handler::{ApiMessage, GeneralMessage}; + +const BASE_URL: &'static str = "http://powertools.ngni.us"; + +/// Get search results web method +pub fn search_by_app_id() -> impl AsyncCallable { + let getter = move || { + move |steam_app_id: u32| { + let req_url = format!("{}/api/setting/by_app_id/{}", BASE_URL, steam_app_id); + match ureq::get(&req_url).call() { + Ok(response) => { + let json_res: std::io::Result> = response.into_json(); + match json_res { + Ok(search_results) => { + // search results may be quite large, so let's do the JSON string conversion in the background (blocking) thread + match serde_json::to_string(&search_results) { + Err(e) => log::error!("Cannot convert search results from `{}` to JSON: {}", req_url, e), + Ok(s) => return s, + } + } + Err(e) => { + log::error!("Cannot parse response from `{}`: {}", req_url, e) + } + } + } + Err(e) => log::warn!("Cannot get search results from `{}`: {}", req_url, e), + } + "[]".to_owned() + } + }; + super::async_utils::AsyncIsh { + trans_setter: |params| { + if let Some(Primitive::F64(app_id)) = params.get(0) { + Ok(*app_id as u32) + } else { + Err("search_by_app_id missing/invalid parameter 0".to_owned()) + } + }, + set_get: getter, + trans_getter: |result| vec![Primitive::Json(result)], + } +} + +fn web_config_to_settings_json(meta: community_settings_core::v1::Metadata) -> crate::persist::SettingsJson { + crate::persist::SettingsJson { + version: crate::persist::LATEST_VERSION, + name: meta.name, + variant: u64::MAX, // TODO maybe change this to use the 64 low bits of id (u64::MAX will cause it to generate a new id when added to file variant map + persistent: true, + cpus: meta.config.cpus.into_iter().map(|cpu| crate::persist::CpuJson { + online: cpu.online, + clock_limits: cpu.clock_limits.map(|lim| crate::persist::MinMaxJson { + min: lim.min, + max: lim.max, + }), + governor: cpu.governor, + root: None, + }).collect(), + gpu: crate::persist::GpuJson { + fast_ppt: meta.config.gpu.fast_ppt, + slow_ppt: meta.config.gpu.slow_ppt, + tdp: meta.config.gpu.tdp, + tdp_boost: meta.config.gpu.tdp_boost, + clock_limits: meta.config.gpu.clock_limits.map(|lim| crate::persist::MinMaxJson { + min: lim.min, + max: lim.max, + }), + slow_memory: meta.config.gpu.slow_memory, + root: None, + }, + battery: crate::persist::BatteryJson { + charge_rate: meta.config.battery.charge_rate, + charge_mode: meta.config.battery.charge_mode, + events: meta.config.battery.events.into_iter().map(|be| crate::persist::BatteryEventJson { + charge_rate: be.charge_rate, + charge_mode: be.charge_mode, + trigger: be.trigger, + }).collect(), + root: None, + }, + provider: Some(crate::persist::DriverJson::AutoDetect), + } +} + +/// Download config web method +pub fn download_new_config(sender: Sender) -> impl AsyncCallable { + let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety + let getter = move || { + let sender2 = sender.clone(); + move |id: u128| { + let req_url = format!("{}/api/setting/by_id/{}", BASE_URL, id); + match ureq::get(&req_url).call() { + Ok(response) => { + let json_res: std::io::Result = response.into_json(); + match json_res { + Ok(meta) => { + let (tx, rx) = mpsc::channel(); + let callback = + move |values: Vec| tx.send(values).expect("download_new_config callback send failed"); + sender2 + .lock() + .unwrap() + .send(ApiMessage::General(GeneralMessage::AddVariant(web_config_to_settings_json(meta), Box::new(callback)))) + .expect("download_new_config send failed"); + return rx.recv().expect("download_new_config callback recv failed"); + } + Err(e) => { + log::error!("Cannot parse response from `{}`: {}", req_url, e) + } + } + } + Err(e) => log::warn!("Cannot get setting result from `{}`: {}", req_url, e), + } + vec![] + } + }; + super::async_utils::AsyncIsh { + trans_setter: |params| { + if let Some(Primitive::String(id)) = params.get(0) { + match id.parse::() { + Ok(id) => Ok(id), + Err(e) => Err(format!("download_new_config non-u128 string parameter 0: {} (got `{}`)", e, id)) + } + } else { + Err("download_new_config missing/invalid parameter 0".to_owned()) + } + }, + set_get: getter, + trans_getter: |result| { + let mut output = Vec::with_capacity(result.len()); + for status in result.iter() { + output.push(Primitive::Json(serde_json::to_string(status).expect("Failed to serialize variant info to JSON"))); + } + output + }, + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index a07b4d4..e62a4a6 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -299,8 +299,24 @@ fn main() -> Result<(), ()> { "GENERAL_get_periodicals", api::general::get_periodicals(api_sender.clone()) ) + .register_async( + "GENERAL_get_all_variants", + api::general::get_all_variants(api_sender.clone()) + ) + .register_async( + "GENERAL_get_current_variant", + api::general::get_current_variant(api_sender.clone()) + ) .register_async("MESSAGE_get", message_getter) - .register_async("MESSAGE_dismiss", message_dismisser); + .register_async("MESSAGE_dismiss", message_dismisser) + .register_async( + "WEB_search_by_app", + api::web::search_by_app_id() + ) + .register_async( + "WEB_download_new", + api::web::download_new_config(api_sender.clone()) + ); if let Err(e) = loaded_settings.on_set() { e.iter() diff --git a/backend/src/persist/file.rs b/backend/src/persist/file.rs index 2750996..b6cdee2 100644 --- a/backend/src/persist/file.rs +++ b/backend/src/persist/file.rs @@ -37,14 +37,24 @@ impl FileJson { ron::de::from_reader(&mut file).map_err(|e| SerdeError::Serde(e.into())) } - pub fn update_variant_or_create>(path: P, setting: SettingsJson, given_name: String) -> Result<(), SerdeError> { + fn next_available_id(&self) -> u64 { + self.variants.keys() + .max() + .map(|k| k+1) + .unwrap_or(0) + } + + pub fn update_variant_or_create>(path: P, mut setting: SettingsJson, given_name: String) -> Result { if !setting.persistent { - return Ok(()) + return Self::open(path) } let path = path.as_ref(); let file = if path.exists() { let mut file = Self::open(path)?; + if setting.variant == u64::MAX { + setting.variant = file.next_available_id(); + } file.variants.insert(setting.variant, setting); file } else { @@ -57,6 +67,7 @@ impl FileJson { } }; - file.save(path) + file.save(path)?; + Ok(file) } } diff --git a/backend/src/persist/mod.rs b/backend/src/persist/mod.rs index 4c9a31b..809890d 100644 --- a/backend/src/persist/mod.rs +++ b/backend/src/persist/mod.rs @@ -14,3 +14,5 @@ pub use general::{MinMaxJson, SettingsJson}; pub use gpu::GpuJson; pub use error::{SerdeError, RonError}; + +pub const LATEST_VERSION: u64 = 0; diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index b1d5cf8..d2c6b8d 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -89,14 +89,45 @@ impl TGeneral for General { self.variant_id = id; } - fn get_variant_name(&self) -> &'_ str { - &self.variant_name - } - fn variant_name(&mut self, name: String) { self.variant_name = name; } + fn get_variants(&self) -> Vec { + if let Ok(file) = crate::persist::FileJson::open(self.get_path()) { + file.variants.into_iter() + .map(|(id, conf)| crate::api::VariantInfo { + id: id.to_string(), + name: conf.name, + }) + .collect() + } else { + vec![self.get_variant_info()] + } + } + + fn add_variant(&self, variant: crate::persist::SettingsJson) -> Result, SettingError> { + let variant_name = variant.name.clone(); + crate::persist::FileJson::update_variant_or_create(self.get_path(), variant, variant_name) + .map_err(|e| SettingError { + msg: format!("failed to add variant: {}", e), + setting: SettingVariant::General, + }) + .map(|file| file.variants.into_iter() + .map(|(id, conf)| crate::api::VariantInfo { + id: id.to_string(), + name: conf.name, + }) + .collect()) + } + + fn get_variant_info(&self) -> crate::api::VariantInfo { + crate::api::VariantInfo { + id: self.variant_id.to_string(), + name: self.variant_name.clone(), + } + } + fn provider(&self) -> crate::persist::DriverJson { self.driver.clone() } @@ -265,9 +296,10 @@ impl Settings { }*/ pub fn json(&self) -> SettingsJson { + let variant_info = self.general.get_variant_info(); SettingsJson { version: LATEST_VERSION, - name: self.general.get_variant_name().to_owned(), + name: variant_info.name, variant: self.general.get_variant_id(), persistent: self.general.get_persistent(), cpus: self.cpus.json(), diff --git a/backend/src/settings/traits.rs b/backend/src/settings/traits.rs index 8d93c55..cacff0a 100644 --- a/backend/src/settings/traits.rs +++ b/backend/src/settings/traits.rs @@ -109,14 +109,18 @@ pub trait TGeneral: OnSet + OnResume + OnPowerEvent + Debug + Send { fn name(&mut self, name: String); - fn get_variant_id(&self) -> u64; - fn variant_id(&mut self, id: u64); - fn get_variant_name(&self) -> &'_ str; - fn variant_name(&mut self, name: String); + fn get_variant_id(&self) -> u64; + + fn get_variants(&self) -> Vec; + + fn get_variant_info(&self) -> crate::api::VariantInfo; + + fn add_variant(&self, variant: crate::persist::SettingsJson) -> Result, SettingError>; + fn provider(&self) -> crate::persist::DriverJson; } diff --git a/src/backend.ts b/src/backend.ts index 1d8d1f0..b172690 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -349,3 +349,34 @@ export async function getPeriodicals(): Promise { settings_path: result[4], }; } + +export type StoreMetadata = { + name: string, + steam_app_id: number, + steam_user_id: number, + steam_username: string, + tags: string[], + id: string, + //config: any, +} + +export async function searchStoreByAppId(id: number): Promise { + return (await call_backend("WEB_search_by_app", [id]))[0]; +} + +export type VariantInfo = { + id: string, + name: string, +} + +export async function storeDownloadById(id: string): Promise { + return (await call_backend("WEB_download_new", [id])); +} + +export async function getAllSettingVariants(): Promise { + return (await call_backend("GENERAL_get_all_variants", [])); +} + +export async function getCurrentSettingVariant(): Promise { + return (await call_backend("GENERAL_get_current_variant", []))[0]; +} From 743f6425806d999d40ecea3b752082012386fa80 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 13 Jan 2024 12:32:12 -0500 Subject: [PATCH 23/56] Fix compile errors from smokepatio refactor, fix warnings from new compiler version --- backend/Cargo.lock | 7 +++ backend/Cargo.toml | 2 +- backend/src/persist/mod.rs | 2 +- backend/src/settings/generic_amd/mod.rs | 2 +- backend/src/settings/steam_deck/battery.rs | 54 +++++++++++++--------- backend/src/settings/steam_deck/mod.rs | 2 +- backend/src/settings/steam_deck/util.rs | 7 ++- backend/src/settings/unknown/mod.rs | 2 +- backend/src/state/mod.rs | 8 ++-- src/consts.ts | 2 + src/index.tsx | 4 ++ 11 files changed, 58 insertions(+), 34 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 0daaacc..8ea1bcf 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -370,6 +370,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encoding" version = "0.2.33" @@ -1327,6 +1333,7 @@ dependencies = [ name = "smokepatio" version = "0.1.0" dependencies = [ + "embedded-io", "log", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 15a72f7..e419cd1 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -30,7 +30,7 @@ simplelog = "0.12" limits_core = { version = "3", path = "./limits_core" } community_settings_core = { version = "0.1", path = "./community_settings_core" } regex = "1" -smokepatio = { version = "*", path = "../../smokepatio" } +smokepatio = { version = "0.1", features = [ "std" ], path = "../../smokepatio" } #libryzenadj = { version = "0.14", path = "../../libryzenadj-rs-14" } libryzenadj = { version = "0.13" } # ureq's tls feature does not like musl targets diff --git a/backend/src/persist/mod.rs b/backend/src/persist/mod.rs index 809890d..bfc0b2a 100644 --- a/backend/src/persist/mod.rs +++ b/backend/src/persist/mod.rs @@ -13,6 +13,6 @@ pub use file::FileJson; pub use general::{MinMaxJson, SettingsJson}; pub use gpu::GpuJson; -pub use error::{SerdeError, RonError}; +pub use error::SerdeError; pub const LATEST_VERSION: u64 = 0; diff --git a/backend/src/settings/generic_amd/mod.rs b/backend/src/settings/generic_amd/mod.rs index 0f443a9..89a2292 100644 --- a/backend/src/settings/generic_amd/mod.rs +++ b/backend/src/settings/generic_amd/mod.rs @@ -1,7 +1,7 @@ mod cpu; mod gpu; -pub use cpu::{Cpu, Cpus}; +pub use cpu::Cpus; pub use gpu::Gpu; fn _impl_checker() { diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index 1dea06c..12514aa 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -1,12 +1,12 @@ use std::convert::Into; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use sysfuss::{PowerSupplyAttribute, PowerSupplyPath, HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, SysEntity, SysEntityAttributesExt, SysAttribute}; use sysfuss::capability::attributes; use limits_core::json_v2::GenericBatteryLimit; -use smokepatio::ec::ChargeMode; +use smokepatio::ec::{ControllerSet, unnamed_power::{UnnamedPowerEC, ChargeMode}}; use crate::api::RangeLimit; use crate::persist::{BatteryEventJson, BatteryJson}; use crate::settings::{TBattery, ProviderBuilder}; @@ -21,6 +21,7 @@ pub struct Battery { state: crate::state::steam_deck::Battery, sysfs_bat: PowerSupplyPath, sysfs_hwmon: Arc, + bat_ec: Arc>, } #[derive(Debug, Clone)] @@ -39,6 +40,7 @@ struct EventInstruction { charge_mode: Option, is_triggered: bool, sysfs_hwmon: Arc, + bat_ec: Arc>, } impl OnPowerEvent for EventInstruction { @@ -116,7 +118,7 @@ impl EventInstruction { } } - fn from_json(other: BatteryEventJson, _version: u64, hwmon: Arc) -> Self { + fn from_json(other: BatteryEventJson, _version: u64, hwmon: Arc, ec: Arc>) -> Self { Self { trigger: Self::str_to_trigger(&other.trigger).unwrap_or(EventTrigger::Ignored), charge_rate: other.charge_rate, @@ -126,17 +128,17 @@ impl EventInstruction { .flatten(), is_triggered: false, sysfs_hwmon: hwmon, + bat_ec: ec, } } fn set_charge_mode(&self) -> Result<(), SettingError> { if let Some(charge_mode) = self.charge_mode { - smokepatio::ec::set(smokepatio::ec::Setting::ChargeMode, charge_mode as _) - .map_err(|e| SettingError { - msg: format!("Failed to set charge mode: {}", e), - setting: crate::settings::SettingVariant::Battery, - }) - .map(|_| ()) + let mut lock = self.bat_ec.lock().expect("failed to lock battery controller"); + lock.set(charge_mode).map_err(|_| SettingError { + msg: format!("Failed to set charge mode"), + setting: crate::settings::SettingVariant::Battery, + }) } else { Ok(()) } @@ -329,20 +331,18 @@ impl Battery { fn set_charge_mode(&mut self) -> Result<(), SettingError> { if let Some(charge_mode) = self.charge_mode { self.state.charge_mode_set = true; - smokepatio::ec::set(smokepatio::ec::Setting::ChargeMode, charge_mode as _) - .map_err(|e| SettingError { - msg: format!("Failed to set charge mode: {}", e), - setting: crate::settings::SettingVariant::Battery, - }) - .map(|_| ()) + let mut lock = self.bat_ec.lock().expect("Failed to lock battery controller"); + lock.set(charge_mode).map_err(|_| SettingError { + msg: format!("Failed to set charge mode"), + setting: crate::settings::SettingVariant::Battery, + }) } else if self.state.charge_mode_set { self.state.charge_mode_set = false; - smokepatio::ec::set(smokepatio::ec::Setting::ChargeMode, ChargeMode::Normal as _) - .map_err(|e| SettingError { - msg: format!("Failed to set charge mode: {}", e), - setting: crate::settings::SettingVariant::Battery, - }) - .map(|_| ()) + let mut lock = self.bat_ec.lock().expect("Failed to lock battery controller"); + lock.set(ChargeMode::Normal).map_err(|_| SettingError { + msg: format!("Failed to set charge mode"), + setting: crate::settings::SettingVariant::Battery, + }) } else { Ok(()) } @@ -488,6 +488,7 @@ impl Into for Battery { impl ProviderBuilder for Battery { fn from_json_and_limits(persistent: BatteryJson, version: u64, limits: GenericBatteryLimit) -> Self { let hwmon_sys = Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)); + let ec = Arc::new(Mutex::new(UnnamedPowerEC::new())); match version { 0 => Self { charge_rate: persistent.charge_rate, @@ -498,12 +499,13 @@ impl ProviderBuilder for Battery { events: persistent .events .into_iter() - .map(|x| EventInstruction::from_json(x, version, hwmon_sys.clone())) + .map(|x| EventInstruction::from_json(x, version, hwmon_sys.clone(), ec.clone())) .collect(), limits: limits, state: crate::state::steam_deck::Battery::default(), sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), sysfs_hwmon: hwmon_sys, + bat_ec: ec, }, _ => Self { charge_rate: persistent.charge_rate, @@ -514,12 +516,13 @@ impl ProviderBuilder for Battery { events: persistent .events .into_iter() - .map(|x| EventInstruction::from_json(x, version, hwmon_sys.clone())) + .map(|x| EventInstruction::from_json(x, version, hwmon_sys.clone(), ec.clone())) .collect(), limits: limits, state: crate::state::steam_deck::Battery::default(), sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), sysfs_hwmon: hwmon_sys, + bat_ec: ec, }, } } @@ -533,6 +536,7 @@ impl ProviderBuilder for Battery { state: crate::state::steam_deck::Battery::default(), sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), sysfs_hwmon: Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)), + bat_ec: Arc::new(Mutex::new(UnnamedPowerEC::new())), } } } @@ -728,6 +732,7 @@ impl TBattery for Battery { charge_mode: Some(ChargeMode::Idle), is_triggered: false, sysfs_hwmon: self.sysfs_hwmon.clone(), + bat_ec: self.bat_ec.clone(), }; } else { self.events.remove(index); @@ -743,6 +748,7 @@ impl TBattery for Battery { charge_mode: Some(ChargeMode::Idle), is_triggered: false, sysfs_hwmon: self.sysfs_hwmon.clone(), + bat_ec: self.bat_ec.clone(), }); } // lower limit @@ -760,6 +766,7 @@ impl TBattery for Battery { charge_mode: Some(ChargeMode::Normal), is_triggered: false, sysfs_hwmon: self.sysfs_hwmon.clone(), + bat_ec: self.bat_ec.clone(), }; } else { self.events.remove(index); @@ -776,6 +783,7 @@ impl TBattery for Battery { charge_mode: Some(ChargeMode::Normal), is_triggered: false, sysfs_hwmon: self.sysfs_hwmon.clone(), + bat_ec: self.bat_ec.clone(), }); } } diff --git a/backend/src/settings/steam_deck/mod.rs b/backend/src/settings/steam_deck/mod.rs index 676d7fe..b3f840d 100644 --- a/backend/src/settings/steam_deck/mod.rs +++ b/backend/src/settings/steam_deck/mod.rs @@ -5,7 +5,7 @@ mod power_dpm_force; mod util; pub use battery::Battery; -pub use cpu::{Cpu, Cpus}; +pub use cpu::Cpus; pub use gpu::Gpu; pub(self) use power_dpm_force::{POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT, DPM_FORCE_LIMITS_ATTRIBUTE}; diff --git a/backend/src/settings/steam_deck/util.rs b/backend/src/settings/steam_deck/util.rs index c57c70a..2d37f51 100644 --- a/backend/src/settings/steam_deck/util.rs +++ b/backend/src/settings/steam_deck/util.rs @@ -27,15 +27,18 @@ const THINGS: &[u8] = &[ const TIME_UNIT: std::time::Duration = std::time::Duration::from_millis(200); pub fn flash_led() { + use smokepatio::ec::ControllerSet; + let mut ec = smokepatio::ec::unnamed_power::UnnamedPowerEC::new(); for &code in THINGS { let on = code != 0; - if let Err(e) = smokepatio::ec::led::constant::set(if on { smokepatio::ec::led::constant::Colour::White } else { smokepatio::ec::led::constant::Colour::Off }) { + let colour = if on { smokepatio::ec::unnamed_power::StaticColour::Red } else { smokepatio::ec::unnamed_power::StaticColour::Off }; + if let Err(e) = ec.set(colour) { log::error!("Thing err: {}", e); } std::thread::sleep(TIME_UNIT); } log::debug!("Restoring LED state"); - smokepatio::ec::led::constant::set(smokepatio::ec::led::constant::Colour::Off) + ec.set(smokepatio::ec::unnamed_power::StaticColour::Off) .map_err(|e| log::error!("Failed to restore LED status: {}", e)) .unwrap(); } diff --git a/backend/src/settings/unknown/mod.rs b/backend/src/settings/unknown/mod.rs index bd0f419..200c1ea 100644 --- a/backend/src/settings/unknown/mod.rs +++ b/backend/src/settings/unknown/mod.rs @@ -3,7 +3,7 @@ mod cpu; mod gpu; pub use battery::Battery; -pub use cpu::{Cpu, Cpus}; +pub use cpu::Cpus; pub use gpu::Gpu; fn _impl_checker() { diff --git a/backend/src/state/mod.rs b/backend/src/state/mod.rs index 570de60..cd8d3bc 100644 --- a/backend/src/state/mod.rs +++ b/backend/src/state/mod.rs @@ -1,8 +1,8 @@ -mod error; -mod traits; +//mod error; +//mod traits; pub mod generic; pub mod steam_deck; -pub use error::StateError; -pub use traits::OnPoll; +//pub use error::StateError; +//pub use traits::OnPoll; diff --git a/src/consts.ts b/src/consts.ts index 4364cb6..415f5a5 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -30,6 +30,8 @@ export const SLOW_MEMORY_GPU = "GPU_slow_memory"; export const PERSISTENT_GEN = "GENERAL_persistent"; export const NAME_GEN = "GENERAL_name"; export const PATH_GEN = "GENERAL_path"; +export const VARIANTS_GEN = "GENERAL_setting_variants"; +export const CURRENT_VARIANT_GEN = "GENERAL_current_variant"; export const MESSAGE_LIST = "MESSAGE_messages"; diff --git a/src/index.tsx b/src/index.tsx index e65bfbe..9dd25e1 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -58,6 +58,8 @@ import { PERSISTENT_GEN, NAME_GEN, PATH_GEN, + VARIANTS_GEN, + CURRENT_VARIANT_GEN, MESSAGE_LIST, @@ -158,6 +160,8 @@ const reload = function() { backend.resolve(backend.getGeneralPersistent(), (value: boolean) => { set_value(PERSISTENT_GEN, value) }); backend.resolve(backend.getGeneralSettingsName(), (name: string) => { set_value(NAME_GEN, name) }); backend.resolve(backend.getGeneralSettingsPath(), (path: string) => { set_value(PATH_GEN, path) }); + backend.resolve(backend.getAllSettingVariants(), (variants: backend.VariantInfo[]) => { set_value(VARIANTS_GEN, variants) }); + backend.resolve(backend.getCurrentSettingVariant(), (variant: backend.VariantInfo) => { set_value(CURRENT_VARIANT_GEN, variant) }); backend.resolve(backend.getInfo(), (info: string) => { set_value(BACKEND_INFO, info) }); backend.resolve(backend.getDriverProviderName("gpu"), (driver: string) => { set_value(DRIVER_INFO, driver) }); From bba9452383d7e15b140fce5a1c9cc890a443f82c Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 21 Jan 2024 20:03:04 -0500 Subject: [PATCH 24/56] Add front-end components for setting variants, fix back-end segfault --- backend/Cargo.lock | 1 + backend/Cargo.toml | 1 + backend/src/api/general.rs | 36 ++++- backend/src/api/handler.rs | 10 ++ backend/src/main.rs | 6 + backend/src/settings/general.rs | 49 ++++--- backend/src/utility.rs | 10 ++ package.json | 8 +- pnpm-lock.yaml | 248 ++++++++++++++++---------------- src/backend.ts | 14 +- src/index.tsx | 83 ++++++++++- 11 files changed, 309 insertions(+), 157 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 8ea1bcf..64ff1dd 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1073,6 +1073,7 @@ version = "1.5.0-ng1" dependencies = [ "async-trait", "community_settings_core", + "libc", "libryzenadj", "limits_core", "log", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index e419cd1..8a8e2b0 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -31,6 +31,7 @@ limits_core = { version = "3", path = "./limits_core" } community_settings_core = { version = "0.1", path = "./community_settings_core" } regex = "1" smokepatio = { version = "0.1", features = [ "std" ], path = "../../smokepatio" } +libc = "0.2" #libryzenadj = { version = "0.14", path = "../../libryzenadj-rs-14" } libryzenadj = { version = "0.13" } # ureq's tls feature does not like musl targets diff --git a/backend/src/api/general.rs b/backend/src/api/general.rs index a534d22..2fb27d4 100644 --- a/backend/src/api/general.rs +++ b/backend/src/api/general.rs @@ -65,17 +65,17 @@ pub fn load_settings( move |params_in: super::ApiParameterType| { if let Some(Primitive::String(id)) = params_in.get(0) { if let Some(Primitive::String(name)) = params_in.get(1) { - if let Some(Primitive::F64(variant_id)) = params_in.get(2) { + if let Some(Primitive::String(variant_id)) = params_in.get(2) { if let Some(Primitive::String(variant_name)) = params_in.get(3) { setter(id.parse().unwrap_or_default(), name.to_owned(), - *variant_id as _, + variant_id.parse().unwrap_or_default(), Some(variant_name.to_owned())); vec![true.into()] } else { setter(id.parse().unwrap_or_default(), name.to_owned(), - *variant_id as _, + variant_id.parse().unwrap_or_default(), None); vec![true.into()] } @@ -95,6 +95,36 @@ pub fn load_settings( } } +/// Generate load app settings from file web method +pub fn load_variant( + sender: Sender, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety + let setter = move |variant: u64, variant_name: Option| { + sender + .lock() + .unwrap() + .send(ApiMessage::LoadVariant(variant, variant_name.unwrap_or_else(|| crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned()))) + .expect("load_settings send failed") + }; + move |params_in: super::ApiParameterType| { + if let Some(Primitive::String(variant_id)) = params_in.get(0) { + if let Some(Primitive::String(variant_name)) = params_in.get(1) { + setter(variant_id.parse().unwrap_or(u64::MAX), + Some(variant_name.to_owned())); + vec![true.into()] + } else { + setter(variant_id.parse().unwrap_or_default(), + None); + vec![true.into()] + } + } else { + log::warn!("load_settings missing variant id parameter"); + vec!["load_settings missing variant id parameter".into()] + } + } +} + /// Generate load default settings from file web method pub fn load_default_settings( sender: Sender, diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index a00507e..c49d70f 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -23,6 +23,7 @@ pub enum ApiMessage { PowerVibeCheck, WaitForEmptyQueue(Callback<()>), LoadSettings(u64, String, u64, String), // (path, name, variant, variant name) + LoadVariant(u64, String), // (variant, variant name) -- path and name assumed to be for current profile LoadMainSettings, LoadSystemSettings, GetLimits(Callback), @@ -275,6 +276,7 @@ fn print_errors(call_name: &str, errors: Vec) { impl ApiMessageHandler { pub fn process_forever(&mut self, settings: &mut Settings) { + crate::utility::ioperm_power_ec(); //let mut dirty_echo = true; // set everything twice, to make sure PowerTools wins on race conditions while let Ok(msg) = self.intake.recv() { let mut dirty = self.process(settings, msg); @@ -387,6 +389,14 @@ impl ApiMessageHandler { } true } + ApiMessage::LoadVariant(variant_id, variant_name) => { + let path = settings.general.get_path(); + match settings.load_file(path.into(), settings.general.get_name().to_owned(), variant_id, variant_name, false) { + Ok(success) => log::info!("Loaded settings file? {}", success), + Err(e) => log::warn!("Load file err: {}", e), + } + true + } ApiMessage::LoadMainSettings => { match settings.load_file( crate::consts::DEFAULT_SETTINGS_FILE.into(), diff --git a/backend/src/main.rs b/backend/src/main.rs index e62a4a6..121630a 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -254,6 +254,10 @@ fn main() -> Result<(), ()> { "GENERAL_load_settings", api::general::load_settings(api_sender.clone()), ) + .register( + "GENERAL_load_variant", + api::general::load_variant(api_sender.clone()), + ) .register( "GENERAL_load_default_settings", api::general::load_default_settings(api_sender.clone()), @@ -318,6 +322,8 @@ fn main() -> Result<(), ()> { api::web::download_new_config(api_sender.clone()) ); + utility::ioperm_power_ec(); + if let Err(e) = loaded_settings.on_set() { e.iter() .for_each(|e| log::error!("Startup Settings.on_set() error: {}", e)); diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index d2c6b8d..75167a8 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -229,27 +229,38 @@ impl Settings { ) -> Result { let json_path = crate::utility::settings_dir().join(&filename); if json_path.exists() { - let file_json = FileJson::open(&json_path).map_err(|e| SettingError { - msg: format!("Failed to open settings {}: {}", json_path.display(), e), - setting: SettingVariant::General, - })?; - let settings_json = Self::get_variant(&file_json, variant, variant_name)?; - if !settings_json.persistent { - log::warn!( - "Loaded persistent config `{}` ({}) with persistent=false", - &settings_json.name, - json_path.display() - ); - *self.general.persistent() = false; - self.general.name(name); + if variant == u64::MAX { + *self.general.persistent() = true; + let file_json = FileJson::update_variant_or_create(&json_path, self.json(), variant_name.clone()).map_err(|e| SettingError { + msg: format!("Failed to open settings {}: {}", json_path.display(), e), + setting: SettingVariant::General, + })?; + self.general.variant_id(file_json.variants.iter().find(|(_key, val)| val.name == variant_name).map(|(key, _val)| *key).expect("Setting variant was not added properly")); + self.general.variant_name(variant_name); } else { - let x = super::Driver::init(name, settings_json, json_path.clone()); - log::info!("Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), x.cpus.provider(), x.gpu.provider(), x.battery.provider()); - self.general = x.general; - self.cpus = x.cpus; - self.gpu = x.gpu; - self.battery = x.battery; + let file_json = FileJson::open(&json_path).map_err(|e| SettingError { + msg: format!("Failed to open settings {}: {}", json_path.display(), e), + setting: SettingVariant::General, + })?; + let settings_json = Self::get_variant(&file_json, variant, variant_name)?; + if !settings_json.persistent { + log::warn!( + "Loaded persistent config `{}` ({}) with persistent=false", + &settings_json.name, + json_path.display() + ); + *self.general.persistent() = false; + self.general.name(name); + } else { + let x = super::Driver::init(name, settings_json, json_path.clone()); + log::info!("Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), x.cpus.provider(), x.gpu.provider(), x.battery.provider()); + self.general = x.general; + self.cpus = x.cpus; + self.gpu = x.gpu; + self.battery = x.battery; + } } + } else { if system_defaults { self.load_system_default(name, variant, variant_name); diff --git a/backend/src/utility.rs b/backend/src/utility.rs index 123ce3a..f6999c7 100644 --- a/backend/src/utility.rs +++ b/backend/src/utility.rs @@ -103,6 +103,16 @@ pub fn read_version_file() -> String { } } } + +pub fn ioperm_power_ec() { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + unsafe { + let temp_ec = smokepatio::ec::unnamed_power::UnnamedPowerEC::new(); + libc::ioperm(temp_ec.ec().data() as _, 1, 1); + libc::ioperm(temp_ec.ec().cmd() as _, 1, 1); + } +} + #[cfg(test)] mod generate { #[test] diff --git a/package.json b/package.json index 11a3af9..433c5b1 100644 --- a/package.json +++ b/package.json @@ -31,16 +31,16 @@ "@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-typescript": "^8.5.0", "@types/react": "16.14.0", - "@types/webpack": "^5.28.1", + "@types/webpack": "^5.28.5", "rollup": "^2.79.1", "rollup-plugin-import-assets": "^1.1.1", "shx": "^0.3.4", - "tslib": "^2.5.3", + "tslib": "^2.6.2", "typescript": "^4.9.5" }, "dependencies": { - "decky-frontend-lib": "~3.21.1", - "react-icons": "^4.9.0", + "decky-frontend-lib": "~3.24.3", + "react-icons": "^5.0.1", "usdpl-front": "file:src/usdpl_front" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c815130..9c87589 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,11 +6,11 @@ settings: dependencies: decky-frontend-lib: - specifier: ~3.21.1 - version: 3.21.1 + specifier: ~3.24.3 + version: 3.24.3 react-icons: - specifier: ^4.9.0 - version: 4.9.0(react@18.2.0) + specifier: ^5.0.1 + version: 5.0.1(react@18.2.0) usdpl-front: specifier: file:src/usdpl_front version: file:src/usdpl_front @@ -30,13 +30,13 @@ devDependencies: version: 4.0.0(rollup@2.79.1) '@rollup/plugin-typescript': specifier: ^8.5.0 - version: 8.5.0(rollup@2.79.1)(tslib@2.5.3)(typescript@4.9.5) + version: 8.5.0(rollup@2.79.1)(tslib@2.6.2)(typescript@4.9.5) '@types/react': specifier: 16.14.0 version: 16.14.0 '@types/webpack': - specifier: ^5.28.1 - version: 5.28.1 + specifier: ^5.28.5 + version: 5.28.5 rollup: specifier: ^2.79.1 version: 2.79.1 @@ -47,8 +47,8 @@ devDependencies: specifier: ^0.3.4 version: 0.3.4 tslib: - specifier: ^2.5.3 - version: 2.5.3 + specifier: ^2.6.2 + version: 2.6.2 typescript: specifier: ^4.9.5 version: 4.9.5 @@ -61,11 +61,11 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.22 dev: true - /@jridgewell/resolve-uri@3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} dev: true @@ -74,26 +74,22 @@ packages: engines: {node: '>=6.0.0'} dev: true - /@jridgewell/source-map@0.3.3: - resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} + /@jridgewell/source-map@0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - dev: true - - /@jridgewell/sourcemap-codec@1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + '@jridgewell/trace-mapping': 0.3.22 dev: true /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true - /@jridgewell/trace-mapping@0.3.18: - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} + /@jridgewell/trace-mapping@0.3.22: + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 dev: true /@rollup/plugin-commonjs@21.1.0(rollup@2.79.1): @@ -108,7 +104,7 @@ packages: glob: 7.2.3 is-reference: 1.2.1 magic-string: 0.25.9 - resolve: 1.22.2 + resolve: 1.22.8 rollup: 2.79.1 dev: true @@ -132,7 +128,7 @@ packages: deepmerge: 4.3.1 is-builtin-module: 3.2.1 is-module: 1.0.0 - resolve: 1.22.2 + resolve: 1.22.8 rollup: 2.79.1 dev: true @@ -146,7 +142,7 @@ packages: rollup: 2.79.1 dev: true - /@rollup/plugin-typescript@8.5.0(rollup@2.79.1)(tslib@2.5.3)(typescript@4.9.5): + /@rollup/plugin-typescript@8.5.0(rollup@2.79.1)(tslib@2.6.2)(typescript@4.9.5): resolution: {integrity: sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ==} engines: {node: '>=8.0.0'} peerDependencies: @@ -158,9 +154,9 @@ packages: optional: true dependencies: '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - resolve: 1.22.2 + resolve: 1.22.8 rollup: 2.79.1 - tslib: 2.5.3 + tslib: 2.6.2 typescript: 4.9.5 dev: true @@ -176,59 +172,61 @@ packages: rollup: 2.79.1 dev: true - /@types/eslint-scope@3.7.4: - resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} + /@types/eslint-scope@3.7.7: + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: - '@types/eslint': 8.40.2 - '@types/estree': 1.0.1 + '@types/eslint': 8.56.2 + '@types/estree': 1.0.5 dev: true - /@types/eslint@8.40.2: - resolution: {integrity: sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==} + /@types/eslint@8.56.2: + resolution: {integrity: sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==} dependencies: - '@types/estree': 1.0.1 - '@types/json-schema': 7.0.12 + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 dev: true /@types/estree@0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} dev: true - /@types/estree@1.0.1: - resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/json-schema@7.0.12: - resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true - /@types/node@20.3.1: - resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==} + /@types/node@20.11.5: + resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} + dependencies: + undici-types: 5.26.5 dev: true - /@types/prop-types@15.7.5: - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + /@types/prop-types@15.7.11: + resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} dev: true /@types/react@16.14.0: resolution: {integrity: sha512-jJjHo1uOe+NENRIBvF46tJimUvPnmbQ41Ax0pEm7pRvhPg+wuj8VMOHHiMvaGmZRzRrCtm7KnL5OOE/6kHPK8w==} dependencies: - '@types/prop-types': 15.7.5 - csstype: 3.1.2 + '@types/prop-types': 15.7.11 + csstype: 3.1.3 dev: true /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.3.1 + '@types/node': 20.11.5 dev: true - /@types/webpack@5.28.1: - resolution: {integrity: sha512-qw1MqGZclCoBrpiSe/hokSgQM/su8Ocpl3L/YHE0L6moyaypg4+5F7Uzq7NgaPKPxUxUbQ4fLPLpDWdR27bCZw==} + /@types/webpack@5.28.5: + resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} dependencies: - '@types/node': 20.3.1 + '@types/node': 20.11.5 tapable: 2.2.1 - webpack: 5.87.0 + webpack: 5.89.0 transitivePeerDependencies: - '@swc/core' - esbuild @@ -350,16 +348,16 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true - /acorn-import-assertions@1.9.0(acorn@8.9.0): + /acorn-import-assertions@1.9.0(acorn@8.11.3): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: acorn: ^8 dependencies: - acorn: 8.9.0 + acorn: 8.11.3 dev: true - /acorn@8.9.0: - resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==} + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} hasBin: true dev: true @@ -392,15 +390,15 @@ packages: concat-map: 0.0.1 dev: true - /browserslist@4.21.9: - resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} + /browserslist@4.22.2: + resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001503 - electron-to-chromium: 1.4.433 - node-releases: 2.0.12 - update-browserslist-db: 1.0.11(browserslist@4.21.9) + caniuse-lite: 1.0.30001579 + electron-to-chromium: 1.4.640 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.22.2) dev: true /buffer-from@1.1.2: @@ -412,8 +410,8 @@ packages: engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001503: - resolution: {integrity: sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==} + /caniuse-lite@1.0.30001579: + resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} dev: true /chrome-trace-event@1.0.3: @@ -433,12 +431,12 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true - /csstype@3.1.2: - resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} dev: true - /decky-frontend-lib@3.21.1: - resolution: {integrity: sha512-30605ET9qqZ6St6I9WmMmLGgSrTIdMwo7xy85+lRaF1miUd2icOGEJjwnbVcZDdkal+1fJ3tNEDXlchVfG4TrA==} + /decky-frontend-lib@3.24.3: + resolution: {integrity: sha512-293oUaAgLrezvoz+TOQkarjwAlVlejkelB1WjtxQV4Y5qMpUZhNUtfpQAscGhwg9oQy6UGpZ5urkdPzLiVY52w==} dev: false /deepmerge@4.3.1: @@ -446,8 +444,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /electron-to-chromium@1.4.433: - resolution: {integrity: sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==} + /electron-to-chromium@1.4.640: + resolution: {integrity: sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==} dev: true /enhanced-resolve@5.15.0: @@ -458,8 +456,8 @@ packages: tapable: 2.2.1 dev: true - /es-module-lexer@1.3.0: - resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} + /es-module-lexer@1.4.1: + resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} dev: true /escalade@3.1.1: @@ -521,16 +519,16 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true dev: true optional: true - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: true /glob-to-regexp@0.4.1: @@ -557,11 +555,11 @@ packages: engines: {node: '>=8'} dev: true - /has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} dependencies: - function-bind: 1.1.1 + function-bind: 1.1.2 dev: true /inflight@1.0.6: @@ -587,10 +585,10 @@ packages: builtin-modules: 3.3.0 dev: true - /is-core-module@2.12.1: - resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: - has: 1.0.3 + hasown: 2.0.0 dev: true /is-module@1.0.0: @@ -600,14 +598,14 @@ packages: /is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 dev: true /jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.3.1 + '@types/node': 20.11.5 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -672,8 +670,8 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true - /node-releases@2.0.12: - resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true /once@1.4.0: @@ -700,8 +698,8 @@ packages: engines: {node: '>=8.6'} dev: true - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} dev: true @@ -711,8 +709,8 @@ packages: safe-buffer: 5.2.1 dev: true - /react-icons@4.9.0(react@18.2.0): - resolution: {integrity: sha512-ijUnFr//ycebOqujtqtV9PFS7JjhWg0QU6ykURVHuL4cbofvRCf3f6GMn9+fBktEFQOIVZnuAYLZdiyadRQRFg==} + /react-icons@5.0.1(react@18.2.0): + resolution: {integrity: sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==} peerDependencies: react: '*' dependencies: @@ -730,14 +728,14 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} dependencies: - resolve: 1.22.2 + resolve: 1.22.8 dev: true - /resolve@1.22.2: - resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true dependencies: - is-core-module: 2.12.1 + is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: true @@ -763,7 +761,7 @@ packages: engines: {node: '>=10.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /safe-buffer@5.2.1: @@ -774,13 +772,13 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/json-schema': 7.0.12 + '@types/json-schema': 7.0.15 ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) dev: true - /serialize-javascript@6.0.1: - resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} dependencies: randombytes: 2.1.0 dev: true @@ -838,8 +836,8 @@ packages: engines: {node: '>=6'} dev: true - /terser-webpack-plugin@5.3.9(webpack@5.87.0): - resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} + /terser-webpack-plugin@5.3.10(webpack@5.89.0): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -854,27 +852,27 @@ packages: uglify-js: optional: true dependencies: - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.22 jest-worker: 27.5.1 schema-utils: 3.3.0 - serialize-javascript: 6.0.1 - terser: 5.18.0 - webpack: 5.87.0 + serialize-javascript: 6.0.2 + terser: 5.27.0 + webpack: 5.89.0 dev: true - /terser@5.18.0: - resolution: {integrity: sha512-pdL757Ig5a0I+owA42l6tIuEycRuM7FPY4n62h44mRLRfnOxJkkOHd6i89dOpwZlpF6JXBwaAHF6yWzFrt+QyA==} + /terser@5.27.0: + resolution: {integrity: sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==} engines: {node: '>=10'} hasBin: true dependencies: - '@jridgewell/source-map': 0.3.3 - acorn: 8.9.0 + '@jridgewell/source-map': 0.3.5 + acorn: 8.11.3 commander: 2.20.3 source-map-support: 0.5.21 dev: true - /tslib@2.5.3: - resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: true /typescript@4.9.5: @@ -883,13 +881,17 @@ packages: hasBin: true dev: true - /update-browserslist-db@1.0.11(browserslist@4.21.9): - resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /update-browserslist-db@1.0.13(browserslist@4.22.2): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.21.9 + browserslist: 4.22.2 escalade: 3.1.1 picocolors: 1.0.0 dev: true @@ -897,7 +899,7 @@ packages: /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: - punycode: 2.3.0 + punycode: 2.3.1 dev: true /url-join@4.0.1: @@ -917,8 +919,8 @@ packages: engines: {node: '>=10.13.0'} dev: true - /webpack@5.87.0: - resolution: {integrity: sha512-GOu1tNbQ7p1bDEoFRs2YPcfyGs8xq52yyPBZ3m2VGnXGtV9MxjrkABHm4V9Ia280OefsSLzvbVoXcfLxjKY/Iw==} + /webpack@5.89.0: + resolution: {integrity: sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -927,17 +929,17 @@ packages: webpack-cli: optional: true dependencies: - '@types/eslint-scope': 3.7.4 - '@types/estree': 1.0.1 + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.5 '@webassemblyjs/ast': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.9.0 - acorn-import-assertions: 1.9.0(acorn@8.9.0) - browserslist: 4.21.9 + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) + browserslist: 4.22.2 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 - es-module-lexer: 1.3.0 + es-module-lexer: 1.4.1 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -948,7 +950,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.9(webpack@5.87.0) + terser-webpack-plugin: 5.3.10(webpack@5.89.0) watchpack: 2.4.0 webpack-sources: 3.2.3 transitivePeerDependencies: diff --git a/src/backend.ts b/src/backend.ts index b172690..3f69d87 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -247,7 +247,7 @@ export async function getGeneralPersistent(): Promise { return (await call_backend("GENERAL_get_persistent", []))[0]; } -export async function loadGeneralSettings(id: string, name: string, variant_id: number, variant_name: string | undefined): Promise { +export async function loadGeneralSettings(id: string, name: string, variant_id: string, variant_name: string | undefined): Promise { if (variant_name) { return (await call_backend("GENERAL_load_settings", [id, name, variant_id, variant_name]))[0]; } else { @@ -256,6 +256,15 @@ export async function loadGeneralSettings(id: string, name: string, variant_id: } +export async function loadGeneralSettingsVariant(variant_id: string, variant_name: string | undefined): Promise { + console.log("GENERAL_load_variant"); + if (variant_name) { + return (await call_backend("GENERAL_load_variant", [variant_id, variant_name]))[0]; + } else { + return (await call_backend("GENERAL_load_variant", [variant_id]))[0]; + } +} + export async function loadGeneralDefaultSettings(): Promise { return (await call_backend("GENERAL_load_default_settings", []))[0]; } @@ -361,6 +370,7 @@ export type StoreMetadata = { } export async function searchStoreByAppId(id: number): Promise { + console.log("WEB_search_by_app"); return (await call_backend("WEB_search_by_app", [id]))[0]; } @@ -374,9 +384,11 @@ export async function storeDownloadById(id: string): Promise { } export async function getAllSettingVariants(): Promise { + console.log("GENERAL_get_all_variants"); return (await call_backend("GENERAL_get_all_variants", [])); } export async function getCurrentSettingVariant(): Promise { + console.log("GENERAL_get_current_variant"); return (await call_backend("GENERAL_get_current_variant", []))[0]; } diff --git a/src/index.tsx b/src/index.tsx index 9dd25e1..3fff822 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,7 @@ import { ButtonItem, definePlugin, - //DialogButton, + DialogButton, //Menu, //MenuItem, PanelSection, @@ -13,15 +13,15 @@ import { ToggleField, //Dropdown, Field, - //DropdownOption, - //SingleDropdownOption, + Dropdown, + SingleDropdownOption, //NotchLabel //gamepadDialogClasses, //joinClassNames, } from "decky-frontend-lib"; import { VFC, useState } from "react"; import { GiDrill, GiTimeBomb, GiTimeTrap, GiDynamite } from "react-icons/gi"; -import { HiRefresh, HiTrash } from "react-icons/hi"; +import { HiRefresh, HiTrash, HiPlus, HiUpload } from "react-icons/hi"; //import * as python from "./python"; import * as backend from "./backend"; @@ -73,7 +73,7 @@ import { Battery } from "./components/battery"; import { Cpus } from "./components/cpus"; import { DevMessages } from "./components/message"; -var periodicHook: NodeJS.Timer | null = null; +var periodicHook: NodeJS.Timeout | null = null; var lifetimeHook: any = null; var startHook: any = null; var endHook: any = null; @@ -206,11 +206,12 @@ const registerCallbacks = function(autoclear: boolean) { startHook = SteamClient.Apps.RegisterForGameActionStart((actionType, id) => { //@ts-ignore let gameInfo: any = appStore.GetAppOverviewByGameID(id); + let appId = gameInfo.appid.toString(); backend.log(backend.LogLevel.Info, "RegisterForGameActionStart callback(" + actionType + ", " + id + ")"); // don't use gameInfo.appid, haha backend.resolve( - backend.loadGeneralSettings(id.toString(), gameInfo.display_name, 0, undefined), + backend.loadGeneralSettings(appId, gameInfo.display_name, "0", undefined), (ok: boolean) => { backend.log(backend.LogLevel.Debug, "Loading settings ok? " + ok); reload(); @@ -303,6 +304,11 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { ) } + const variantOptions: SingleDropdownOption[] = (get_value(VARIANTS_GEN) as backend.VariantInfo[]).map((elem) => {return { + data: elem, + label: {elem.name}, + };}); + return ( @@ -338,6 +344,69 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { {get_value(NAME_GEN)} + + + { + backend.log(backend.LogLevel.Debug, "POWERTOOLS: looking for data " + (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).toString()); + return (val.data as backend.VariantInfo).id == (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).id; + })} + strDefaultLabel={(get_value(VARIANTS_GEN) as backend.VariantInfo[])[0].name} + onChange={(elem: SingleDropdownOption) => { + let data = elem.data as backend.VariantInfo; + backend.log(backend.LogLevel.Debug, "Profile variant dropdown selected " + elem.data.toString()); + backend.loadGeneralSettingsVariant(data.id, data.name); + set_value(CURRENT_VARIANT_GEN, elem.data as backend.VariantInfo); + reloadGUI("ProfileVariantGovernor"); + }} + /> + + + + { + backend.log(backend.LogLevel.Debug, "Creating new PowerTools settings variant"); + backend.resolve( + backend.loadGeneralSettingsVariant("please give me a new ID k thx bye" /* anything that cannot be parsed as a u64 will be set to u64::MAX, which will cause the back-end to auto-generate an ID */, undefined), + (ok: boolean) => { + backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok); + reload(); + backend.resolve(backend.waitForComplete(), (_) => { + backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new settings variant"); + tryNotifyProfileChange(); + }); + } + ); + }} + > + + + { + backend.log(backend.LogLevel.Debug, "Clicked on unimplemented upload button"); + }} + > + + + @@ -389,9 +458,9 @@ export default definePlugin((serverApi: ServerAPI) => { content: , icon: ico, onDismount() { + tryNotifyProfileChange = function() {}; backend.log(backend.LogLevel.Debug, "PowerTools shutting down"); clearHooks(); - tryNotifyProfileChange = function() {}; //serverApi.routerHook.removeRoute("/decky-plugin-test"); }, }; From 2986c05170647c96f11347777566c7e3d9fa9d15 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 22 Jan 2024 17:40:15 -0500 Subject: [PATCH 25/56] Revert 37f96a5cdd for main.py --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 018ac69..9f7916b 100644 --- a/main.py +++ b/main.py @@ -17,9 +17,9 @@ class Plugin: env_proc["LD_LIBRARY_PATH"] += ":"+PARENT_DIR+"/bin" else: env_proc["LD_LIBRARY_PATH"] = ":"+PARENT_DIR+"/bin" - '''self.backend_proc = subprocess.Popen( + self.backend_proc = subprocess.Popen( [PARENT_DIR + "/bin/backend"], - env = env_proc)''' + env = env_proc) while True: await asyncio.sleep(1) From 622f1615608e4eb11cf0a329b54a3c197701fab0 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 27 Jan 2024 15:05:41 -0500 Subject: [PATCH 26/56] Add minimal store UI functionality --- backend/src/api/handler.rs | 19 ++- backend/src/api/web.rs | 145 ++++++++++++++++++--- backend/src/main.rs | 8 +- backend/src/persist/file.rs | 4 +- backend/src/settings/detect/auto_detect.rs | 3 + backend/src/settings/driver.rs | 7 +- backend/src/settings/general.rs | 27 ++-- backend/src/settings/traits.rs | 4 + src/backend.ts | 4 + src/components/battery.tsx | 2 +- src/consts.ts | 7 + src/index.tsx | 63 ++++++++- src/store/page.tsx | 48 +++++++ 13 files changed, 297 insertions(+), 44 deletions(-) create mode 100644 src/store/page.tsx diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index c49d70f..62c433f 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -28,6 +28,7 @@ pub enum ApiMessage { LoadSystemSettings, GetLimits(Callback), GetProvider(String, Callback), + UploadCurrentVariant(String, String), // SteamID, Steam username } pub enum BatteryMessage { @@ -251,7 +252,7 @@ impl GeneralMessage { cb(Vec::with_capacity(0)) }, }, - Self::ApplyNow => {} + Self::ApplyNow => {}, } dirty } @@ -304,7 +305,7 @@ impl ApiMessageHandler { if is_persistent { let settings_clone = settings.json(); let save_json: SettingsJson = settings_clone.into(); - if let Err(e) = crate::persist::FileJson::update_variant_or_create(&save_path, save_json, settings.general.get_name().to_owned()) { + if let Err(e) = crate::persist::FileJson::update_variant_or_create(&save_path, settings.general.get_app_id(), save_json, settings.general.get_name().to_owned()) { log::error!("Failed to create/update settings file {}: {}", save_path.display(), e); } //unwrap_maybe_fatal(save_json.save(&save_path), "Failed to save settings"); @@ -383,7 +384,7 @@ impl ApiMessageHandler { } ApiMessage::LoadSettings(id, name, variant_id, variant_name) => { let path = format!("{}.ron", id); - match settings.load_file(path.into(), name, variant_id, variant_name, false) { + match settings.load_file(path.into(), id, name, variant_id, variant_name, false) { Ok(success) => log::info!("Loaded settings file? {}", success), Err(e) => log::warn!("Load file err: {}", e), } @@ -391,7 +392,8 @@ impl ApiMessageHandler { } ApiMessage::LoadVariant(variant_id, variant_name) => { let path = settings.general.get_path(); - match settings.load_file(path.into(), settings.general.get_name().to_owned(), variant_id, variant_name, false) { + let app_id = settings.general.get_app_id(); + match settings.load_file(path.into(), app_id, settings.general.get_name().to_owned(), variant_id, variant_name, false) { Ok(success) => log::info!("Loaded settings file? {}", success), Err(e) => log::warn!("Load file err: {}", e), } @@ -400,6 +402,7 @@ impl ApiMessageHandler { ApiMessage::LoadMainSettings => { match settings.load_file( crate::consts::DEFAULT_SETTINGS_FILE.into(), + 0, crate::consts::DEFAULT_SETTINGS_NAME.to_owned(), 0, crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(), @@ -431,7 +434,13 @@ impl ApiMessageHandler { _ => settings.general.provider(), }); false - } + }, + ApiMessage::UploadCurrentVariant(steam_id, steam_username) => { + //TODO + let steam_app_id = settings.general.get_app_id(); + super::web::upload_settings(steam_app_id, steam_id, steam_username, settings.json()); + false + }, } } diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index a7b5f0d..08b9eab 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -87,34 +87,104 @@ fn web_config_to_settings_json(meta: community_settings_core::v1::Metadata) -> c } } +fn download_config(id: u128) -> std::io::Result { + let req_url = format!("{}/api/setting/by_id/{}", BASE_URL, id); + let response = ureq::get(&req_url).call() + .map_err(|e| { + log::warn!("GET to {} failed: {}", req_url, e); + std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e) + })?; + response.into_json() +} + +pub fn upload_settings(id: u64, user_id: String, username: String, settings: crate::persist::SettingsJson) { + log::info!("Uploading settings {} by {} ({})", settings.name, username, user_id); + let user_id: u64 = match user_id.parse() { + Ok(id) => id, + Err(e) => { + log::error!("Failed to parse `{}` as u64: {} (aborted upload_settings very early)", user_id, e); + return; + } + }; + let meta = settings_to_web_config(id as _, user_id, username, settings); + if let Err(e) = upload_config(meta) { + log::error!("Failed to upload settings: {}", e); + } +} + +fn settings_to_web_config(app_id: u32, user_id: u64, username: String, settings: crate::persist::SettingsJson) -> community_settings_core::v1::Metadata { + community_settings_core::v1::Metadata { + name: settings.name, + steam_app_id: app_id, + steam_user_id: user_id, + steam_username: username, + tags: vec!["wip".to_owned()], + id: "".to_owned(), + config: community_settings_core::v1::Config { + cpus: settings.cpus.into_iter().map(|cpu| community_settings_core::v1::Cpu { + online: cpu.online, + clock_limits: cpu.clock_limits.map(|lim| community_settings_core::v1::MinMax { + min: lim.min, + max: lim.max, + }), + governor: cpu.governor, + }).collect(), + gpu: community_settings_core::v1::Gpu { + fast_ppt: settings.gpu.fast_ppt, + slow_ppt: settings.gpu.slow_ppt, + tdp: settings.gpu.tdp, + tdp_boost: settings.gpu.tdp_boost, + clock_limits: settings.gpu.clock_limits.map(|lim| community_settings_core::v1::MinMax { + min: lim.min, + max: lim.max, + }), + slow_memory: settings.gpu.slow_memory, + }, + battery: community_settings_core::v1::Battery { + charge_rate: settings.battery.charge_rate, + charge_mode: settings.battery.charge_mode, + events: settings.battery.events.into_iter().map(|batt_ev| community_settings_core::v1::BatteryEvent { + trigger: batt_ev.trigger, + charge_rate: batt_ev.charge_rate, + charge_mode: batt_ev.charge_mode, + }).collect(), + }, + }, + } +} + +fn upload_config(config: community_settings_core::v1::Metadata) -> std::io::Result<()> { + let req_url = format!("{}/api/setting", BASE_URL); + ureq::post(&req_url) + .send_json(&config) + .map_err(|e| { + log::warn!("POST to {} failed: {}", req_url, e); + std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e) + }) + .map(|_| ()) +} + /// Download config web method pub fn download_new_config(sender: Sender) -> impl AsyncCallable { let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety let getter = move || { let sender2 = sender.clone(); move |id: u128| { - let req_url = format!("{}/api/setting/by_id/{}", BASE_URL, id); - match ureq::get(&req_url).call() { - Ok(response) => { - let json_res: std::io::Result = response.into_json(); - match json_res { - Ok(meta) => { - let (tx, rx) = mpsc::channel(); - let callback = - move |values: Vec| tx.send(values).expect("download_new_config callback send failed"); - sender2 - .lock() - .unwrap() - .send(ApiMessage::General(GeneralMessage::AddVariant(web_config_to_settings_json(meta), Box::new(callback)))) - .expect("download_new_config send failed"); - return rx.recv().expect("download_new_config callback recv failed"); - } - Err(e) => { - log::error!("Cannot parse response from `{}`: {}", req_url, e) - } - } + match download_config(id) { + Ok(meta) => { + let (tx, rx) = mpsc::channel(); + let callback = + move |values: Vec| tx.send(values).expect("download_new_config callback send failed"); + sender2 + .lock() + .unwrap() + .send(ApiMessage::General(GeneralMessage::AddVariant(web_config_to_settings_json(meta), Box::new(callback)))) + .expect("download_new_config send failed"); + return rx.recv().expect("download_new_config callback recv failed"); + }, + Err(e) => { + log::error!("Invalid response from download: {}", e); } - Err(e) => log::warn!("Cannot get setting result from `{}`: {}", req_url, e), } vec![] } @@ -140,3 +210,36 @@ pub fn download_new_config(sender: Sender) -> impl AsyncCallable { }, } } + +/// Upload currently-loaded variant +pub fn upload_current_variant(sender: Sender) -> impl AsyncCallable { + let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety + let getter = move || { + let sender2 = sender.clone(); + move |(steam_id, steam_username): (String, String)| { + sender2 + .lock() + .unwrap() + .send(ApiMessage::UploadCurrentVariant(steam_id, steam_username)) + .expect("upload_current_variant send failed"); + true + } + }; + super::async_utils::AsyncIsh { + trans_setter: |params| { + if let Some(Primitive::String(steam_id)) = params.get(0) { + if let Some(Primitive::String(steam_username)) = params.get(1) { + Ok((steam_id.to_owned(), steam_username.to_owned())) + } else { + Err("upload_current_variant missing/invalid parameter 1".to_owned()) + } + } else { + Err("upload_current_variant missing/invalid parameter 0".to_owned()) + } + }, + set_get: getter, + trans_getter: |result| { + vec![result.into()] + }, + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 121630a..e39bec6 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -77,15 +77,17 @@ fn main() -> Result<(), ()> { let mut loaded_settings = persist::FileJson::open(utility::settings_dir().join(DEFAULT_SETTINGS_FILE)) .map(|mut file| file.variants.remove(&0) - .map(|settings| settings::Settings::from_json(DEFAULT_SETTINGS_NAME.into(), settings, DEFAULT_SETTINGS_FILE.into())) + .map(|settings| settings::Settings::from_json(DEFAULT_SETTINGS_NAME.into(), settings, DEFAULT_SETTINGS_FILE.into(), 0)) .unwrap_or_else(|| settings::Settings::system_default( DEFAULT_SETTINGS_FILE.into(), + 0, DEFAULT_SETTINGS_NAME.into(), 0, DEFAULT_SETTINGS_VARIANT_NAME.into()))) .unwrap_or_else(|_| { settings::Settings::system_default( DEFAULT_SETTINGS_FILE.into(), + 0, DEFAULT_SETTINGS_NAME.into(), 0, DEFAULT_SETTINGS_VARIANT_NAME.into(), @@ -320,6 +322,10 @@ fn main() -> Result<(), ()> { .register_async( "WEB_download_new", api::web::download_new_config(api_sender.clone()) + ) + .register_async( + "WEB_upload_new", + api::web::upload_current_variant(api_sender.clone()) ); utility::ioperm_power_ec(); diff --git a/backend/src/persist/file.rs b/backend/src/persist/file.rs index b6cdee2..c4a4010 100644 --- a/backend/src/persist/file.rs +++ b/backend/src/persist/file.rs @@ -9,6 +9,7 @@ use super::SettingsJson; pub struct FileJson { pub version: u64, pub name: String, + pub app_id: u64, pub variants: HashMap, } @@ -44,7 +45,7 @@ impl FileJson { .unwrap_or(0) } - pub fn update_variant_or_create>(path: P, mut setting: SettingsJson, given_name: String) -> Result { + pub fn update_variant_or_create>(path: P, app_id: u64, mut setting: SettingsJson, given_name: String) -> Result { if !setting.persistent { return Self::open(path) } @@ -62,6 +63,7 @@ impl FileJson { setting_variants.insert(setting.variant, setting); Self { version: 0, + app_id: app_id, name: given_name, variants: setting_variants, } diff --git a/backend/src/settings/detect/auto_detect.rs b/backend/src/settings/detect/auto_detect.rs index e7b8ded..9556d42 100644 --- a/backend/src/settings/detect/auto_detect.rs +++ b/backend/src/settings/detect/auto_detect.rs @@ -62,6 +62,7 @@ pub fn auto_detect_provider() -> DriverJson { let provider = auto_detect0( None, crate::utility::settings_dir().join("autodetect.json"), + 0, "".to_owned(), 0, crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(), @@ -76,6 +77,7 @@ pub fn auto_detect_provider() -> DriverJson { pub fn auto_detect0( settings_opt: Option<&SettingsJson>, json_path: std::path::PathBuf, + app_id: u64, name: String, variant_id: u64, variant_name: String, @@ -83,6 +85,7 @@ pub fn auto_detect0( let mut general_driver = Box::new(General { persistent: false, path: json_path, + app_id, name, variant_id, variant_name, diff --git a/backend/src/settings/driver.rs b/backend/src/settings/driver.rs index 1aa005e..6f94fff 100644 --- a/backend/src/settings/driver.rs +++ b/backend/src/settings/driver.rs @@ -13,14 +13,15 @@ impl Driver { name: String, settings: &SettingsJson, json_path: std::path::PathBuf, + app_id: u64, ) -> Self { let name_bup = settings.name.clone(); let id_bup = settings.variant; - auto_detect0(Some(settings), json_path, name, id_bup, name_bup) + auto_detect0(Some(settings), json_path, app_id, name, id_bup, name_bup) } - pub fn system_default(json_path: std::path::PathBuf, name: String, variant_id: u64, variant_name: String) -> Self { - auto_detect0(None, json_path, name, variant_id, variant_name) + pub fn system_default(json_path: std::path::PathBuf, app_id: u64, name: String, variant_id: u64, variant_name: String) -> Self { + auto_detect0(None, json_path, app_id, name, variant_id, variant_name) } } diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index 75167a8..d1d913a 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -32,6 +32,7 @@ impl std::fmt::Display for SettingVariant { pub struct General { pub persistent: bool, pub path: PathBuf, + pub app_id: u64, pub name: String, pub variant_id: u64, pub variant_name: String, @@ -73,6 +74,14 @@ impl TGeneral for General { self.path = path; } + fn app_id(&mut self) -> &'_ mut u64 { + &mut self.app_id + } + + fn get_app_id(&self) -> u64 { + self.app_id + } + fn get_name(&self) -> &'_ str { &self.name } @@ -108,7 +117,7 @@ impl TGeneral for General { fn add_variant(&self, variant: crate::persist::SettingsJson) -> Result, SettingError> { let variant_name = variant.name.clone(); - crate::persist::FileJson::update_variant_or_create(self.get_path(), variant, variant_name) + crate::persist::FileJson::update_variant_or_create(self.get_path(), self.get_app_id(), variant, variant_name) .map_err(|e| SettingError { msg: format!("failed to add variant: {}", e), setting: SettingVariant::General, @@ -173,8 +182,8 @@ impl OnSet for Settings { impl Settings { #[inline] - pub fn from_json(name: String, other: SettingsJson, json_path: PathBuf) -> Self { - let x = super::Driver::init(name, &other, json_path.clone()); + pub fn from_json(name: String, other: SettingsJson, json_path: PathBuf, app_id: u64) -> Self { + let x = super::Driver::init(name, &other, json_path.clone(), app_id); log::info!( "Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), @@ -190,8 +199,8 @@ impl Settings { } } - pub fn system_default(json_path: PathBuf, name: String, variant_id: u64, variant_name: String) -> Self { - let driver = super::Driver::system_default(json_path, name, variant_id, variant_name); + pub fn system_default(json_path: PathBuf, app_id: u64, name: String, variant_id: u64, variant_name: String) -> Self { + let driver = super::Driver::system_default(json_path, app_id, name, variant_id, variant_name); Self { general: driver.general, cpus: driver.cpus, @@ -201,7 +210,7 @@ impl Settings { } pub fn load_system_default(&mut self, name: String, variant_id: u64, variant_name: String) { - let driver = super::Driver::system_default(self.general.get_path().to_owned(), name, variant_id, variant_name); + let driver = super::Driver::system_default(self.general.get_path().to_owned(), self.general.get_app_id(), name, variant_id, variant_name); self.cpus = driver.cpus; self.gpu = driver.gpu; self.battery = driver.battery; @@ -222,6 +231,7 @@ impl Settings { pub fn load_file( &mut self, filename: PathBuf, + app_id: u64, name: String, variant: u64, variant_name: String, @@ -231,7 +241,7 @@ impl Settings { if json_path.exists() { if variant == u64::MAX { *self.general.persistent() = true; - let file_json = FileJson::update_variant_or_create(&json_path, self.json(), variant_name.clone()).map_err(|e| SettingError { + let file_json = FileJson::update_variant_or_create(&json_path, app_id, self.json(), variant_name.clone()).map_err(|e| SettingError { msg: format!("Failed to open settings {}: {}", json_path.display(), e), setting: SettingVariant::General, })?; @@ -252,7 +262,7 @@ impl Settings { *self.general.persistent() = false; self.general.name(name); } else { - let x = super::Driver::init(name, settings_json, json_path.clone()); + let x = super::Driver::init(name, settings_json, json_path.clone(), app_id); log::info!("Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), x.cpus.provider(), x.gpu.provider(), x.battery.provider()); self.general = x.general; self.cpus = x.cpus; @@ -270,6 +280,7 @@ impl Settings { } *self.general.persistent() = false; } + *self.general.app_id() = app_id; self.general.path(filename); self.general.variant_id(variant); Ok(*self.general.persistent()) diff --git a/backend/src/settings/traits.rs b/backend/src/settings/traits.rs index cacff0a..dd0fc69 100644 --- a/backend/src/settings/traits.rs +++ b/backend/src/settings/traits.rs @@ -105,6 +105,10 @@ pub trait TGeneral: OnSet + OnResume + OnPowerEvent + Debug + Send { fn path(&mut self, path: std::path::PathBuf); + fn app_id(&mut self) -> &'_ mut u64; + + fn get_app_id(&self) -> u64; + fn get_name(&self) -> &'_ str; fn name(&mut self, name: String); diff --git a/src/backend.ts b/src/backend.ts index 3f69d87..a04a79a 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -383,6 +383,10 @@ export async function storeDownloadById(id: string): Promise { return (await call_backend("WEB_download_new", [id])); } +export async function storeUpload(steam_id: string, steam_username: string): Promise { + return (await call_backend("WEB_upload_new", [steam_id, steam_username])); +} + export async function getAllSettingVariants(): Promise { console.log("GENERAL_get_all_variants"); return (await call_backend("GENERAL_get_all_variants", [])); diff --git a/src/components/battery.tsx b/src/components/battery.tsx index 0199e1b..59b476d 100644 --- a/src/components/battery.tsx +++ b/src/components/battery.tsx @@ -1,5 +1,5 @@ import { Fragment } from "react"; -import {Component} from "react"; +import { Component } from "react"; import { ToggleField, SliderField, diff --git a/src/consts.ts b/src/consts.ts index 415f5a5..8fc068a 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -35,6 +35,13 @@ export const CURRENT_VARIANT_GEN = "GENERAL_current_variant"; export const MESSAGE_LIST = "MESSAGE_messages"; +export const INTERNAL_STEAM_ID = "INTERNAL_steam_id"; +export const INTERNAL_STEAM_USERNAME = "INTERNAL_stream_username"; + +export const STORE_RESULTS = "INTERNAL_store_results"; + export const PERIODICAL_BACKEND_PERIOD = 5000; // milliseconds export const AUTOMATIC_REAPPLY_WAIT = 2000; // milliseconds +export const STORE_RESULTS_URI = "/plugins/PowerTools/settings_store"; + diff --git a/src/index.tsx b/src/index.tsx index 3fff822..3348742 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,6 +15,7 @@ import { Field, Dropdown, SingleDropdownOption, + Navigation, //NotchLabel //gamepadDialogClasses, //joinClassNames, @@ -22,6 +23,7 @@ import { import { VFC, useState } from "react"; import { GiDrill, GiTimeBomb, GiTimeTrap, GiDynamite } from "react-icons/gi"; import { HiRefresh, HiTrash, HiPlus, HiUpload } from "react-icons/hi"; +import { TbWorldPlus } from "react-icons/tb"; //import * as python from "./python"; import * as backend from "./backend"; @@ -63,6 +65,12 @@ import { MESSAGE_LIST, + INTERNAL_STEAM_ID, + INTERNAL_STEAM_USERNAME, + + STORE_RESULTS, + STORE_RESULTS_URI, + PERIODICAL_BACKEND_PERIOD, AUTOMATIC_REAPPLY_WAIT, } from "./consts"; @@ -73,10 +81,13 @@ import { Battery } from "./components/battery"; import { Cpus } from "./components/cpus"; import { DevMessages } from "./components/message"; +import { StoreResultsPage } from "./store/page"; + var periodicHook: NodeJS.Timeout | null = null; var lifetimeHook: any = null; var startHook: any = null; var endHook: any = null; +var userHook: any = null; var usdplReady = false; var tryNotifyProfileChange = function() {}; @@ -118,6 +129,10 @@ const reload = function() { console.debug("POWERTOOLS: got limits ", limits); }); + if (!get_value(STORE_RESULTS)) { + backend.resolve(backend.searchStoreByAppId(0), (results) => set_value(STORE_RESULTS, results)); + } + backend.resolve(backend.getBatteryCurrent(), (rate: number) => { set_value(CURRENT_BATT, rate) }); backend.resolve_nullable(backend.getBatteryChargeRate(), (rate: number | null) => { set_value(CHARGE_RATE_BATT, rate) }); backend.resolve_nullable(backend.getBatteryChargeMode(), (mode: string | null) => { set_value(CHARGE_MODE_BATT, mode) }); @@ -175,6 +190,7 @@ const clearHooks = function() { lifetimeHook?.unregister(); startHook?.unregister(); endHook?.unregister(); + userHook?.unregister(); backend.log(backend.LogLevel.Info, "Unregistered PowerTools callbacks, so long and thanks for all the fish."); }; @@ -209,7 +225,6 @@ const registerCallbacks = function(autoclear: boolean) { let appId = gameInfo.appid.toString(); backend.log(backend.LogLevel.Info, "RegisterForGameActionStart callback(" + actionType + ", " + id + ")"); - // don't use gameInfo.appid, haha backend.resolve( backend.loadGeneralSettings(appId, gameInfo.display_name, "0", undefined), (ok: boolean) => { @@ -221,6 +236,12 @@ const registerCallbacks = function(autoclear: boolean) { }); } ); + backend.resolve( + backend.searchStoreByAppId(appId), + (results: backend.StoreMetadata[]) => { + set_value(STORE_RESULTS, results); + } + ); }); // this fires immediately, so let's ignore that callback @@ -236,6 +257,20 @@ const registerCallbacks = function(autoclear: boolean) { setTimeout(() => backend.forceApplySettings(), AUTOMATIC_REAPPLY_WAIT); }); + //@ts-ignore + userHook = SteamClient.User.RegisterForCurrentUserChanges((data) => { + const accountName = data.strAccountName; + const steamId = data.strSteamID; + SteamClient.User.GetLoginUsers().then((users: any) => { + users.forEach((user: any) => { + if (user && user.accountName == accountName) { + set_value(INTERNAL_STEAM_ID, steamId); + set_value(INTERNAL_STEAM_USERNAME, user.personaName ? user.personaName : accountName); + } + }); + }); + }); + backend.log(backend.LogLevel.Debug, "Registered PowerTools callbacks, hello!"); }; @@ -373,7 +408,7 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { }}> = ({}) => { { - backend.log(backend.LogLevel.Debug, "Clicked on unimplemented upload button"); + const steamId = get_value(INTERNAL_STEAM_ID); + const steamName = get_value(INTERNAL_STEAM_USERNAME); + if (steamId && steamName) { + backend.storeUpload(steamId, steamName); + } else { + backend.log(backend.LogLevel.Warn, "Cannot upload with null steamID (is null: " + !steamId + ") and/or username (is null: " + !steamName + ")"); + } }} > + { + Navigation.Navigate(STORE_RESULTS_URI); + Navigation.CloseSideMenus(); + }} + > + + @@ -453,6 +507,7 @@ export default definePlugin((serverApi: ServerAPI) => { ico = ; } //registerCallbacks(false); + serverApi.routerHook.addRoute(STORE_RESULTS_URI, StoreResultsPage); return { title:
PowerTools
, content: , diff --git a/src/store/page.tsx b/src/store/page.tsx new file mode 100644 index 0000000..74926e2 --- /dev/null +++ b/src/store/page.tsx @@ -0,0 +1,48 @@ +import { Component, Fragment } from "react"; + +import * as backend from "../backend"; +import { tr } from "usdpl-front"; +import { get_value} from "usdpl-front"; + +import { + STORE_RESULTS, +} from "../consts"; + +export class StoreResultsPage extends Component { + constructor() { + super({}); + this.state = { + reloadThingy: "/shrug", + }; + } + + render() { + const storeItems = get_value(STORE_RESULTS) as backend.StoreMetadata[] | undefined; + console.log("POWERTOOLS: Rendering store results", storeItems); + if (storeItems) { + if (storeItems.length == 0) { + backend.log(backend.LogLevel.Warn, "No store results; got array with length 0 from cache"); + return (
+ { tr("No results") /* TODO translate */ } +
); + } else { + // TODO + return storeItems.map((meta: backend.StoreMetadata) => { +
+
{ meta.name }
+
{ tr("Created by") /* TODO translate */} { meta.steam_username }
+
{ meta.tags.map((tag: string) => {tag}) }
+ Hey NG you should finish this page +
+ }); + } + + } else { + backend.log(backend.LogLevel.Warn, "Store failed to load; got null from cache"); + // store did not pre-load when the game started + return ( + { tr("Store failed to load") /* TODO translate */ } + ); + } + } +} From a1c44cdea7ae27f7a09fd194c580c79b291d25d1 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 27 Jan 2024 18:44:43 -0500 Subject: [PATCH 27/56] Improve memory clock selection for #140, fix dpm_performance enforcement check for GPU --- .../community_settings_core/src/v1/setting.rs | 2 +- backend/limits_core/src/json_v2/base.rs | 50 +++--- backend/limits_core/src/json_v2/gpu_limit.rs | 19 +++ backend/src/api/api_types.rs | 3 +- backend/src/api/gpu.rs | 33 +++- backend/src/api/handler.rs | 10 +- backend/src/api/web.rs | 4 +- backend/src/main.rs | 4 + backend/src/persist/gpu.rs | 4 +- backend/src/settings/detect/auto_detect.rs | 10 ++ backend/src/settings/generic/gpu.rs | 11 +- backend/src/settings/generic_amd/gpu.rs | 11 +- backend/src/settings/steam_deck/gpu.rs | 161 +++++++++++++----- .../settings/steam_deck/power_dpm_force.rs | 2 +- backend/src/settings/traits.rs | 4 +- backend/src/settings/unknown/gpu.rs | 17 +- src/backend.ts | 13 +- src/components/gpu.tsx | 38 ++++- src/index.tsx | 2 +- 19 files changed, 285 insertions(+), 113 deletions(-) diff --git a/backend/community_settings_core/src/v1/setting.rs b/backend/community_settings_core/src/v1/setting.rs index 259a7b5..8d2ea03 100644 --- a/backend/community_settings_core/src/v1/setting.rs +++ b/backend/community_settings_core/src/v1/setting.rs @@ -43,5 +43,5 @@ pub struct Gpu { pub tdp: Option, pub tdp_boost: Option, pub clock_limits: Option>, - pub slow_memory: bool, + pub memory_clock: Option, } diff --git a/backend/limits_core/src/json_v2/base.rs b/backend/limits_core/src/json_v2/base.rs index 23c0717..57abec8 100644 --- a/backend/limits_core/src/json_v2/base.rs +++ b/backend/limits_core/src/json_v2/base.rs @@ -16,35 +16,11 @@ impl Default for Base { fn default() -> Self { Base { configs: vec![ - super::Config { - name: "Steam Deck Custom".to_owned(), - conditions: super::Conditions { - dmi: None, - cpuinfo: Some("model name\t: AMD Custom APU (0405)|(0932)\n".to_owned()), - os: None, - command: None, - file_exists: Some("./limits_override.json".into()), - }, - limits: super::Limits { - cpu: super::Limit { - provider: super::CpuLimitType::SteamDeckAdvance, - limits: super::GenericCpusLimit::default_for(super::CpuLimitType::SteamDeckAdvance), - }, - gpu: super::Limit { - provider: super::GpuLimitType::SteamDeckAdvance, - limits: super::GenericGpuLimit::default_for(super::GpuLimitType::SteamDeckAdvance), - }, - battery: super::Limit { - provider: super::BatteryLimitType::SteamDeckAdvance, - limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::SteamDeckAdvance), - }, - } - }, super::Config { name: "Steam Deck".to_owned(), conditions: super::Conditions { dmi: None, - cpuinfo: Some("model name\t: AMD Custom APU (0405)|(0932)\n".to_owned()), + cpuinfo: Some("model name\t: AMD Custom APU 0405\n".to_owned()), os: None, command: None, file_exists: None, @@ -64,6 +40,30 @@ impl Default for Base { }, } }, + super::Config { + name: "Steam Deck OLED".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: Some("model name\t: AMD Custom APU 0932\n".to_owned()), + os: None, + command: None, + file_exists: None, + }, + limits: super::Limits { + cpu: super::Limit { + provider: super::CpuLimitType::SteamDeck, + limits: super::GenericCpusLimit::default_for(super::CpuLimitType::SteamDeck), + }, + gpu: super::Limit { + provider: super::GpuLimitType::SteamDeck, + limits: super::GenericGpuLimit::default_for(super::GpuLimitType::SteamDeckOLED), + }, + battery: super::Limit { + provider: super::BatteryLimitType::SteamDeck, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::SteamDeck), + }, + } + }, super::Config { name: "AMD R3 2300U".to_owned(), conditions: super::Conditions { diff --git a/backend/limits_core/src/json_v2/gpu_limit.rs b/backend/limits_core/src/json_v2/gpu_limit.rs index 32e47f6..39643ff 100644 --- a/backend/limits_core/src/json_v2/gpu_limit.rs +++ b/backend/limits_core/src/json_v2/gpu_limit.rs @@ -8,6 +8,8 @@ pub enum GpuLimitType { SteamDeck, #[serde(rename = "GabeBoyAdvance", alias = "SteamDeckAdvance")] SteamDeckAdvance, + #[serde(rename = "GabeBoy101", alias = "SteamDeckOLED")] + SteamDeckOLED, Generic, GenericAMD, Unknown, @@ -27,6 +29,8 @@ pub struct GenericGpuLimit { pub clock_min: Option>, pub clock_max: Option>, pub clock_step: Option, + pub memory_clock: Option>, + pub memory_clock_step: Option, pub skip_resume_reclock: bool, } @@ -34,6 +38,7 @@ impl GenericGpuLimit { pub fn default_for(t: GpuLimitType) -> Self { match t { GpuLimitType::SteamDeck | GpuLimitType::SteamDeckAdvance => Self::default_steam_deck(), + GpuLimitType::SteamDeckOLED => Self::default_steam_deck_oled(), _t => Self::default(), } } @@ -64,10 +69,24 @@ impl GenericGpuLimit { max: Some(1600), }), clock_step: Some(100), + // Disabled for now since LCD version is a bit broken on sysfs right now + /*memory_clock: Some(RangeLimit { + min: Some(400), + max: Some(800), + }), + memory_clock_step: Some(400),*/ + memory_clock: None, + memory_clock_step: None, skip_resume_reclock: false, } } + fn default_steam_deck_oled() -> Self { + let mut sd = Self::default_steam_deck(); + sd.memory_clock_step = Some(200); + sd + } + pub fn apply_override(&mut self, limit_override: Self) { if let Some(range) = limit_override.fast_ppt { if range.min.is_none() && range.max.is_none() { diff --git a/backend/src/api/api_types.rs b/backend/src/api/api_types.rs index cf889d3..8854656 100644 --- a/backend/src/api/api_types.rs +++ b/backend/src/api/api_types.rs @@ -59,7 +59,8 @@ pub struct GpuLimits { pub clock_min_limits: Option>, pub clock_max_limits: Option>, pub clock_step: u64, - pub memory_control_capable: bool, + pub memory_control: Option>, + pub memory_step: u64, } #[derive(Serialize, Deserialize)] diff --git a/backend/src/api/gpu.rs b/backend/src/api/gpu.rs index 320a331..f8b178d 100644 --- a/backend/src/api/gpu.rs +++ b/backend/src/api/gpu.rs @@ -160,17 +160,17 @@ pub fn set_slow_memory( sender: Sender, ) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety - let setter = move |value: bool| { + let setter = move |value: u64| { sender .lock() .unwrap() - .send(ApiMessage::Gpu(GpuMessage::SetSlowMemory(value))) + .send(ApiMessage::Gpu(GpuMessage::SetMemoryClock(Some(value)))) .expect("unset_clock_limits send failed") }; move |params_in: super::ApiParameterType| { - if let Some(&Primitive::Bool(memory_is_slow)) = params_in.get(0) { - setter(memory_is_slow); - vec![memory_is_slow.into()] + if let Some(&Primitive::F64(mem_clock)) = params_in.get(0) { + setter(mem_clock as _); + vec![mem_clock.into()] } else { vec!["set_slow_memory missing parameter 0".into()] } @@ -183,14 +183,14 @@ pub fn get_slow_memory(sender: Sender) -> impl AsyncCallable { let sender2 = sender.clone(); move || { let (tx, rx) = mpsc::channel(); - let callback = move |value: bool| { + let callback = move |value: Option| { tx.send(value) .expect("get_slow_memory callback send failed") }; sender2 .lock() .unwrap() - .send(ApiMessage::Gpu(GpuMessage::GetSlowMemory(Box::new( + .send(ApiMessage::Gpu(GpuMessage::GetMemoryClock(Box::new( callback, )))) .expect("get_slow_memory send failed"); @@ -199,6 +199,23 @@ pub fn get_slow_memory(sender: Sender) -> impl AsyncCallable { }; super::async_utils::AsyncIshGetter { set_get: getter, - trans_getter: |value: bool| vec![value.into()], + trans_getter: |value: Option| vec![super::utility::map_optional(value)], + } +} + +pub fn unset_slow_memory( + sender: Sender, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety + let setter = move || { + sender + .lock() + .unwrap() + .send(ApiMessage::Gpu(GpuMessage::SetMemoryClock(None))) + .expect("unset_slow_memory send failed") + }; + move |_: super::ApiParameterType| { + setter(); + vec![true.into()] } } diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index 62c433f..f07ce58 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -198,8 +198,8 @@ pub enum GpuMessage { GetPpt(Callback<(Option, Option)>), SetClockLimits(Option>), GetClockLimits(Callback>>), - SetSlowMemory(bool), - GetSlowMemory(Callback), + SetMemoryClock(Option), + GetMemoryClock(Callback>), } impl GpuMessage { @@ -210,8 +210,8 @@ impl GpuMessage { Self::GetPpt(cb) => cb(settings.get_ppt()), Self::SetClockLimits(clocks) => settings.clock_limits(clocks), Self::GetClockLimits(cb) => cb(settings.get_clock_limits().map(|x| x.to_owned())), - Self::SetSlowMemory(val) => *settings.slow_memory() = val, - Self::GetSlowMemory(cb) => cb(*settings.slow_memory()), + Self::SetMemoryClock(val) => settings.memory_clock(val), + Self::GetMemoryClock(cb) => cb(settings.get_memory_clock()), } dirty } @@ -219,7 +219,7 @@ impl GpuMessage { fn is_modify(&self) -> bool { matches!( self, - Self::SetPpt(_, _) | Self::SetClockLimits(_) | Self::SetSlowMemory(_) + Self::SetPpt(_, _) | Self::SetClockLimits(_) | Self::SetMemoryClock(_) ) } } diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index 08b9eab..81d5cb0 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -70,7 +70,7 @@ fn web_config_to_settings_json(meta: community_settings_core::v1::Metadata) -> c min: lim.min, max: lim.max, }), - slow_memory: meta.config.gpu.slow_memory, + memory_clock: meta.config.gpu.memory_clock, root: None, }, battery: crate::persist::BatteryJson { @@ -138,7 +138,7 @@ fn settings_to_web_config(app_id: u32, user_id: u64, username: String, settings: min: lim.min, max: lim.max, }), - slow_memory: settings.gpu.slow_memory, + memory_clock: settings.gpu.memory_clock, }, battery: community_settings_core::v1::Battery { charge_rate: settings.battery.charge_rate, diff --git a/backend/src/main.rs b/backend/src/main.rs index e39bec6..f2f683b 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -243,6 +243,10 @@ fn main() -> Result<(), ()> { "GPU_get_slow_memory", api::gpu::get_slow_memory(api_sender.clone()), ) + .register( + "GPU_unset_slow_memory", + api::gpu::unset_slow_memory(api_sender.clone()), + ) // general API functions .register( "GENERAL_set_persistent", diff --git a/backend/src/persist/gpu.rs b/backend/src/persist/gpu.rs index a91f9de..57df0f2 100644 --- a/backend/src/persist/gpu.rs +++ b/backend/src/persist/gpu.rs @@ -11,7 +11,7 @@ pub struct GpuJson { pub tdp: Option, pub tdp_boost: Option, pub clock_limits: Option>, - pub slow_memory: bool, + pub memory_clock: Option, #[serde(skip_serializing_if = "Option::is_none")] pub root: Option, } @@ -24,7 +24,7 @@ impl Default for GpuJson { tdp: None, tdp_boost: None, clock_limits: None, - slow_memory: false, + memory_clock: None, root: None, } } diff --git a/backend/src/settings/detect/auto_detect.rs b/backend/src/settings/detect/auto_detect.rs index 9556d42..d21e502 100644 --- a/backend/src/settings/detect/auto_detect.rs +++ b/backend/src/settings/detect/auto_detect.rs @@ -203,6 +203,13 @@ pub fn auto_detect0( relevant_limits.gpu.limits, )) } + GpuLimitType::SteamDeckOLED => { + Box::new(crate::settings::steam_deck::Gpu::from_json_and_limits( + settings.gpu.clone(), + settings.version, + relevant_limits.gpu.limits, + )) + } GpuLimitType::Generic => { Box::new(crate::settings::generic::Gpu::from_json_and_limits( settings.gpu.clone(), @@ -289,6 +296,9 @@ pub fn auto_detect0( GpuLimitType::SteamDeckAdvance => { Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits)) } + GpuLimitType::SteamDeckOLED => { + Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits)) + } GpuLimitType::Generic => { Box::new(crate::settings::generic::Gpu::from_limits(relevant_limits.gpu.limits)) } diff --git a/backend/src/settings/generic/gpu.rs b/backend/src/settings/generic/gpu.rs index 27c4322..3e2b5cf 100644 --- a/backend/src/settings/generic/gpu.rs +++ b/backend/src/settings/generic/gpu.rs @@ -111,7 +111,7 @@ impl Into for Gpu { tdp: self.tdp, tdp_boost: self.tdp_boost, clock_limits: self.clock_limits.map(|x| x.into()), - slow_memory: false, + memory_clock: None, root: self.sysfs.root().and_then(|p| p.as_ref().to_str().map(|s| s.to_owned())) } } @@ -177,7 +177,8 @@ impl TGpu for Gpu { .clone() .map(|x| RangeLimit::new(x.min.unwrap_or(0), x.max.unwrap_or(3_000))), clock_step: self.limits.clock_step.unwrap_or(100), - memory_control_capable: false, + memory_control: None, + memory_step: 100, } } @@ -227,8 +228,10 @@ impl TGpu for Gpu { self.clock_limits.as_ref() } - fn slow_memory(&mut self) -> &mut bool { - &mut self.slow_memory + fn memory_clock(&mut self, _speed: Option) {} + + fn get_memory_clock(&self) -> Option { + None } fn provider(&self) -> crate::persist::DriverJson { diff --git a/backend/src/settings/generic_amd/gpu.rs b/backend/src/settings/generic_amd/gpu.rs index baf1508..0a1331a 100644 --- a/backend/src/settings/generic_amd/gpu.rs +++ b/backend/src/settings/generic_amd/gpu.rs @@ -299,7 +299,8 @@ fn bad_gpu_limits() -> crate::api::GpuLimits { clock_min_limits: None, clock_max_limits: None, clock_step: 100, - memory_control_capable: false, + memory_control: None, + memory_step: 400, } } @@ -333,8 +334,12 @@ impl TGpu for Gpu { self.generic.get_clock_limits() } - fn slow_memory(&mut self) -> &mut bool { - self.generic.slow_memory() + fn memory_clock(&mut self, speed: Option) { + self.generic.memory_clock(speed) + } + + fn get_memory_clock(&self) -> Option { + self.generic.get_memory_clock() } fn provider(&self) -> crate::persist::DriverJson { diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index 7b06454..fe918eb 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -1,6 +1,6 @@ use std::convert::Into; -use sysfuss::{BasicEntityPath, HwMonPath, SysEntity, capability::attributes, SysEntityAttributesExt, SysAttribute}; +use sysfuss::{BasicEntityPath, HwMonPath, SysEntity, capability::attributes, SysEntityAttributes, SysEntityAttributesExt, SysAttribute}; use limits_core::json_v2::GenericGpuLimit; @@ -20,7 +20,7 @@ pub struct Gpu { pub fast_ppt: Option, pub slow_ppt: Option, pub clock_limits: Option>, - pub slow_memory: bool, + pub memory_clock: Option, limits: GenericGpuLimit, state: crate::state::steam_deck::Gpu, sysfs_card: BasicEntityPath, @@ -47,6 +47,8 @@ enum ClockType { const MAX_CLOCK: u64 = 1600; const MIN_CLOCK: u64 = 200; +const MAX_MEMORY_CLOCK: u64 = 800; +const MIN_MEMORY_CLOCK: u64 = 400; const MAX_FAST_PPT: u64 = 30_000_000; const MIN_FAST_PPT: u64 = 1_000_000; const MAX_SLOW_PPT: u64 = 29_000_000; @@ -108,6 +110,63 @@ impl Gpu { }) } + fn is_memory_clock_maxed(&self) -> bool { + if let Some(clock) = &self.memory_clock { + if let Some(limit) = &self.limits.memory_clock { + if let Some(limit) = &limit.max { + if let Some(step) = &self.limits.memory_clock_step { + log::debug!("chosen_clock: {}, limit_clock: {}, step: {}", clock, limit, step); + return clock > &(limit - step); + } else { + log::debug!("chosen_clock: {}, limit_clock: {}", clock, limit); + return clock == limit; + } + } + } + } + true + } + + fn quantize_memory_clock(&self, clock: u64) -> u64 { + if let Ok(f) = self.sysfs_card.read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) { + let options = parse_pp_dpm_fclk(&String::from_utf8_lossy(&f)); + // round (and find) nearest valid clock step + // roughly price is right strategy (clock step will always be lower or equal to chosen) + for i in 0..options.len() { + let (current_val_opt, current_speed_opt) = &options[i]; + let current_speed_opt = *current_speed_opt as u64; + if clock == current_speed_opt { + return *current_val_opt as _; + } else if current_speed_opt > clock { + if i == 0 { + return *current_val_opt as _; + } else { + return options[i-1].0 as _; + } + } + } + options[options.len() - 1].0 as _ + } else { + self.is_memory_clock_maxed() as u64 + } + } + + fn build_memory_clock_payload(&self, clock: u64) -> String { + let max_val = self.quantize_memory_clock(clock); + match max_val { + 0 => "0\n".to_owned(), + max_val => { + use std::fmt::Write; + let mut payload = String::from("0"); + for i in 1..max_val { + write!(payload, " {}", i).expect("Failed to write to memory payload (should be infallible!?)"); + } + write!(payload, " {}\n", max_val).expect("Failed to write to memory payload (should be infallible!?)"); + payload + } + } + } + fn set_clocks(&mut self) -> Result<(), Vec> { let mut errors = Vec::new(); if let Some(clock_limits) = &self.clock_limits { @@ -130,7 +189,7 @@ impl Gpu { || POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.needs_manual() { self.state.clock_limits_set = false; - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_gpu(self.slow_memory); + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_gpu(!self.is_memory_clock_maxed()); if POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.needs_manual() { POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.enforce_level(&self.sysfs_card)?; // disable manual clock limits @@ -155,47 +214,36 @@ impl Gpu { } } - fn set_slow_memory(&self, slow: bool) -> Result<(), SettingError> { + fn set_memory_speed(&self, clock: u64) -> Result<(), SettingError> { let path = GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.path(&self.sysfs_card); - if slow { - self.sysfs_card.set(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned(), slow as u8).map_err(|e| { - SettingError { - msg: format!("Failed to write to `{}`: {}", path.display(), e), - setting: crate::settings::SettingVariant::Gpu, - } - }) - } else { - // NOTE: there is a GPU driver/hardware bug that prevents this from working - self.sysfs_card.set(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned(), "0 1\n").map_err(|e| { - SettingError { - msg: format!("Failed to write to `{}`: {}", path.display(), e), - setting: crate::settings::SettingVariant::Gpu, - } - }) - } + let payload = self.build_memory_clock_payload(clock); + log::debug!("Generated payload for gpu fclk (memory): `{}` (is maxed? {})", payload, self.is_memory_clock_maxed()); + self.sysfs_card.set(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned(), payload).map_err(|e| { + SettingError { + msg: format!("Failed to write to `{}`: {}", path.display(), e), + setting: crate::settings::SettingVariant::Gpu, + } + }) } fn set_force_performance_related(&mut self) -> Result<(), Vec> { let mut errors = Vec::new(); + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_gpu(!self.is_memory_clock_maxed() || self.clock_limits.is_some()); + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT + .enforce_level(&self.sysfs_card) + .unwrap_or_else(|mut e| errors.append(&mut e)); // enable/disable downclock of GPU memory (to 400Mhz?) - if self.slow_memory { - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_gpu(true); - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT - .enforce_level(&self.sysfs_card) - .unwrap_or_else(|mut e| errors.append(&mut e)); - self.set_slow_memory(self.slow_memory).unwrap_or_else(|e| errors.push(e)); - } else if POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.needs_manual() { - self.set_slow_memory(self.slow_memory).unwrap_or_else(|e| errors.push(e)); - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_gpu(self.clock_limits.is_some()); - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT - .enforce_level(&self.sysfs_card) - .unwrap_or_else(|mut e| errors.append(&mut e)); - } + self.set_memory_speed( + self.memory_clock + .or_else(|| self.limits.memory_clock + .map(|lim| lim.max.unwrap_or(MAX_MEMORY_CLOCK)) + ).unwrap_or(MAX_MEMORY_CLOCK) + ).unwrap_or_else(|e| errors.push(e)); self.set_clocks() .unwrap_or_else(|mut e| errors.append(&mut e)); // commit changes (if no errors have already occured) if errors.is_empty() { - if self.slow_memory || self.clock_limits.is_some() { + if !self.is_memory_clock_maxed() || self.clock_limits.is_some() { self.set_confirm().map_err(|e| { errors.push(e); errors @@ -294,6 +342,9 @@ impl Gpu { Some(max.clamp(self.limits.clock_max.and_then(|lim| lim.min).unwrap_or(MIN_CLOCK), self.limits.clock_max.and_then(|lim| lim.max).unwrap_or(MAX_CLOCK))); } } + if let Some(mem_clock) = self.memory_clock { + self.memory_clock = Some(mem_clock.clamp(self.limits.memory_clock.and_then(|lim| lim.min).unwrap_or(MIN_MEMORY_CLOCK), self.limits.memory_clock.and_then(|lim| lim.max).unwrap_or(MAX_MEMORY_CLOCK))); + } } } @@ -306,7 +357,7 @@ impl Into for Gpu { tdp: None, tdp_boost: None, clock_limits: self.clock_limits.map(|x| x.into()), - slow_memory: self.slow_memory, + memory_clock: self.memory_clock, root: self.sysfs_card.root().or(self.sysfs_hwmon.root()).and_then(|p| p.as_ref().to_str().map(|r| r.to_owned())) } } @@ -319,7 +370,7 @@ impl ProviderBuilder for Gpu { fast_ppt: persistent.fast_ppt, slow_ppt: persistent.slow_ppt, clock_limits: persistent.clock_limits.map(|x| min_max_from_json(x, version)), - slow_memory: persistent.slow_memory, + memory_clock: persistent.memory_clock, limits: limits, state: crate::state::steam_deck::Gpu::default(), sysfs_card: Self::find_card_sysfs(persistent.root.clone()), @@ -329,7 +380,7 @@ impl ProviderBuilder for Gpu { fast_ppt: persistent.fast_ppt, slow_ppt: persistent.slow_ppt, clock_limits: persistent.clock_limits.map(|x| min_max_from_json(x, version)), - slow_memory: persistent.slow_memory, + memory_clock: persistent.memory_clock, limits: limits, state: crate::state::steam_deck::Gpu::default(), sysfs_card: Self::find_card_sysfs(persistent.root.clone()), @@ -343,7 +394,7 @@ impl ProviderBuilder for Gpu { fast_ppt: None, slow_ppt: None, clock_limits: None, - slow_memory: false, + memory_clock: None, limits: limits, state: crate::state::steam_deck::Gpu::default(), sysfs_card: Self::find_card_sysfs(None::<&'static str>), @@ -393,7 +444,11 @@ impl TGpu for Gpu { max: super::util::range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), }), clock_step: self.limits.clock_step.unwrap_or(100), - memory_control_capable: true, + memory_control: Some(RangeLimit { + min: super::util::range_min_or_fallback(&self.limits.memory_clock, MIN_MEMORY_CLOCK), + max: super::util::range_max_or_fallback(&self.limits.memory_clock, MAX_MEMORY_CLOCK), + }), + memory_step: self.limits.memory_clock_step.unwrap_or(400), } } @@ -421,11 +476,35 @@ impl TGpu for Gpu { self.clock_limits.as_ref() } - fn slow_memory(&mut self) -> &mut bool { - &mut self.slow_memory + fn memory_clock(&mut self, speed: Option) { + self.memory_clock = speed; + } + + fn get_memory_clock(&self) -> Option { + self.memory_clock } fn provider(&self) -> crate::persist::DriverJson { crate::persist::DriverJson::SteamDeck } } + +fn parse_pp_dpm_fclk(s: &str) -> Vec<(usize, usize)> { // (value, MHz) + let mut result = Vec::new(); + for line in s.split('\n') { + if !line.is_empty() { + if let Some((val, freq_mess)) = line.split_once(':') { + if let Ok(val) = val.parse::() { + if let Some((freq, _unit)) = freq_mess.trim().split_once(|c: char| !c.is_digit(10)) { + if let Ok(freq) = freq.parse::() { + result.push((val, freq)); + } + } + } + } + } else { + break; + } + } + result +} diff --git a/backend/src/settings/steam_deck/power_dpm_force.rs b/backend/src/settings/steam_deck/power_dpm_force.rs index 72404cc..d804a18 100644 --- a/backend/src/settings/steam_deck/power_dpm_force.rs +++ b/backend/src/settings/steam_deck/power_dpm_force.rs @@ -96,7 +96,7 @@ impl PDFPLManager { if let Ok(mode_now) = entity.attribute::(DPM_FORCE_LIMITS_ATTRIBUTE.to_owned()) { - log::debug!("Mode for `{}` is now `{}`", path.display(), mode_now); + log::debug!("Mode for `{}` is now `{}` ({:#b})", path.display(), mode_now, self.get()); } else { log::debug!("Error getting new mode for debugging purposes"); } diff --git a/backend/src/settings/traits.rs b/backend/src/settings/traits.rs index dd0fc69..5f993b2 100644 --- a/backend/src/settings/traits.rs +++ b/backend/src/settings/traits.rs @@ -59,7 +59,9 @@ pub trait TGpu: OnSet + OnResume + OnPowerEvent + Debug + Send { fn get_clock_limits(&self) -> Option<&MinMax>; - fn slow_memory(&mut self) -> &mut bool; + fn memory_clock(&mut self, speed: Option); + + fn get_memory_clock(&self) -> Option; fn provider(&self) -> crate::persist::DriverJson { crate::persist::DriverJson::AutoDetect diff --git a/backend/src/settings/unknown/gpu.rs b/backend/src/settings/unknown/gpu.rs index f7b5b36..c676cda 100644 --- a/backend/src/settings/unknown/gpu.rs +++ b/backend/src/settings/unknown/gpu.rs @@ -8,13 +8,11 @@ use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError}; #[derive(Debug, Clone)] -pub struct Gpu { - slow_memory: bool, // ignored -} +pub struct Gpu {} impl Gpu { pub fn system_default() -> Self { - Self { slow_memory: false } + Self { } } } @@ -37,7 +35,7 @@ impl Into for Gpu { tdp: None, tdp_boost: None, clock_limits: None, - slow_memory: false, + memory_clock: None, root: None, } } @@ -69,7 +67,8 @@ impl TGpu for Gpu { clock_min_limits: None, clock_max_limits: None, clock_step: 100, - memory_control_capable: false, + memory_control: None, + memory_step: 400, } } @@ -89,8 +88,10 @@ impl TGpu for Gpu { None } - fn slow_memory(&mut self) -> &mut bool { - &mut self.slow_memory + fn memory_clock(&mut self, _speed: Option) {} + + fn get_memory_clock(&self) -> Option { + None } fn provider(&self) -> crate::persist::DriverJson { diff --git a/src/backend.ts b/src/backend.ts index a04a79a..bff625a 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -90,7 +90,8 @@ export type GpuLimits = { clock_min_limits: RangeLimit | null; clock_max_limits: RangeLimit | null; clock_step: number; - memory_control_capable: boolean; + memory_control: RangeLimit | null, + memory_step: number, }; // API @@ -229,14 +230,18 @@ export async function unsetGpuClockLimits(): Promise { return (await call_backend("GPU_unset_clock_limits", [])); } -export async function setGpuSlowMemory(val: boolean): Promise { - return (await call_backend("GPU_set_slow_memory", [val]))[0]; +export async function setGpuSlowMemory(clock: number): Promise { + return (await call_backend("GPU_set_slow_memory", [clock]))[0]; } -export async function getGpuSlowMemory(): Promise { +export async function getGpuSlowMemory(): Promise { return (await call_backend("GPU_get_slow_memory", []))[0]; } +export async function unsetGpuSlowMemory(): Promise { + return (await call_backend("GPU_unset_slow_memory", [])); +} + // general export async function setGeneralPersistent(val: boolean): Promise { diff --git a/src/components/gpu.tsx b/src/components/gpu.tsx index a8fdd52..fb8dfc6 100644 --- a/src/components/gpu.tsx +++ b/src/components/gpu.tsx @@ -180,16 +180,42 @@ export class Gpu extends Component { }} />} - {(get_value(LIMITS_INFO) as backend.SettingsLimits).gpu.memory_control_capable && + {((get_value(LIMITS_INFO) as backend.SettingsLimits).gpu.memory_control) && { - backend.resolve(backend.setGpuSlowMemory(value), (val: boolean) => { - set_value(SLOW_MEMORY_GPU, val); - reloadGUI("GPUSlowMemory"); - }) + if (value) { + set_value(SLOW_MEMORY_GPU, (get_value(LIMITS_INFO) as backend.SettingsLimits).gpu.memory_control!.max); + reloadGUI("GPUMemFreqToggle"); + } else { + set_value(SLOW_MEMORY_GPU, null); + backend.resolve(backend.unsetGpuSlowMemory(), (_: any[]) => { + reloadGUI("GPUUnsetMemFreq"); + }); + } + }} + /> + } + {get_value(SLOW_MEMORY_GPU) != null && + { + backend.log(backend.LogLevel.Debug, "GPU memory clock Max is now " + val.toString()); + backend.resolve( + backend.setGpuSlowMemory(val), + (val: number) => { + set_value(SLOW_MEMORY_GPU, val); + reloadGUI("GPUSetMemFreq"); + } + ); }} /> } diff --git a/src/index.tsx b/src/index.tsx index 3348742..1c8bef7 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -170,7 +170,7 @@ const reload = function() { set_value(CLOCK_MIN_GPU, limits[0]); set_value(CLOCK_MAX_GPU, limits[1]); }); - backend.resolve(backend.getGpuSlowMemory(), (status: boolean) => { set_value(SLOW_MEMORY_GPU, status) }); + backend.resolve(backend.getGpuSlowMemory(), (status: number) => { set_value(SLOW_MEMORY_GPU, status) }); backend.resolve(backend.getGeneralPersistent(), (value: boolean) => { set_value(PERSISTENT_GEN, value) }); backend.resolve(backend.getGeneralSettingsName(), (name: string) => { set_value(NAME_GEN, name) }); From 88d359e286bec2fdabde0f4f72b52fb8f4d5200e Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 27 Jan 2024 20:44:36 -0500 Subject: [PATCH 28/56] Remove legacy charge mode and level functionality --- backend/src/api/handler.rs | 2 +- backend/src/settings/steam_deck/battery.rs | 239 +++++++++------------ 2 files changed, 104 insertions(+), 137 deletions(-) diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index f07ce58..95fba2c 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -66,7 +66,7 @@ impl BatteryMessage { /// Message instructs the driver to modify settings fn is_modify(&self) -> bool { - matches!(self, Self::SetChargeRate(_) | Self::SetChargeMode(_)) + matches!(self, Self::SetChargeRate(_) | Self::SetChargeMode(_) | Self::SetChargeLimit(_)) } } diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index 12514aa..e36b127 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -16,6 +16,7 @@ use crate::settings::{OnPowerEvent, OnResume, OnSet, PowerMode, SettingError}; pub struct Battery { pub charge_rate: Option, pub charge_mode: Option, + pub charge_limit: Option, events: Vec, limits: GenericBatteryLimit, state: crate::state::steam_deck::Battery, @@ -93,6 +94,7 @@ impl EventInstruction { match mode { EventTrigger::PluggedIn => "plug-in".to_owned(), EventTrigger::PluggedOut => "plug-out".to_owned(), + // EventInstruction uses 1.0 to represent full, but Strings use 100.0 EventTrigger::BatteryAbove(x) => format!(">{:#0.2}", x * 100.0), EventTrigger::BatteryBelow(x) => format!("<{:#0.2}", x * 100.0), EventTrigger::Ignored => "/shrug".to_owned(), @@ -146,14 +148,9 @@ impl EventInstruction { fn set_charge_rate(&self) -> Result<(), SettingError> { if let Some(charge_rate) = self.charge_rate { - let attr = if MAX_BATTERY_CHARGE_RATE_ATTR.exists(&*self.sysfs_hwmon) { - MAX_BATTERY_CHARGE_RATE_ATTR - } else { - MAXIMUM_BATTERY_CHARGE_RATE_ATTR - }; - self.sysfs_hwmon.set(attr, charge_rate).map_err( + self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_RATE_ATTR, charge_rate).map_err( |e| SettingError { - msg: format!("Failed to write to `{:?}`: {}", attr, e), + msg: format!("Failed to write to `{:?}`: {}", MAX_BATTERY_CHARGE_RATE_ATTR, e), setting: crate::settings::SettingVariant::Battery, }, ) @@ -215,7 +212,6 @@ const HWMON_NEEDS: &[HwMonAttribute] = &[ //HwMonAttribute::custom("maximum_battery_charge_rate"), // NOTE: Cannot filter by custom capabilities ]; -const MAXIMUM_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute = HwMonAttribute::custom("maximum_battery_charge_rate"); const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute = HwMonAttribute::custom("maximum_battery_charge_rate"); const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute = HwMonAttribute::custom("max_battery_charge_level"); @@ -297,13 +293,8 @@ impl Battery { fn set_charge_rate(&mut self) -> Result<(), SettingError> { if let Some(charge_rate) = self.charge_rate { self.state.charge_rate_set = true; - let attr = if MAX_BATTERY_CHARGE_RATE_ATTR.exists(&*self.sysfs_hwmon) { - MAX_BATTERY_CHARGE_RATE_ATTR - } else { - MAXIMUM_BATTERY_CHARGE_RATE_ATTR - }; - let path = attr.path(&*self.sysfs_hwmon); - self.sysfs_hwmon.set(attr, charge_rate).map_err( + let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon); + self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_RATE_ATTR, charge_rate).map_err( |e| SettingError { msg: format!("Failed to write to `{}`: {}", path.display(), e), setting: crate::settings::SettingVariant::Battery, @@ -311,13 +302,8 @@ impl Battery { ) } else if self.state.charge_rate_set { self.state.charge_rate_set = false; - let attr = if MAX_BATTERY_CHARGE_RATE_ATTR.exists(&*self.sysfs_hwmon) { - MAX_BATTERY_CHARGE_RATE_ATTR - } else { - MAXIMUM_BATTERY_CHARGE_RATE_ATTR - }; - let path = attr.path(&*self.sysfs_hwmon); - self.sysfs_hwmon.set(attr, self.limits.charge_rate.and_then(|lim| lim.max).unwrap_or(2500)).map_err( + let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon); + self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_RATE_ATTR, self.limits.charge_rate.and_then(|lim| lim.max).unwrap_or(2500)).map_err( |e| SettingError { msg: format!("Failed to write to `{}`: {}", path.display(), e), setting: crate::settings::SettingVariant::Battery, @@ -348,10 +334,35 @@ impl Battery { } } + fn set_charge_limit(&mut self) -> Result<(), SettingError> { + let attr_exists = MAX_BATTERY_CHARGE_LEVEL_ATTR.exists(&*self.sysfs_hwmon); + log::debug!("Does battery limit attribute (max_battery_charge_level) exist? {}", attr_exists); + if let Some(charge_limit) = self.charge_limit { + self.state.charge_limit_set = true; + self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_LEVEL_ATTR, (charge_limit * 100.0).round() as u64) + .map_err(|e| SettingError { + msg: format!("Failed to write to {:?}: {}", MAX_BATTERY_CHARGE_LEVEL_ATTR, e), + setting: crate::settings::SettingVariant::Battery, + } + ) + } else if self.state.charge_limit_set { + self.state.charge_limit_set = false; + self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_LEVEL_ATTR, 0) + .map_err(|e| SettingError { + msg: format!("Failed to reset (write to) {:?}: {}", MAX_BATTERY_CHARGE_LEVEL_ATTR, e), + setting: crate::settings::SettingVariant::Battery, + } + ) + } else { + Ok(()) + } + } + fn set_all(&mut self) -> Result<(), Vec> { let mut errors = Vec::new(); self.set_charge_rate().unwrap_or_else(|e| errors.push(e)); self.set_charge_mode().unwrap_or_else(|e| errors.push(e)); + self.set_charge_limit().unwrap_or_else(|e| errors.push(e)); if errors.is_empty() { Ok(()) } else { @@ -471,15 +482,76 @@ impl Battery { } None } + + fn remove_charge_limit_instructions(mut self) -> Self { + if let Some(lim_ev) = self.find_limit_event() { + log::debug!("Found limit event @ {}", lim_ev); + if let Some(unlim_ev) = self.find_unlimit_event() { + log::debug!("Found unlimit event @ {}", unlim_ev); + self.charge_limit = match &self.events[lim_ev].trigger { + EventTrigger::BatteryAbove(x) => Some(*x), + _ => panic!("Got limit event with wrong event trigger variant"), + }; + log::debug!("Charge limit detected as {}", self.charge_limit.unwrap()); + if lim_ev > unlim_ev { + self.events.remove(lim_ev); + self.events.remove(unlim_ev); + } else { + self.events.remove(unlim_ev); + self.events.remove(lim_ev); + } + } + } + self + } + + fn with_charge_limit_instructions(&self) -> Vec { + if let Some(limit) = self.charge_limit { + log::debug!("Adding charge limit event instructions for limit {}", limit); + let mut events = self.events.clone(); + // upper limit + log::info!( + "Creating Steam Deck charge limit event instruction of >{}", + limit + ); + events.push(EventInstruction { + trigger: EventTrigger::BatteryAbove(limit), + charge_rate: None, + charge_mode: Some(ChargeMode::Idle), + is_triggered: false, + sysfs_hwmon: self.sysfs_hwmon.clone(), + bat_ec: self.bat_ec.clone(), + }); + // lower limit + let limit = (limit - 0.10).clamp(0.0, 1.0); + log::info!( + "Creating Steam Deck charge limit event instruction of <{}", + limit + ); + events.push(EventInstruction { + trigger: EventTrigger::BatteryBelow(limit), + charge_rate: None, + charge_mode: Some(ChargeMode::Normal), + is_triggered: false, + sysfs_hwmon: self.sysfs_hwmon.clone(), + bat_ec: self.bat_ec.clone(), + }); + events + } else { + log::debug!("No charge limit set, skipping add of event instructions"); + self.events.clone() + } + } } impl Into for Battery { #[inline] fn into(self) -> BatteryJson { + let events = self.with_charge_limit_instructions(); BatteryJson { charge_rate: self.charge_rate, charge_mode: self.charge_mode.map(Self::charge_mode_to_str), - events: self.events.into_iter().map(|x| x.into()).collect(), + events: events.into_iter().map(|x| x.into()).collect(), root: self.sysfs_bat.root().or(self.sysfs_hwmon.root()).and_then(|p| p.as_ref().to_str().map(|x| x.to_owned())) } } @@ -496,6 +568,7 @@ impl ProviderBuilder for Battery { .charge_mode .map(|x| Self::str_to_charge_mode(&x)) .flatten(), + charge_limit: None, events: persistent .events .into_iter() @@ -506,13 +579,14 @@ impl ProviderBuilder for Battery { sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), sysfs_hwmon: hwmon_sys, bat_ec: ec, - }, + }.remove_charge_limit_instructions(), _ => Self { charge_rate: persistent.charge_rate, charge_mode: persistent .charge_mode .map(|x| Self::str_to_charge_mode(&x)) .flatten(), + charge_limit: None, events: persistent .events .into_iter() @@ -523,7 +597,7 @@ impl ProviderBuilder for Battery { sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), sysfs_hwmon: hwmon_sys, bat_ec: ec, - }, + }.remove_charge_limit_instructions(), } } @@ -531,13 +605,14 @@ impl ProviderBuilder for Battery { Self { charge_rate: None, charge_mode: None, + charge_limit: None, events: Vec::new(), limits: limits, state: crate::state::steam_deck::Battery::default(), sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), sysfs_hwmon: Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)), bat_ec: Arc::new(Mutex::new(UnnamedPowerEC::new())), - } + }.remove_charge_limit_instructions() } } @@ -575,41 +650,10 @@ impl OnPowerEvent for Battery { PowerMode::BatteryCharge(_) => Ok(()), } .unwrap_or_else(|mut e| errors.append(&mut e)); - let attr_exists = MAX_BATTERY_CHARGE_LEVEL_ATTR.exists(&*self.sysfs_hwmon); - log::debug!("Does battery limit attribute (max_battery_charge_level) exist? {}", attr_exists); - let mut charge_limit_set_now = false; for ev in &mut self.events { - if attr_exists { - if let EventTrigger::BatteryAbove(level) = ev.trigger { - if let Some(ChargeMode::Idle) = ev.charge_mode { - self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_LEVEL_ATTR, (level * 100.0).round() as u64) - .unwrap_or_else(|e| errors.push( - SettingError { - msg: format!("Failed to write to {:?}: {}", MAX_BATTERY_CHARGE_LEVEL_ATTR, e), - setting: crate::settings::SettingVariant::Battery, - } - )); - self.state.charge_limit_set = true; - charge_limit_set_now = true; - } - } - } ev.on_power_event(new_mode) .unwrap_or_else(|mut e| errors.append(&mut e)); } - if self.state.charge_limit_set != charge_limit_set_now { - // only true when charge_limit_set is false and self.state.charge_limit_set is true - self.state.charge_limit_set = false; - if attr_exists { - self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_LEVEL_ATTR, 0) - .unwrap_or_else(|e| errors.push( - SettingError { - msg: format!("Failed to reset (write to) {:?}: {}", MAX_BATTERY_CHARGE_LEVEL_ATTR, e), - setting: crate::settings::SettingVariant::Battery, - } - )); - } - } if errors.is_empty() { Ok(()) } else { @@ -718,88 +762,11 @@ impl TBattery for Battery { } fn charge_limit(&mut self, limit: Option) { - // upper limit - let index = self.find_limit_event(); - if let Some(index) = index { - if let Some(limit) = limit { - log::info!( - "Updating Steam Deck charge limit event instruction to >{}", - limit - ); - self.events[index] = EventInstruction { - trigger: EventTrigger::BatteryAbove(limit / 100.0), - charge_rate: None, - charge_mode: Some(ChargeMode::Idle), - is_triggered: false, - sysfs_hwmon: self.sysfs_hwmon.clone(), - bat_ec: self.bat_ec.clone(), - }; - } else { - self.events.remove(index); - } - } else if let Some(limit) = limit { - log::info!( - "Creating Steam Deck charge limit event instruction of >{}", - limit - ); - self.events.push(EventInstruction { - trigger: EventTrigger::BatteryAbove(limit / 100.0), - charge_rate: None, - charge_mode: Some(ChargeMode::Idle), - is_triggered: false, - sysfs_hwmon: self.sysfs_hwmon.clone(), - bat_ec: self.bat_ec.clone(), - }); - } - // lower limit - let index = self.find_unlimit_event(); - if let Some(index) = index { - if let Some(limit) = limit { - let limit = (limit - 10.0).clamp(0.0, 100.0); - log::info!( - "Updating Steam Deck charge limit event instruction to <{}", - limit - ); - self.events[index] = EventInstruction { - trigger: EventTrigger::BatteryBelow(limit / 100.0), - charge_rate: None, - charge_mode: Some(ChargeMode::Normal), - is_triggered: false, - sysfs_hwmon: self.sysfs_hwmon.clone(), - bat_ec: self.bat_ec.clone(), - }; - } else { - self.events.remove(index); - } - } else if let Some(limit) = limit { - let limit = (limit - 10.0).clamp(0.0, 100.0); - log::info!( - "Creating Steam Deck charge limit event instruction of <{}", - limit - ); - self.events.push(EventInstruction { - trigger: EventTrigger::BatteryBelow(limit / 100.0), - charge_rate: None, - charge_mode: Some(ChargeMode::Normal), - is_triggered: false, - sysfs_hwmon: self.sysfs_hwmon.clone(), - bat_ec: self.bat_ec.clone(), - }); - } + self.charge_limit = limit.map(|lim| lim / 100.0); } fn get_charge_limit(&self) -> Option { - let index = self.find_limit_event(); - if let Some(index) = index { - if let EventTrigger::BatteryAbove(limit) = self.events[index].trigger { - Some(limit * 100.0) - } else { - log::error!("Got index {} for battery charge limit which does not have expected event trigger: {:?}", index, &self.events); - None - } - } else { - None - } + self.charge_limit.map(|lim| lim * 100.0) } fn check_power(&mut self) -> Result, Vec> { From fa03202f6b4f92196fa645d090f0564c8ef855bd Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 28 Jan 2024 11:01:11 -0500 Subject: [PATCH 29/56] Use global DFL, fix debug UI component not updating --- package.json | 4 +-- plugin.json | 2 +- pnpm-lock.yaml | 58 ++++++++++++++++++++-------------------- rollup.config.js | 3 ++- src/components/debug.tsx | 15 ++++++++--- 5 files changed, 45 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 433c5b1..7f17bc1 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,10 @@ "rollup-plugin-import-assets": "^1.1.1", "shx": "^0.3.4", "tslib": "^2.6.2", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "decky-frontend-lib": "~3.24.4" }, "dependencies": { - "decky-frontend-lib": "~3.24.3", "react-icons": "^5.0.1", "usdpl-front": "file:src/usdpl_front" } diff --git a/plugin.json b/plugin.json index fdcb585..2a5ee54 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "name": "PowerTools", "author": "NGnius", - "flags": ["root", "_debug"], + "flags": ["root", "_debug", "global-dfl"], "publish": { "discord_id": "106537989684887552", "description": "Power tweaks for power users", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c87589..c6e3504 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,6 @@ settings: excludeLinksFromLockfile: false dependencies: - decky-frontend-lib: - specifier: ~3.24.3 - version: 3.24.3 react-icons: specifier: ^5.0.1 version: 5.0.1(react@18.2.0) @@ -37,6 +34,9 @@ devDependencies: '@types/webpack': specifier: ^5.28.5 version: 5.28.5 + decky-frontend-lib: + specifier: ~3.24.4 + version: 3.24.4 rollup: specifier: ^2.79.1 version: 2.79.1 @@ -198,8 +198,8 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true - /@types/node@20.11.5: - resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} + /@types/node@20.11.9: + resolution: {integrity: sha512-CQXNuMoS/VcoAMISe5pm4JnEd1Br5jildbQEToEMQvutmv+EaQr90ry9raiudgpyDuqFiV9e4rnjSfLNq12M5w==} dependencies: undici-types: 5.26.5 dev: true @@ -218,15 +218,15 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.9 dev: true /@types/webpack@5.28.5: resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.9 tapable: 2.2.1 - webpack: 5.89.0 + webpack: 5.90.0 transitivePeerDependencies: - '@swc/core' - esbuild @@ -390,15 +390,15 @@ packages: concat-map: 0.0.1 dev: true - /browserslist@4.22.2: - resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} + /browserslist@4.22.3: + resolution: {integrity: sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001579 - electron-to-chromium: 1.4.640 + caniuse-lite: 1.0.30001581 + electron-to-chromium: 1.4.648 node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.22.2) + update-browserslist-db: 1.0.13(browserslist@4.22.3) dev: true /buffer-from@1.1.2: @@ -410,8 +410,8 @@ packages: engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001579: - resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} + /caniuse-lite@1.0.30001581: + resolution: {integrity: sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==} dev: true /chrome-trace-event@1.0.3: @@ -435,17 +435,17 @@ packages: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} dev: true - /decky-frontend-lib@3.24.3: - resolution: {integrity: sha512-293oUaAgLrezvoz+TOQkarjwAlVlejkelB1WjtxQV4Y5qMpUZhNUtfpQAscGhwg9oQy6UGpZ5urkdPzLiVY52w==} - dev: false + /decky-frontend-lib@3.24.4: + resolution: {integrity: sha512-aCrzVS74V68PQxi5qFS6rjQwWryVy+yYTQKZztXT/T5v1jlw3AKXpTi47pCQl+6TdAIru4hAAjOF8TSf4iONaw==} + dev: true /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} dev: true - /electron-to-chromium@1.4.640: - resolution: {integrity: sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==} + /electron-to-chromium@1.4.648: + resolution: {integrity: sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg==} dev: true /enhanced-resolve@5.15.0: @@ -605,7 +605,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.9 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -836,7 +836,7 @@ packages: engines: {node: '>=6'} dev: true - /terser-webpack-plugin@5.3.10(webpack@5.89.0): + /terser-webpack-plugin@5.3.10(webpack@5.90.0): resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -857,7 +857,7 @@ packages: schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.27.0 - webpack: 5.89.0 + webpack: 5.90.0 dev: true /terser@5.27.0: @@ -885,13 +885,13 @@ packages: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true - /update-browserslist-db@1.0.13(browserslist@4.22.2): + /update-browserslist-db@1.0.13(browserslist@4.22.3): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.22.2 + browserslist: 4.22.3 escalade: 3.1.1 picocolors: 1.0.0 dev: true @@ -919,8 +919,8 @@ packages: engines: {node: '>=10.13.0'} dev: true - /webpack@5.89.0: - resolution: {integrity: sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==} + /webpack@5.90.0: + resolution: {integrity: sha512-bdmyXRCXeeNIePv6R6tGPyy20aUobw4Zy8r0LUS2EWO+U+Ke/gYDgsCh7bl5rB6jPpr4r0SZa6dPxBxLooDT3w==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -936,7 +936,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.6 acorn: 8.11.3 acorn-import-assertions: 1.9.0(acorn@8.11.3) - browserslist: 4.22.2 + browserslist: 4.22.3 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 es-module-lexer: 1.4.1 @@ -950,7 +950,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(webpack@5.89.0) + terser-webpack-plugin: 5.3.10(webpack@5.90.0) watchpack: 2.4.0 webpack-sources: 3.2.3 transitivePeerDependencies: diff --git a/rollup.config.js b/rollup.config.js index 8717908..3b2e789 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -24,12 +24,13 @@ export default defineConfig({ }) ], context: 'window', - external: ['react', 'react-dom'], + external: ['react', 'react-dom', 'decky-frontend-lib'], output: { file: 'dist/index.js', globals: { react: 'SP_REACT', 'react-dom': 'SP_REACTDOM', + 'decky-frontend-lib': 'DFL', }, format: 'iife', exports: 'default', diff --git a/src/components/debug.tsx b/src/components/debug.tsx index 69b6ffd..e41d8ac 100644 --- a/src/components/debug.tsx +++ b/src/components/debug.tsx @@ -22,11 +22,16 @@ let isSpecialDay = now.getDate() == 1 && now.getMonth() == 3; export class Debug extends Component { render() { - return buildDebug(); + const reloadGUI = (x: string) => this.setState((_state) => { + return { + reloadThingy: x, + }; + }); + return buildDebug(reloadGUI); } } -function buildDebug() { +function buildDebug(reloadGUI: (x: string) => void) { return ({/* Version Info */}
{eggCount % 10 == 9 ? "Ha! Nerd" : tr("Debug")} @@ -41,6 +46,7 @@ function buildDebug() { Navigation.NavigateToExternalWeb("https://git.ngni.us/NG-SD-Plugins/PowerTools/releases"); } eggCount++; + reloadGUI("BackendInfo"); }}> {eggCount % 10 == 9 ? "by NGnius" : get_value(BACKEND_INFO)} @@ -48,14 +54,14 @@ function buildDebug() { eggCount++}> + onClick={() => {eggCount++; reloadGUI("FrameworkInfo");}}> {eggCount % 10 == 9 ? "<3 <3 <3" : target_usdpl()} eggCount++}> + onClick={() => {eggCount++; reloadGUI("DriverInfo");}}> {eggCount % 10 == 9 ? "Ryan Gosling" : get_value(DRIVER_INFO)} @@ -69,6 +75,7 @@ function buildDebug() { Navigation.NavigateToExternalWeb("https://git.ngni.us/NG-SD-Plugins/usdpl-rs"); } eggCount++; + reloadGUI("USDPLInfo"); }}> v{version_usdpl()} From 3e8a263f4ea1bfe28af6ad309672275c06aa2b97 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 28 Jan 2024 12:13:55 -0500 Subject: [PATCH 30/56] Add thing at startup in debug builds (for testing) --- backend/src/main.rs | 6 ++++++ backend/src/settings/steam_deck/mod.rs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/backend/src/main.rs b/backend/src/main.rs index f2f683b..4ceab20 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -334,6 +334,12 @@ fn main() -> Result<(), ()> { utility::ioperm_power_ec(); + #[cfg(debug_assertions)] + std::thread::spawn(|| { + utility::ioperm_power_ec(); + settings::steam_deck::util::flash_led(); + }); + if let Err(e) = loaded_settings.on_set() { e.iter() .for_each(|e| log::error!("Startup Settings.on_set() error: {}", e)); diff --git a/backend/src/settings/steam_deck/mod.rs b/backend/src/settings/steam_deck/mod.rs index b3f840d..a87f005 100644 --- a/backend/src/settings/steam_deck/mod.rs +++ b/backend/src/settings/steam_deck/mod.rs @@ -2,6 +2,9 @@ mod battery; mod cpu; mod gpu; mod power_dpm_force; +#[cfg(debug_assertions)] +pub mod util; +#[cfg(not(debug_assertions))] mod util; pub use battery::Battery; From 0ee30eebe63b3d175d1c307aca7729778761d550 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 28 Jan 2024 19:55:20 -0500 Subject: [PATCH 31/56] Add dev driver --- backend/limits_core/src/json_v2/base.rs | 34 +++ .../limits_core/src/json_v2/battery_limit.rs | 25 ++ backend/limits_core/src/json_v2/cpu_limit.rs | 39 ++++ backend/limits_core/src/json_v2/gpu_limit.rs | 46 +++- backend/src/persist/driver.rs | 2 + backend/src/settings/detect/auto_detect.rs | 30 +++ backend/src/settings/dev_mode/battery.rs | 146 ++++++++++++ backend/src/settings/dev_mode/cpu.rs | 221 ++++++++++++++++++ backend/src/settings/dev_mode/gpu.rs | 141 +++++++++++ backend/src/settings/dev_mode/mod.rs | 15 ++ backend/src/settings/driver.rs | 1 + backend/src/settings/mod.rs | 3 +- 12 files changed, 701 insertions(+), 2 deletions(-) create mode 100644 backend/src/settings/dev_mode/battery.rs create mode 100644 backend/src/settings/dev_mode/cpu.rs create mode 100644 backend/src/settings/dev_mode/gpu.rs create mode 100644 backend/src/settings/dev_mode/mod.rs diff --git a/backend/limits_core/src/json_v2/base.rs b/backend/limits_core/src/json_v2/base.rs index 57abec8..7733759 100644 --- a/backend/limits_core/src/json_v2/base.rs +++ b/backend/limits_core/src/json_v2/base.rs @@ -16,6 +16,30 @@ impl Default for Base { fn default() -> Self { Base { configs: vec![ + super::Config { + name: "Devs mode best mode".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: None, + os: None, + command: None, + file_exists: Some("/etc/powertools_dev_mode".into()), + }, + limits: super::Limits { + cpu: super::Limit { + provider: super::CpuLimitType::DevMode, + limits: super::GenericCpusLimit::default_for(super::CpuLimitType::DevMode), + }, + gpu: super::Limit { + provider: super::GpuLimitType::DevMode, + limits: super::GenericGpuLimit::default_for(super::GpuLimitType::DevMode), + }, + battery: super::Limit { + provider: super::BatteryLimitType::DevMode, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::DevMode), + }, + } + }, super::Config { name: "Steam Deck".to_owned(), conditions: super::Conditions { @@ -83,8 +107,10 @@ impl Default for Base { clock_max: Some(super::RangeLimit { min: Some(1000), max: Some(3700) }), clock_step: Some(100), skip_resume_reclock: false, + ..Default::default() }; 4], global_governors: true, + experiments: false, } }, gpu: super::GpuLimit { @@ -125,8 +151,10 @@ impl Default for Base { clock_max: Some(super::RangeLimit { min: Some(1000), max: Some(4000) }), clock_step: Some(100), skip_resume_reclock: false, + ..Default::default() }; 12], // 6 cores with SMTx2 global_governors: true, + experiments: false, } }, gpu: super::GpuLimit { @@ -167,8 +195,10 @@ impl Default for Base { clock_max: Some(super::RangeLimit { min: Some(1000), max: Some(4500) }), clock_step: Some(100), skip_resume_reclock: false, + ..Default::default() }; 16], // 8 cores with SMTx2 global_governors: true, + experiments: false, } }, gpu: super::GpuLimit { @@ -209,8 +239,10 @@ impl Default for Base { clock_max: Some(super::RangeLimit { min: Some(1000), max: Some(4700) }), clock_step: Some(100), skip_resume_reclock: false, + ..Default::default() }; 16], // 8 cores with SMTx2 global_governors: true, + experiments: false, } }, gpu: super::GpuLimit { @@ -251,8 +283,10 @@ impl Default for Base { clock_max: Some(super::RangeLimit { min: Some(400), max: Some(5100) }), clock_step: Some(100), skip_resume_reclock: false, + ..Default::default() }; 16], // 8 cores with SMTx2 global_governors: true, + experiments: false, } }, gpu: super::GpuLimit { diff --git a/backend/limits_core/src/json_v2/battery_limit.rs b/backend/limits_core/src/json_v2/battery_limit.rs index 4986283..d0d4890 100644 --- a/backend/limits_core/src/json_v2/battery_limit.rs +++ b/backend/limits_core/src/json_v2/battery_limit.rs @@ -10,6 +10,7 @@ pub enum BatteryLimitType { SteamDeckAdvance, Generic, Unknown, + DevMode, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -18,12 +19,14 @@ pub struct GenericBatteryLimit { pub charge_modes: Vec, pub charge_limit: Option>, // battery charge % pub extra_readouts: bool, + pub experiments: bool, } impl GenericBatteryLimit { pub fn default_for(t: BatteryLimitType) -> Self { match t { BatteryLimitType::SteamDeck | BatteryLimitType::SteamDeckAdvance => Self::default_steam_deck(), + BatteryLimitType::DevMode => Self::default_dev_mode(), _t => Self::default(), } } @@ -44,6 +47,27 @@ impl GenericBatteryLimit { max: Some(90.0), }), extra_readouts: false, + experiments: false, + } + } + + fn default_dev_mode() -> Self { + Self { + charge_rate: Some(RangeLimit { + min: Some(0), + max: Some(1_000), + }), + charge_modes: vec![ + "normal".to_owned(), + "discharge".to_owned(), + "idle".to_owned(), + ], + charge_limit: Some(RangeLimit { + min: Some(1.0), + max: Some(99.0), + }), + extra_readouts: true, + experiments: true, } } @@ -67,5 +91,6 @@ impl GenericBatteryLimit { } } self.extra_readouts = limit_override.extra_readouts; + self.experiments = limit_override.experiments; } } diff --git a/backend/limits_core/src/json_v2/cpu_limit.rs b/backend/limits_core/src/json_v2/cpu_limit.rs index b2621d7..e391078 100644 --- a/backend/limits_core/src/json_v2/cpu_limit.rs +++ b/backend/limits_core/src/json_v2/cpu_limit.rs @@ -12,12 +12,14 @@ pub enum CpuLimitType { Generic, GenericAMD, Unknown, + DevMode, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct GenericCpusLimit { pub cpus: Vec, pub global_governors: bool, + pub experiments: bool, } impl GenericCpusLimit { @@ -27,6 +29,14 @@ impl GenericCpusLimit { Self { cpus: [(); 8].iter().enumerate().map(|(i, _)| GenericCpuLimit::default_for(&t, i)).collect(), global_governors: true, + experiments: false, + } + }, + CpuLimitType::DevMode => { + Self { + cpus: [(); 11].iter().enumerate().map(|(i, _)| GenericCpuLimit::default_for(&t, i)).collect(), + global_governors: true, + experiments: true, } }, t => { @@ -38,6 +48,7 @@ impl GenericCpusLimit { Self { cpus, global_governors: true, + experiments: false, } } } @@ -65,6 +76,7 @@ impl GenericCpusLimit { .for_each(|(cpu, limit_override)| cpu.apply_override(limit_override)); } self.global_governors = limit_override.global_governors; + self.experiments = limit_override.experiments; } } @@ -73,18 +85,39 @@ pub struct GenericCpuLimit { pub clock_min: Option>, pub clock_max: Option>, pub clock_step: Option, + pub tdp: Option>, + pub tdp_boost: Option>, + pub tdp_divisor: Option, + pub tdp_step: Option, pub skip_resume_reclock: bool, + pub experiments: bool, } impl GenericCpuLimit { pub fn default_for(t: &CpuLimitType, _index: usize) -> Self { match t { CpuLimitType::SteamDeck | CpuLimitType::SteamDeckAdvance => Self::default_steam_deck(), + CpuLimitType::DevMode => Self { + clock_min: Some(RangeLimit { min: Some(100), max: Some(5000) }), + clock_max: Some(RangeLimit { min: Some(100), max: Some(4800) }), + clock_step: Some(100), + tdp: Some(RangeLimit { min: Some(1_000_000), max: Some(100_000_000) }), + tdp_boost: Some(RangeLimit { min: Some(1_000_000), max: Some(110_000_000) }), + tdp_divisor: Some(1_000_000), + tdp_step: Some(1), + skip_resume_reclock: false, + experiments: true, + }, _ => Self { clock_min: None, clock_max: None, clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, }, } } @@ -100,7 +133,12 @@ impl GenericCpuLimit { max: Some(3500), }), clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, } } @@ -124,5 +162,6 @@ impl GenericCpuLimit { } self.clock_step = limit_override.clock_step; self.skip_resume_reclock = limit_override.skip_resume_reclock; + self.experiments = limit_override.experiments; } } diff --git a/backend/limits_core/src/json_v2/gpu_limit.rs b/backend/limits_core/src/json_v2/gpu_limit.rs index 39643ff..1662149 100644 --- a/backend/limits_core/src/json_v2/gpu_limit.rs +++ b/backend/limits_core/src/json_v2/gpu_limit.rs @@ -8,11 +8,12 @@ pub enum GpuLimitType { SteamDeck, #[serde(rename = "GabeBoyAdvance", alias = "SteamDeckAdvance")] SteamDeckAdvance, - #[serde(rename = "GabeBoy101", alias = "SteamDeckOLED")] + #[serde(rename = "GabeBoySP", alias = "SteamDeckOLED")] SteamDeckOLED, Generic, GenericAMD, Unknown, + DevMode, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -25,6 +26,7 @@ pub struct GenericGpuLimit { pub ppt_step: Option, pub tdp: Option>, pub tdp_boost: Option>, + pub tdp_divisor: Option, pub tdp_step: Option, pub clock_min: Option>, pub clock_max: Option>, @@ -32,6 +34,7 @@ pub struct GenericGpuLimit { pub memory_clock: Option>, pub memory_clock_step: Option, pub skip_resume_reclock: bool, + pub experiments: bool, } impl GenericGpuLimit { @@ -39,6 +42,7 @@ impl GenericGpuLimit { match t { GpuLimitType::SteamDeck | GpuLimitType::SteamDeckAdvance => Self::default_steam_deck(), GpuLimitType::SteamDeckOLED => Self::default_steam_deck_oled(), + GpuLimitType::DevMode => Self::default_dev_mode(), _t => Self::default(), } } @@ -59,6 +63,7 @@ impl GenericGpuLimit { ppt_step: Some(1), tdp: None, tdp_boost: None, + tdp_divisor: None, tdp_step: None, clock_min: Some(RangeLimit { min: Some(400), @@ -78,6 +83,7 @@ impl GenericGpuLimit { memory_clock: None, memory_clock_step: None, skip_resume_reclock: false, + experiments: false, } } @@ -87,6 +93,43 @@ impl GenericGpuLimit { sd } + fn default_dev_mode() -> Self { + Self { + fast_ppt: Some(RangeLimit { + min: Some(3_000_000), + max: Some(11_000_000), + }), + fast_ppt_default: Some(10_000_000), + slow_ppt: Some(RangeLimit { + min: Some(7_000_000), + max: Some(11_000_000), + }), + slow_ppt_default: Some(10_000_000), + ppt_divisor: Some(1_000_000), + ppt_step: Some(1), + tdp: Some(RangeLimit { min: Some(1_000_000), max: Some(100_000_000) }), + tdp_boost: Some(RangeLimit { min: Some(1_000_000), max: Some(110_000_000) }), + tdp_divisor: Some(1_000_000), + tdp_step: Some(1), + clock_min: Some(RangeLimit { + min: Some(100), + max: Some(1000), + }), + clock_max: Some(RangeLimit { + min: Some(100), + max: Some(1100), + }), + clock_step: Some(100), + memory_clock: Some(RangeLimit { + min: Some(100), + max: Some(1100), + }), + memory_clock_step: Some(100), + skip_resume_reclock: false, + experiments: true, + } + } + pub fn apply_override(&mut self, limit_override: Self) { if let Some(range) = limit_override.fast_ppt { if range.min.is_none() && range.max.is_none() { @@ -149,5 +192,6 @@ impl GenericGpuLimit { self.clock_step = Some(val); } self.skip_resume_reclock = limit_override.skip_resume_reclock; + self.experiments = limit_override.experiments; } } diff --git a/backend/src/persist/driver.rs b/backend/src/persist/driver.rs index 1a774bd..56f50d6 100644 --- a/backend/src/persist/driver.rs +++ b/backend/src/persist/driver.rs @@ -17,4 +17,6 @@ pub enum DriverJson { #[default] #[serde(rename = "auto")] AutoDetect, + #[serde(rename = "indev")] + DevMode, } diff --git a/backend/src/settings/detect/auto_detect.rs b/backend/src/settings/detect/auto_detect.rs index d21e502..a9f8b92 100644 --- a/backend/src/settings/detect/auto_detect.rs +++ b/backend/src/settings/detect/auto_detect.rs @@ -185,6 +185,13 @@ pub fn auto_detect0( settings.version, relevant_limits.cpu.limits, )) + }, + CpuLimitType::DevMode => { + Box::new(crate::settings::dev_mode::Cpus::from_json_and_limits( + settings.cpus.clone(), + settings.version, + relevant_limits.cpu.limits, + )) } }; @@ -230,6 +237,13 @@ pub fn auto_detect0( settings.version, relevant_limits.gpu.limits, )) + }, + GpuLimitType::DevMode => { + Box::new(crate::settings::dev_mode::Gpu::from_json_and_limits( + settings.gpu.clone(), + settings.version, + relevant_limits.gpu.limits, + )) } }; let battery_driver: Box = match relevant_limits.battery.provider { @@ -260,6 +274,13 @@ pub fn auto_detect0( settings.version, relevant_limits.battery.limits, )) + }, + BatteryLimitType::DevMode => { + Box::new(crate::settings::dev_mode::Battery::from_json_and_limits( + settings.battery.clone(), + settings.version, + relevant_limits.battery.limits, + )) } }; @@ -288,6 +309,9 @@ pub fn auto_detect0( CpuLimitType::Unknown => { Box::new(crate::settings::unknown::Cpus::from_limits(relevant_limits.cpu.limits)) } + CpuLimitType::DevMode => { + Box::new(crate::settings::dev_mode::Cpus::from_limits(relevant_limits.cpu.limits)) + } }; let gpu_driver: Box = match relevant_limits.gpu.provider { GpuLimitType::SteamDeck => { @@ -308,6 +332,9 @@ pub fn auto_detect0( GpuLimitType::Unknown => { Box::new(crate::settings::unknown::Gpu::from_limits(relevant_limits.gpu.limits)) } + GpuLimitType::DevMode => { + Box::new(crate::settings::dev_mode::Gpu::from_limits(relevant_limits.gpu.limits)) + } }; let battery_driver: Box = match relevant_limits.battery.provider { BatteryLimitType::SteamDeck => { @@ -322,6 +349,9 @@ pub fn auto_detect0( BatteryLimitType::Unknown => { Box::new(crate::settings::unknown::Battery::from_limits(relevant_limits.battery.limits)) } + BatteryLimitType::DevMode => { + Box::new(crate::settings::dev_mode::Battery::from_limits(relevant_limits.battery.limits)) + } }; return Driver { general: general_driver, diff --git a/backend/src/settings/dev_mode/battery.rs b/backend/src/settings/dev_mode/battery.rs new file mode 100644 index 0000000..ac7cf79 --- /dev/null +++ b/backend/src/settings/dev_mode/battery.rs @@ -0,0 +1,146 @@ +use std::convert::Into; + +use limits_core::json_v2::GenericBatteryLimit; + +use crate::persist::BatteryJson; +use crate::settings::{TBattery, ProviderBuilder}; +use crate::settings::{OnResume, OnSet, SettingError}; + +#[derive(Clone)] +pub struct Battery { + persist: BatteryJson, + version: u64, + limits: GenericBatteryLimit, + charge_limit: Option, +} + +impl std::fmt::Debug for Battery { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("dev_mode_Battery") + //.field("persist", &self.persist) + .field("version", &self.version) + .field("limits", &self.limits) + .finish_non_exhaustive() + } +} + +impl Into for Battery { + #[inline] + fn into(self) -> BatteryJson { + self.persist + } +} + +impl ProviderBuilder for Battery { + fn from_json_and_limits(persist: BatteryJson, version: u64, limits: GenericBatteryLimit) -> Self { + Battery { + persist, + version, + limits, + charge_limit: None, + } + } + + fn from_limits(limits: GenericBatteryLimit) -> Self { + Battery { + persist: BatteryJson { charge_rate: None, charge_mode: None, events: vec![], root: None }, + version: 0, + limits, + charge_limit: None, + } + } +} + +impl OnSet for Battery { + fn on_set(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Battery::on_set(self)"); + Ok(()) + } +} + +impl OnResume for Battery { + fn on_resume(&self) -> Result<(), Vec> { + log::debug!("dev_mode_Battery::on_resume(self)"); + Ok(()) + } +} + +impl crate::settings::OnPowerEvent for Battery {} + +impl TBattery for Battery { + fn limits(&self) -> crate::api::BatteryLimits { + log::debug!("dev_mode_Battery::limits(self) -> {{...}}"); + crate::api::BatteryLimits { + charge_current: self.limits.charge_rate.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(11), max: lim.max.unwrap_or(1111) }), + charge_current_step: 10, + charge_modes: self.limits.charge_modes.clone(), + charge_limit: self.limits.charge_limit.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(2.0), max: lim.max.unwrap_or(98.0) }), + charge_limit_step: 1.0, + } + } + + fn json(&self) -> crate::persist::BatteryJson { + log::debug!("dev_mode_Battery::json(self) -> {{...}}"); + self.clone().into() + } + + fn charge_rate(&mut self, rate: Option) { + log::debug!("dev_mode_Battery::charge_rate(self, {:?})", rate); + self.persist.charge_rate = rate; + } + + fn get_charge_rate(&self) -> Option { + log::debug!("dev_mode_Battery::get_charge_rate(self) -> {:?}", self.persist.charge_rate); + self.persist.charge_rate + } + + fn charge_mode(&mut self, rate: Option) { + log::debug!("dev_mode_Battery::charge_mode(self, {:?})", rate); + self.persist.charge_mode = rate; + } + + fn get_charge_mode(&self) -> Option { + log::debug!("dev_mode_Battery::get_charge_mode(self) -> {:?}", self.persist.charge_mode); + self.persist.charge_mode.clone() + } + + fn read_charge_full(&self) -> Option { + log::debug!("dev_mode_Battery::read_charge_full(self) -> None"); + None + } + + fn read_charge_now(&self) -> Option { + log::debug!("dev_mode_Battery::read_charge_now(self) -> None"); + None + } + + fn read_charge_design(&self) -> Option { + log::debug!("dev_mode_Battery::read_charge_design(self) -> None"); + None + } + + fn read_current_now(&self) -> Option { + log::debug!("dev_mode_Battery::read_current_now(self) -> None"); + None + } + + fn read_charge_power(&self) -> Option { + log::debug!("dev_mode_Battery::read_charge_power(self) -> None"); + None + } + + fn charge_limit(&mut self, limit: Option) { + log::debug!("dev_mode_Battery::charge_limit(self, {:?})", limit); + self.charge_limit = limit; + } + + fn get_charge_limit(&self) -> Option { + log::debug!("dev_mode_Battery::get_charge_limit(self) -> {:?}", self.charge_limit); + self.charge_limit + } + + fn provider(&self) -> crate::persist::DriverJson { + log::debug!("dev_mode_Battery::provider(self) -> DevMode"); + crate::persist::DriverJson::DevMode + } +} diff --git a/backend/src/settings/dev_mode/cpu.rs b/backend/src/settings/dev_mode/cpu.rs new file mode 100644 index 0000000..c278fe1 --- /dev/null +++ b/backend/src/settings/dev_mode/cpu.rs @@ -0,0 +1,221 @@ +use std::convert::Into; + +use limits_core::json_v2::{GenericCpusLimit, GenericCpuLimit}; + +use crate::persist::CpuJson; +use crate::settings::MinMax; +use crate::settings::{OnResume, OnSet, SettingError}; +use crate::settings::{TCpu, TCpus, ProviderBuilder}; + +#[derive(Debug, Clone)] +pub struct Cpus { + cpus: Vec, + #[allow(dead_code)] + version: u64, + smt_enabled: bool, + #[allow(dead_code)] + limits: GenericCpusLimit, +} + +impl OnSet for Cpus { + fn on_set(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Cpus::on_set(self)"); + for cpu in self.cpus.iter_mut() { + cpu.on_set()?; + } + Ok(()) + } +} + +impl OnResume for Cpus { + fn on_resume(&self) -> Result<(), Vec> { + log::debug!("dev_mode_Cpus::on_resume(self)"); + for cpu in self.cpus.iter() { + cpu.on_resume()?; + } + Ok(()) + } +} + +impl crate::settings::OnPowerEvent for Cpus {} + +impl ProviderBuilder, GenericCpusLimit> for Cpus { + fn from_json_and_limits(persistent: Vec, version: u64, limits: GenericCpusLimit) -> Self { + let mut cpus = Vec::with_capacity(persistent.len()); + for (i, cpu) in persistent.iter().enumerate() { + cpus.push(Cpu::from_json_and_limits(cpu.to_owned(), version, i, limits.cpus.get(i).map(|x| x.to_owned()).unwrap_or_else(|| { + log::warn!("No cpu limit for index {}, using default", i); + Default::default() + }))); + } + let smt_guess = crate::settings::util::guess_smt(&persistent); + Self { + cpus, + version, + smt_enabled: smt_guess, + limits, + } + } + + fn from_limits(limits: GenericCpusLimit) -> Self { + let mut cpus = Vec::with_capacity(limits.cpus.len()); + for (i, cpu) in limits.cpus.iter().enumerate() { + cpus.push(Cpu::from_limits(i, cpu.to_owned())); + } + Self { + cpus, + version: 0, + smt_enabled: true, + limits, + } + } +} + +impl TCpus for Cpus { + fn limits(&self) -> crate::api::CpusLimits { + log::debug!("dev_mode_Cpus::limits(self) -> {{...}}"); + crate::api::CpusLimits { + cpus: self.cpus.iter().map(|x| x.limits()).collect(), + count: self.cpus.len(), + smt_capable: true, + governors: vec!["this".to_owned(), "is".to_owned(), "dev".to_owned(), "mode".to_owned()], + } + } + + fn json(&self) -> Vec { + log::debug!("dev_mode_Cpus::json(self) -> {{...}}"); + self.cpus.iter().map(|x| x.to_owned().into()).collect() + } + + fn cpus(&mut self) -> Vec<&mut dyn TCpu> { + log::debug!("dev_mode_Cpus::cpus(self) -> {{...}}"); + self.cpus.iter_mut().map(|x| x as &mut dyn TCpu).collect() + } + + fn len(&self) -> usize { + log::debug!("dev_mode_Cpus::len(self) -> {}", self.cpus.len()); + self.cpus.len() + } + + fn smt(&mut self) -> &'_ mut bool { + log::debug!("dev_mode_Cpus::smt(self) -> {}", self.smt_enabled); + &mut self.smt_enabled + } + + fn provider(&self) -> crate::persist::DriverJson { + log::debug!("dev_mode_Cpus::provider(self) -> DevMode"); + crate::persist::DriverJson::DevMode + } +} + +#[derive(Clone)] +pub struct Cpu { + persist: CpuJson, + version: u64, + index: usize, + limits: GenericCpuLimit, + clock_limits: Option>, +} + +impl std::fmt::Debug for Cpu { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("dev_mode_Cpu") + //.field("persist", &self.persist) + .field("version", &self.version) + .field("index", &self.index) + .field("limits", &self.limits) + .finish_non_exhaustive() + } +} + +impl Cpu { + #[inline] + pub fn from_json_and_limits(other: CpuJson, version: u64, i: usize, limits: GenericCpuLimit) -> Self { + let clock_limits = other.clock_limits.clone().map(|lim| MinMax { min: lim.min, max: lim.max }); + match version { + 0 => Self { + persist: other, + version, + index: i, + limits, + clock_limits, + }, + _ => Self { + persist: other, + version, + index: i, + limits, + clock_limits, + }, + } + } + + #[inline] + pub fn from_limits(i: usize, limits: GenericCpuLimit) -> Self { + Self { + persist: CpuJson { online: true, clock_limits: None, governor: "".to_owned(), root: None }, + version: 0, + index: i, + limits, + clock_limits: None, + } + } + + fn limits(&self) -> crate::api::CpuLimits { + crate::api::CpuLimits { + clock_min_limits: self.limits.clock_min.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(1100), max: lim.max.unwrap_or(6900) }), + clock_max_limits: self.limits.clock_max.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(4200), max: lim.max.unwrap_or(4300) }), + clock_step: self.limits.clock_step.unwrap_or(11), + governors: vec!["this".to_owned(), "is".to_owned(), "dev".to_owned(), "mode".to_owned()], + } + } +} + +impl Into for Cpu { + #[inline] + fn into(self) -> CpuJson { + self.persist + } +} + +impl OnSet for Cpu { + fn on_set(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Cpu::on_set(self)"); + Ok(()) + } +} + +impl OnResume for Cpu { + fn on_resume(&self) -> Result<(), Vec> { + log::debug!("dev_mode_Cpu::on_resume(self)"); + Ok(()) + } +} + +impl TCpu for Cpu { + fn online(&mut self) -> &mut bool { + log::debug!("dev_mode_Cpu::online(self) -> {}", self.persist.online); + &mut self.persist.online + } + + fn governor(&mut self, governor: String) { + log::debug!("dev_mode_Cpu::governor(self, {})", governor); + self.persist.governor = governor; + } + + fn get_governor(&self) -> &'_ str { + log::debug!("dev_mode_Cpu::governor(self) -> {}", self.persist.governor); + &self.persist.governor + } + + fn clock_limits(&mut self, limits: Option>) { + log::debug!("dev_mode_Cpu::clock_limits(self, {:?})", limits); + self.clock_limits = limits; + self.persist.clock_limits = self.clock_limits.clone().map(|lim| crate::persist::MinMaxJson { max: lim.max, min: lim.min }); + } + + fn get_clock_limits(&self) -> Option<&MinMax> { + log::debug!("dev_mode_Cpu::get_clock_limits(self) -> {:?}", self.clock_limits.as_ref()); + self.clock_limits.as_ref() + } +} diff --git a/backend/src/settings/dev_mode/gpu.rs b/backend/src/settings/dev_mode/gpu.rs new file mode 100644 index 0000000..b9f6e76 --- /dev/null +++ b/backend/src/settings/dev_mode/gpu.rs @@ -0,0 +1,141 @@ +use std::convert::Into; + +use limits_core::json_v2::GenericGpuLimit; + +use crate::persist::GpuJson; +use crate::settings::MinMax; +use crate::settings::{TGpu, ProviderBuilder}; +use crate::settings::{OnResume, OnSet, SettingError}; + +#[derive(Clone)] +pub struct Gpu { + persist: GpuJson, + version: u64, + limits: GenericGpuLimit, + clock_limits: Option>, +} + +impl std::fmt::Debug for Gpu { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("dev_mode_Gpu") + //.field("persist", &self.persist) + .field("version", &self.version) + .field("limits", &self.limits) + .finish_non_exhaustive() + } +} + +impl ProviderBuilder for Gpu { + fn from_json_and_limits(persist: GpuJson, version: u64, limits: GenericGpuLimit) -> Self { + let clock_limits = persist.clock_limits.clone().map(|lim| MinMax { min: lim.min, max: lim.max }); + Self { + persist, + version, + limits, + clock_limits, + } + } + + fn from_limits(limits: GenericGpuLimit) -> Self { + Self { + persist: GpuJson { + fast_ppt: None, + slow_ppt: None, + tdp: None, + tdp_boost: None, + clock_limits: None, + memory_clock: None, + root: None, + }, + version: 0, + limits, + clock_limits: None, + } + } +} + +impl Into for Gpu { + #[inline] + fn into(self) -> GpuJson { + self.persist + } +} + +impl OnSet for Gpu { + fn on_set(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Gpu::on_set(self)"); + Ok(()) + } +} + +impl OnResume for Gpu { + fn on_resume(&self) -> Result<(), Vec> { + log::debug!("dev_mode_Gpu::on_resume(self)"); + Ok(()) + } +} + +impl crate::settings::OnPowerEvent for Gpu {} + +impl TGpu for Gpu { + fn limits(&self) -> crate::api::GpuLimits { + log::debug!("dev_mode_Gpu::limits(self) -> {{...}}"); + let ppt_divisor = self.limits.ppt_divisor.unwrap_or(1_000_000); + let tdp_divisor = self.limits.tdp_divisor.unwrap_or(1_000_000); + crate::api::GpuLimits { + fast_ppt_limits: self.limits.fast_ppt.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(11_000_000) / ppt_divisor, max: lim.max.unwrap_or(42_000_000) / ppt_divisor }), + slow_ppt_limits: self.limits.slow_ppt.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(7_000_000) / ppt_divisor, max: lim.max.unwrap_or(69_000_000) / ppt_divisor }), + ppt_step: self.limits.ppt_step.unwrap_or(1), + tdp_limits: self.limits.tdp.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(11_000_000) / tdp_divisor, max: lim.max.unwrap_or(69_000_000) / tdp_divisor }), + tdp_boost_limits: self.limits.tdp_boost.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(7_000_000) / tdp_divisor, max: lim.max.unwrap_or(69_000_000) / tdp_divisor }), + tdp_step: self.limits.tdp_step.unwrap_or(1), + clock_min_limits: self.limits.clock_min.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(1100), max: lim.max.unwrap_or(6900) }), + clock_max_limits: self.limits.clock_max.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(1100), max: lim.max.unwrap_or(4200) }), + clock_step: self.limits.clock_step.unwrap_or(100), + memory_control: self.limits.memory_clock.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(100), max: lim.max.unwrap_or(1100) }), + memory_step: self.limits.memory_clock_step.unwrap_or(400), + } + } + + fn json(&self) -> crate::persist::GpuJson { + log::debug!("dev_mode_Gpu::json(self) -> {{...}}"); + self.clone().into() + } + + fn ppt(&mut self, fast: Option, slow: Option) { + log::debug!("dev_mode_Gpu::ppt(self, fast: {:?}, slow: {:?})", fast, slow); + self.persist.fast_ppt = fast; + self.persist.slow_ppt = slow; + } + + fn get_ppt(&self) -> (Option, Option) { + log::debug!("dev_mode_Gpu::get_ppt(self) -> (fast: {:?}, slow: {:?})", self.persist.fast_ppt, self.persist.slow_ppt); + (self.persist.fast_ppt, self.persist.slow_ppt) + } + + fn clock_limits(&mut self, limits: Option>) { + log::debug!("dev_mode_Gpu::clock_limits(self, {:?})", limits); + self.clock_limits = limits; + self.persist.clock_limits = self.clock_limits.clone().map(|lim| crate::persist::MinMaxJson { max: lim.max, min: lim.min }); + } + + fn get_clock_limits(&self) -> Option<&MinMax> { + log::debug!("dev_mode_Gpu::get_clock_limits(self) -> {:?}", self.clock_limits.as_ref()); + self.clock_limits.as_ref() + } + + fn memory_clock(&mut self, speed: Option) { + log::debug!("dev_mode_Gpu::memory_clock(self, {:?})", speed); + self.persist.memory_clock = speed; + } + + fn get_memory_clock(&self) -> Option { + log::debug!("dev_mode_Gpu::memory_clock(self) -> {:?}", self.persist.memory_clock); + self.persist.memory_clock + } + + fn provider(&self) -> crate::persist::DriverJson { + log::debug!("dev_mode_Gpu::provider(self) -> DevMode"); + crate::persist::DriverJson::DevMode + } +} diff --git a/backend/src/settings/dev_mode/mod.rs b/backend/src/settings/dev_mode/mod.rs new file mode 100644 index 0000000..200c1ea --- /dev/null +++ b/backend/src/settings/dev_mode/mod.rs @@ -0,0 +1,15 @@ +mod battery; +mod cpu; +mod gpu; + +pub use battery::Battery; +pub use cpu::Cpus; +pub use gpu::Gpu; + +fn _impl_checker() { + fn impl_provider_builder, J, L>() {} + + impl_provider_builder::(); + impl_provider_builder::, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::(); +} diff --git a/backend/src/settings/driver.rs b/backend/src/settings/driver.rs index 6f94fff..73eb1f1 100644 --- a/backend/src/settings/driver.rs +++ b/backend/src/settings/driver.rs @@ -37,5 +37,6 @@ pub fn maybe_do_button() { } DriverJson::Unknown => log::warn!("Can't do button activities on unknown platform"), DriverJson::AutoDetect => log::warn!("WTF, why is auto_detect detecting AutoDetect???"), + DriverJson::DevMode => log::error!("Hello dev world!"), } } diff --git a/backend/src/settings/mod.rs b/backend/src/settings/mod.rs index 5bcb3f4..e18fa4b 100644 --- a/backend/src/settings/mod.rs +++ b/backend/src/settings/mod.rs @@ -10,6 +10,7 @@ pub mod generic; pub mod generic_amd; pub mod steam_deck; pub mod unknown; +pub mod dev_mode; pub use detect::{auto_detect0, auto_detect_provider, limits_worker::spawn as limits_worker_spawn, get_dev_messages}; pub use driver::Driver; @@ -23,7 +24,7 @@ pub use traits::{OnPowerEvent, OnResume, OnSet, PowerMode, TBattery, TCpu, TCpus mod tests { #[test] fn system_defaults_test() { - let settings = super::Settings::system_default("idc".into(), "Cool name".into(), 0, "Variant 0".into()); + let settings = super::Settings::system_default("idc".into(), 0, "Cool name".into(), 0, "Variant 0".into()); println!("Loaded system settings: {:?}", settings); } } From 85a9fc8f7e69088aa3dbc2b0f93f3ce0246f95a6 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 28 Jan 2024 20:24:29 -0500 Subject: [PATCH 32/56] Allow device auto detection to tell steam deck driver the specific model --- .../limits_core/src/json_v2/battery_limit.rs | 6 +-- backend/limits_core/src/json_v2/cpu_limit.rs | 8 ++-- backend/limits_core/src/json_v2/gpu_limit.rs | 4 +- backend/src/main.rs | 2 +- backend/src/persist/driver.rs | 4 +- backend/src/settings/detect/auto_detect.rs | 42 +++++++------------ backend/src/settings/driver.rs | 2 +- backend/src/settings/steam_deck/battery.rs | 14 ++++++- backend/src/settings/steam_deck/cpu.rs | 14 ++++++- backend/src/settings/steam_deck/gpu.rs | 16 ++++++- backend/src/settings/steam_deck/mod.rs | 9 ++-- 11 files changed, 74 insertions(+), 47 deletions(-) diff --git a/backend/limits_core/src/json_v2/battery_limit.rs b/backend/limits_core/src/json_v2/battery_limit.rs index d0d4890..0c3de9d 100644 --- a/backend/limits_core/src/json_v2/battery_limit.rs +++ b/backend/limits_core/src/json_v2/battery_limit.rs @@ -6,8 +6,8 @@ use super::RangeLimit; pub enum BatteryLimitType { #[serde(rename = "GabeBoy", alias = "SteamDeck")] SteamDeck, - #[serde(rename = "GabeBoyAdvance", alias = "SteamDeckAdvance")] - SteamDeckAdvance, + #[serde(rename = "GabeBoySP", alias = "SteamDeckOLED")] + SteamDeckOLED, Generic, Unknown, DevMode, @@ -25,7 +25,7 @@ pub struct GenericBatteryLimit { impl GenericBatteryLimit { pub fn default_for(t: BatteryLimitType) -> Self { match t { - BatteryLimitType::SteamDeck | BatteryLimitType::SteamDeckAdvance => Self::default_steam_deck(), + BatteryLimitType::SteamDeck | BatteryLimitType::SteamDeckOLED => Self::default_steam_deck(), BatteryLimitType::DevMode => Self::default_dev_mode(), _t => Self::default(), } diff --git a/backend/limits_core/src/json_v2/cpu_limit.rs b/backend/limits_core/src/json_v2/cpu_limit.rs index e391078..58af870 100644 --- a/backend/limits_core/src/json_v2/cpu_limit.rs +++ b/backend/limits_core/src/json_v2/cpu_limit.rs @@ -7,8 +7,8 @@ use super::RangeLimit; pub enum CpuLimitType { #[serde(rename = "GabeBoy", alias = "SteamDeck")] SteamDeck, - #[serde(rename = "GabeBoyAdvance", alias = "SteamDeckAdvance")] - SteamDeckAdvance, + #[serde(rename = "GabeBoySP", alias = "SteamDeckOLED")] + SteamDeckOLED, Generic, GenericAMD, Unknown, @@ -25,7 +25,7 @@ pub struct GenericCpusLimit { impl GenericCpusLimit { pub fn default_for(t: CpuLimitType) -> Self { match t { - CpuLimitType::SteamDeck | CpuLimitType::SteamDeckAdvance => { + CpuLimitType::SteamDeck | CpuLimitType::SteamDeckOLED => { Self { cpus: [(); 8].iter().enumerate().map(|(i, _)| GenericCpuLimit::default_for(&t, i)).collect(), global_governors: true, @@ -96,7 +96,7 @@ pub struct GenericCpuLimit { impl GenericCpuLimit { pub fn default_for(t: &CpuLimitType, _index: usize) -> Self { match t { - CpuLimitType::SteamDeck | CpuLimitType::SteamDeckAdvance => Self::default_steam_deck(), + CpuLimitType::SteamDeck | CpuLimitType::SteamDeckOLED => Self::default_steam_deck(), CpuLimitType::DevMode => Self { clock_min: Some(RangeLimit { min: Some(100), max: Some(5000) }), clock_max: Some(RangeLimit { min: Some(100), max: Some(4800) }), diff --git a/backend/limits_core/src/json_v2/gpu_limit.rs b/backend/limits_core/src/json_v2/gpu_limit.rs index 1662149..f7d1dbe 100644 --- a/backend/limits_core/src/json_v2/gpu_limit.rs +++ b/backend/limits_core/src/json_v2/gpu_limit.rs @@ -6,8 +6,6 @@ use super::RangeLimit; pub enum GpuLimitType { #[serde(rename = "GabeBoy", alias = "SteamDeck")] SteamDeck, - #[serde(rename = "GabeBoyAdvance", alias = "SteamDeckAdvance")] - SteamDeckAdvance, #[serde(rename = "GabeBoySP", alias = "SteamDeckOLED")] SteamDeckOLED, Generic, @@ -40,7 +38,7 @@ pub struct GenericGpuLimit { impl GenericGpuLimit { pub fn default_for(t: GpuLimitType) -> Self { match t { - GpuLimitType::SteamDeck | GpuLimitType::SteamDeckAdvance => Self::default_steam_deck(), + GpuLimitType::SteamDeck => Self::default_steam_deck(), GpuLimitType::SteamDeckOLED => Self::default_steam_deck_oled(), GpuLimitType::DevMode => Self::default_dev_mode(), _t => Self::default(), diff --git a/backend/src/main.rs b/backend/src/main.rs index 4ceab20..3a52085 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -337,7 +337,7 @@ fn main() -> Result<(), ()> { #[cfg(debug_assertions)] std::thread::spawn(|| { utility::ioperm_power_ec(); - settings::steam_deck::util::flash_led(); + settings::steam_deck::flash_led(); }); if let Err(e) = loaded_settings.on_set() { diff --git a/backend/src/persist/driver.rs b/backend/src/persist/driver.rs index 56f50d6..7e18a8b 100644 --- a/backend/src/persist/driver.rs +++ b/backend/src/persist/driver.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; pub enum DriverJson { #[serde(rename = "steam-deck", alias = "gabe-boy")] SteamDeck, - #[serde(rename = "steam-deck-oc", alias = "gabe-boy-advance")] - SteamDeckAdvance, + #[serde(rename = "steam-deck-oled", alias = "gabe-boy-sp")] + SteamDeckOLED, #[serde(rename = "generic")] Generic, #[serde(rename = "generic-amd")] diff --git a/backend/src/settings/detect/auto_detect.rs b/backend/src/settings/detect/auto_detect.rs index a9f8b92..31ded4d 100644 --- a/backend/src/settings/detect/auto_detect.rs +++ b/backend/src/settings/detect/auto_detect.rs @@ -156,14 +156,14 @@ pub fn auto_detect0( settings.cpus.clone(), settings.version, relevant_limits.cpu.limits, - )) + ).variant(super::super::steam_deck::Model::LCD)) } - CpuLimitType::SteamDeckAdvance => { + CpuLimitType::SteamDeckOLED => { Box::new(crate::settings::steam_deck::Cpus::from_json_and_limits( settings.cpus.clone(), settings.version, relevant_limits.cpu.limits, - )) + ).variant(super::super::steam_deck::Model::OLED)) } CpuLimitType::Generic => Box::new(crate::settings::generic::Cpus::< crate::settings::generic::Cpu, @@ -201,21 +201,14 @@ pub fn auto_detect0( settings.gpu.clone(), settings.version, relevant_limits.gpu.limits, - )) - } - GpuLimitType::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Gpu::from_json_and_limits( - settings.gpu.clone(), - settings.version, - relevant_limits.gpu.limits, - )) + ).variant(super::super::steam_deck::Model::LCD)) } GpuLimitType::SteamDeckOLED => { Box::new(crate::settings::steam_deck::Gpu::from_json_and_limits( settings.gpu.clone(), settings.version, relevant_limits.gpu.limits, - )) + ).variant(super::super::steam_deck::Model::OLED)) } GpuLimitType::Generic => { Box::new(crate::settings::generic::Gpu::from_json_and_limits( @@ -252,14 +245,14 @@ pub fn auto_detect0( settings.battery.clone(), settings.version, relevant_limits.battery.limits, - )) + ).variant(super::super::steam_deck::Model::LCD)) } - BatteryLimitType::SteamDeckAdvance => { + BatteryLimitType::SteamDeckOLED => { Box::new(crate::settings::steam_deck::Battery::from_json_and_limits( settings.battery.clone(), settings.version, relevant_limits.battery.limits, - )) + ).variant(super::super::steam_deck::Model::OLED)) } BatteryLimitType::Generic => Box::new( crate::settings::generic::Battery::from_json_and_limits( @@ -293,10 +286,10 @@ pub fn auto_detect0( } else { let cpu_driver: Box = match relevant_limits.cpu.provider { CpuLimitType::SteamDeck => { - Box::new(crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits)) + Box::new(crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits).variant(super::super::steam_deck::Model::LCD)) } - CpuLimitType::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits)) + CpuLimitType::SteamDeckOLED => { + Box::new(crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits).variant(super::super::steam_deck::Model::OLED)) } CpuLimitType::Generic => { Box::new(crate::settings::generic::Cpus::< @@ -315,13 +308,10 @@ pub fn auto_detect0( }; let gpu_driver: Box = match relevant_limits.gpu.provider { GpuLimitType::SteamDeck => { - Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits)) - } - GpuLimitType::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits)) + Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits).variant(super::super::steam_deck::Model::LCD)) } GpuLimitType::SteamDeckOLED => { - Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits)) + Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits).variant(super::super::steam_deck::Model::OLED)) } GpuLimitType::Generic => { Box::new(crate::settings::generic::Gpu::from_limits(relevant_limits.gpu.limits)) @@ -338,10 +328,10 @@ pub fn auto_detect0( }; let battery_driver: Box = match relevant_limits.battery.provider { BatteryLimitType::SteamDeck => { - Box::new(crate::settings::steam_deck::Battery::from_limits(relevant_limits.battery.limits)) + Box::new(crate::settings::steam_deck::Battery::from_limits(relevant_limits.battery.limits).variant(super::super::steam_deck::Model::LCD)) } - BatteryLimitType::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Battery::from_limits(relevant_limits.battery.limits)) + BatteryLimitType::SteamDeckOLED => { + Box::new(crate::settings::steam_deck::Battery::from_limits(relevant_limits.battery.limits).variant(super::super::steam_deck::Model::OLED)) } BatteryLimitType::Generic => { Box::new(crate::settings::generic::Battery::from_limits(relevant_limits.battery.limits)) diff --git a/backend/src/settings/driver.rs b/backend/src/settings/driver.rs index 73eb1f1..adae096 100644 --- a/backend/src/settings/driver.rs +++ b/backend/src/settings/driver.rs @@ -29,7 +29,7 @@ impl Driver { #[inline] pub fn maybe_do_button() { match super::auto_detect_provider() { - DriverJson::SteamDeck | DriverJson::SteamDeckAdvance => { + DriverJson::SteamDeck | DriverJson::SteamDeckOLED => { crate::settings::steam_deck::flash_led(); } DriverJson::Generic | DriverJson::GenericAMD => { diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index e36b127..c0325eb 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -23,6 +23,7 @@ pub struct Battery { sysfs_bat: PowerSupplyPath, sysfs_hwmon: Arc, bat_ec: Arc>, + variant: super::Model, } #[derive(Debug, Clone)] @@ -542,6 +543,11 @@ impl Battery { self.events.clone() } } + + pub fn variant(mut self, model: super::Model) -> Self { + self.variant = model; + self + } } impl Into for Battery { @@ -579,6 +585,7 @@ impl ProviderBuilder for Battery { sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), sysfs_hwmon: hwmon_sys, bat_ec: ec, + variant: super::Model::LCD, }.remove_charge_limit_instructions(), _ => Self { charge_rate: persistent.charge_rate, @@ -597,6 +604,7 @@ impl ProviderBuilder for Battery { sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), sysfs_hwmon: hwmon_sys, bat_ec: ec, + variant: super::Model::LCD, }.remove_charge_limit_instructions(), } } @@ -612,6 +620,7 @@ impl ProviderBuilder for Battery { sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), sysfs_hwmon: Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)), bat_ec: Arc::new(Mutex::new(UnnamedPowerEC::new())), + variant: super::Model::LCD, }.remove_charge_limit_instructions() } } @@ -806,6 +815,9 @@ impl TBattery for Battery { } fn provider(&self) -> crate::persist::DriverJson { - crate::persist::DriverJson::SteamDeck + match self.variant { + super::Model::LCD => crate::persist::DriverJson::SteamDeck, + super::Model::OLED => crate::persist::DriverJson::SteamDeckOLED, + } } } diff --git a/backend/src/settings/steam_deck/cpu.rs b/backend/src/settings/steam_deck/cpu.rs index 3a5cc56..34671c5 100644 --- a/backend/src/settings/steam_deck/cpu.rs +++ b/backend/src/settings/steam_deck/cpu.rs @@ -30,6 +30,7 @@ pub struct Cpus { pub smt: bool, pub smt_capable: bool, pub(super) limits: GenericCpusLimit, + variant: super::Model, } impl OnSet for Cpus { @@ -100,6 +101,11 @@ impl Cpus { Err(_) => (false, false), } } + + pub fn variant(mut self, model: super::Model) -> Self { + self.variant = model; + self + } } impl ProviderBuilder, GenericCpusLimit> for Cpus { @@ -144,6 +150,7 @@ impl ProviderBuilder, GenericCpusLimit> for Cpus { smt: smt_guess, smt_capable: can_smt, limits: limits, + variant: super::Model::LCD, } } @@ -168,6 +175,7 @@ impl ProviderBuilder, GenericCpusLimit> for Cpus { smt: true, smt_capable: can_smt, limits: limits, + variant: super::Model::LCD, } } else { Self { @@ -175,6 +183,7 @@ impl ProviderBuilder, GenericCpusLimit> for Cpus { smt: false, smt_capable: false, limits: limits, + variant: super::Model::LCD, } } } @@ -218,7 +227,10 @@ impl TCpus for Cpus { } fn provider(&self) -> crate::persist::DriverJson { - crate::persist::DriverJson::SteamDeck + match self.variant { + super::Model::LCD => crate::persist::DriverJson::SteamDeck, + super::Model::OLED => crate::persist::DriverJson::SteamDeckOLED, + } } } diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index fe918eb..e8aafb0 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -24,7 +24,8 @@ pub struct Gpu { limits: GenericGpuLimit, state: crate::state::steam_deck::Gpu, sysfs_card: BasicEntityPath, - sysfs_hwmon: HwMonPath + sysfs_hwmon: HwMonPath, + variant: super::Model, } // same as CPU @@ -346,6 +347,11 @@ impl Gpu { self.memory_clock = Some(mem_clock.clamp(self.limits.memory_clock.and_then(|lim| lim.min).unwrap_or(MIN_MEMORY_CLOCK), self.limits.memory_clock.and_then(|lim| lim.max).unwrap_or(MAX_MEMORY_CLOCK))); } } + + pub fn variant(mut self, model: super::Model) -> Self { + self.variant = model; + self + } } impl Into for Gpu { @@ -375,6 +381,7 @@ impl ProviderBuilder for Gpu { state: crate::state::steam_deck::Gpu::default(), sysfs_card: Self::find_card_sysfs(persistent.root.clone()), sysfs_hwmon: Self::find_hwmon_sysfs(persistent.root), + variant: super::Model::LCD, }, _ => Self { fast_ppt: persistent.fast_ppt, @@ -385,6 +392,7 @@ impl ProviderBuilder for Gpu { state: crate::state::steam_deck::Gpu::default(), sysfs_card: Self::find_card_sysfs(persistent.root.clone()), sysfs_hwmon: Self::find_hwmon_sysfs(persistent.root), + variant: super::Model::LCD, }, } } @@ -399,6 +407,7 @@ impl ProviderBuilder for Gpu { state: crate::state::steam_deck::Gpu::default(), sysfs_card: Self::find_card_sysfs(None::<&'static str>), sysfs_hwmon: Self::find_hwmon_sysfs(None::<&'static str>), + variant: super::Model::LCD, } } } @@ -485,7 +494,10 @@ impl TGpu for Gpu { } fn provider(&self) -> crate::persist::DriverJson { - crate::persist::DriverJson::SteamDeck + match self.variant { + super::Model::LCD => crate::persist::DriverJson::SteamDeck, + super::Model::OLED => crate::persist::DriverJson::SteamDeckOLED, + } } } diff --git a/backend/src/settings/steam_deck/mod.rs b/backend/src/settings/steam_deck/mod.rs index a87f005..5adc6c6 100644 --- a/backend/src/settings/steam_deck/mod.rs +++ b/backend/src/settings/steam_deck/mod.rs @@ -2,9 +2,6 @@ mod battery; mod cpu; mod gpu; mod power_dpm_force; -#[cfg(debug_assertions)] -pub mod util; -#[cfg(not(debug_assertions))] mod util; pub use battery::Battery; @@ -12,6 +9,12 @@ pub use cpu::Cpus; pub use gpu::Gpu; pub(self) use power_dpm_force::{POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT, DPM_FORCE_LIMITS_ATTRIBUTE}; +#[derive(Debug, Clone, Copy)] +pub enum Model { + LCD, + OLED, +} + pub use util::flash_led; fn _impl_checker() { From 59a0727f88f38b0d1a1ca491bf1966f39b951c3f Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 29 Jan 2024 21:32:18 -0500 Subject: [PATCH 33/56] cargo fmt and fix profile variant loading --- backend/src/api/api_types.rs | 1 + backend/src/api/general.rs | 148 +++++++--- backend/src/api/handler.rs | 63 +++- backend/src/api/message.rs | 37 ++- backend/src/api/web.rs | 172 +++++++---- backend/src/consts.rs | 1 - backend/src/main.rs | 49 ++-- backend/src/persist/file.rs | 61 ++-- backend/src/persist/general.rs | 2 +- backend/src/settings/detect/auto_detect.rs | 206 +++++++------ backend/src/settings/detect/limits_worker.rs | 18 +- backend/src/settings/detect/utility.rs | 2 +- backend/src/settings/dev_mode/battery.rs | 40 ++- backend/src/settings/dev_mode/cpu.rs | 79 ++++- backend/src/settings/dev_mode/gpu.rs | 72 ++++- backend/src/settings/dev_mode/mod.rs | 12 +- backend/src/settings/driver.rs | 8 +- backend/src/settings/general.rs | 134 +++++++-- backend/src/settings/generic/battery.rs | 59 ++-- backend/src/settings/generic/cpu.rs | 31 +- backend/src/settings/generic/gpu.rs | 119 +++++--- backend/src/settings/generic/mod.rs | 12 +- backend/src/settings/generic_amd/cpu.rs | 2 +- backend/src/settings/generic_amd/gpu.rs | 48 ++- backend/src/settings/generic_amd/mod.rs | 6 +- backend/src/settings/mod.rs | 20 +- backend/src/settings/steam_deck/battery.rs | 182 ++++++++---- backend/src/settings/steam_deck/cpu.rs | 169 +++++++---- backend/src/settings/steam_deck/gpu.rs | 273 +++++++++++++----- backend/src/settings/steam_deck/mod.rs | 16 +- .../settings/steam_deck/power_dpm_force.rs | 22 +- backend/src/settings/steam_deck/util.rs | 19 +- backend/src/settings/traits.rs | 5 +- backend/src/settings/unknown/battery.rs | 8 +- backend/src/settings/unknown/cpu.rs | 10 +- backend/src/settings/unknown/gpu.rs | 4 +- backend/src/settings/unknown/mod.rs | 12 +- backend/src/settings/util.rs | 5 +- backend/src/utility.rs | 71 +++-- src/index.tsx | 162 ++++++----- 40 files changed, 1640 insertions(+), 720 deletions(-) diff --git a/backend/src/api/api_types.rs b/backend/src/api/api_types.rs index 8854656..7d8e6bf 100644 --- a/backend/src/api/api_types.rs +++ b/backend/src/api/api_types.rs @@ -67,4 +67,5 @@ pub struct GpuLimits { pub struct VariantInfo { pub id: String, pub name: String, + pub id_num: u64, } diff --git a/backend/src/api/general.rs b/backend/src/api/general.rs index 2fb27d4..feda6af 100644 --- a/backend/src/api/general.rs +++ b/backend/src/api/general.rs @@ -59,7 +59,13 @@ pub fn load_settings( sender .lock() .unwrap() - .send(ApiMessage::LoadSettings(id, name, variant, variant_name.unwrap_or_else(|| crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned()))) + .send(ApiMessage::LoadSettings( + id, + name, + variant, + variant_name + .unwrap_or_else(|| crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned()), + )) .expect("load_settings send failed") }; move |params_in: super::ApiParameterType| { @@ -67,16 +73,20 @@ pub fn load_settings( if let Some(Primitive::String(name)) = params_in.get(1) { if let Some(Primitive::String(variant_id)) = params_in.get(2) { if let Some(Primitive::String(variant_name)) = params_in.get(3) { - setter(id.parse().unwrap_or_default(), - name.to_owned(), - variant_id.parse().unwrap_or_default(), - Some(variant_name.to_owned())); + setter( + id.parse().unwrap_or_default(), + name.to_owned(), + variant_id.parse().unwrap_or_default(), + Some(variant_name.to_owned()), + ); vec![true.into()] } else { - setter(id.parse().unwrap_or_default(), - name.to_owned(), - variant_id.parse().unwrap_or_default(), - None); + setter( + id.parse().unwrap_or_default(), + name.to_owned(), + variant_id.parse().unwrap_or_default(), + None, + ); vec![true.into()] } } else { @@ -101,26 +111,32 @@ pub fn load_variant( ) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety let setter = move |variant: u64, variant_name: Option| { + log::debug!("load_variant(variant: {}, variant_name: {:?})", variant, variant_name); sender .lock() .unwrap() - .send(ApiMessage::LoadVariant(variant, variant_name.unwrap_or_else(|| crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned()))) - .expect("load_settings send failed") + .send(ApiMessage::LoadVariant( + variant, + variant_name + .unwrap_or_else(|| "".to_owned()), + )) + .expect("load_variant send failed") }; move |params_in: super::ApiParameterType| { if let Some(Primitive::String(variant_id)) = params_in.get(0) { if let Some(Primitive::String(variant_name)) = params_in.get(1) { - setter(variant_id.parse().unwrap_or(u64::MAX), - Some(variant_name.to_owned())); + setter( + variant_id.parse().unwrap_or(u64::MAX), + Some(variant_name.to_owned()), + ); vec![true.into()] } else { - setter(variant_id.parse().unwrap_or_default(), - None); + setter(variant_id.parse().unwrap_or(u64::MAX), None); vec![true.into()] } } else { - log::warn!("load_settings missing variant id parameter"); - vec!["load_settings missing variant id parameter".into()] + log::warn!("load_variant missing variant id parameter"); + vec!["load_variant missing variant id parameter".into()] } } } @@ -375,32 +391,60 @@ pub fn get_periodicals(sender: Sender) -> impl AsyncCallable { let sender2 = sender.clone(); move || { let (rx_curr, callback_curr) = build_comms("battery current callback send failed"); - let (rx_charge_now, callback_charge_now) = build_comms("battery charge now callback send failed"); - let (rx_charge_full, callback_charge_full) = build_comms("battery charge full callback send failed"); - let (rx_charge_power, callback_charge_power) = build_comms("battery charge power callback send failed"); + let (rx_charge_now, callback_charge_now) = + build_comms("battery charge now callback send failed"); + let (rx_charge_full, callback_charge_full) = + build_comms("battery charge full callback send failed"); + let (rx_charge_power, callback_charge_power) = + build_comms("battery charge power callback send failed"); let (rx_path, callback_path) = build_comms("general get path (periodical) send failed"); - let sender_locked = sender2 - .lock() - .unwrap(); - let curr = wait_for_response(&*sender_locked, rx_curr, - ApiMessage::Battery(super::handler::BatteryMessage::ReadCurrentNow(callback_curr)), "battery current"); - let charge_now = wait_for_response(&*sender_locked, rx_charge_now, - ApiMessage::Battery(super::handler::BatteryMessage::ReadChargeNow(callback_charge_now)), "battery charge now"); - let charge_full = wait_for_response(&*sender_locked, rx_charge_full, - ApiMessage::Battery(super::handler::BatteryMessage::ReadChargeFull(callback_charge_full)), "battery charge full"); - let charge_power = wait_for_response(&*sender_locked, rx_charge_power, - ApiMessage::Battery(super::handler::BatteryMessage::ReadChargePower(callback_charge_power)), "battery charge power"); + let sender_locked = sender2.lock().unwrap(); + let curr = wait_for_response( + &*sender_locked, + rx_curr, + ApiMessage::Battery(super::handler::BatteryMessage::ReadCurrentNow( + callback_curr, + )), + "battery current", + ); + let charge_now = wait_for_response( + &*sender_locked, + rx_charge_now, + ApiMessage::Battery(super::handler::BatteryMessage::ReadChargeNow( + callback_charge_now, + )), + "battery charge now", + ); + let charge_full = wait_for_response( + &*sender_locked, + rx_charge_full, + ApiMessage::Battery(super::handler::BatteryMessage::ReadChargeFull( + callback_charge_full, + )), + "battery charge full", + ); + let charge_power = wait_for_response( + &*sender_locked, + rx_charge_power, + ApiMessage::Battery(super::handler::BatteryMessage::ReadChargePower( + callback_charge_power, + )), + "battery charge power", + ); - let settings_path = wait_for_response(&*sender_locked, rx_path, - ApiMessage::General(GeneralMessage::GetPath(callback_path)), "general get path"); + let settings_path = wait_for_response( + &*sender_locked, + rx_path, + ApiMessage::General(GeneralMessage::GetPath(callback_path)), + "general get path", + ); vec![ super::utility::map_optional(curr), super::utility::map_optional(charge_now), super::utility::map_optional(charge_full), super::utility::map_optional(charge_power), - super::utility::map_optional(settings_path.to_str()), ] } @@ -411,13 +455,20 @@ pub fn get_periodicals(sender: Sender) -> impl AsyncCallable { } } -fn build_comms<'a, T: Send + 'a>(msg: &'static str) -> (mpsc::Receiver, Box) { +fn build_comms<'a, T: Send + 'a>( + msg: &'static str, +) -> (mpsc::Receiver, Box) { let (tx, rx) = mpsc::channel(); let callback = move |t: T| tx.send(t).expect(msg); (rx, Box::new(callback)) } -fn wait_for_response(sender: &Sender, rx: mpsc::Receiver, api_msg: ApiMessage, op: &str) -> T { +fn wait_for_response( + sender: &Sender, + rx: mpsc::Receiver, + api_msg: ApiMessage, + op: &str, +) -> T { sender.send(api_msg).expect(&format!("{} send failed", op)); rx.recv().expect(&format!("{} callback recv failed", op)) } @@ -429,8 +480,10 @@ pub fn get_all_variants(sender: Sender) -> impl AsyncCallable { let sender2 = sender.clone(); move || { let (tx, rx) = mpsc::channel(); - let callback = - move |variants: Vec| tx.send(variants).expect("get_all_variants callback send failed"); + let callback = move |variants: Vec| { + tx.send(variants) + .expect("get_all_variants callback send failed") + }; sender2 .lock() .unwrap() @@ -438,7 +491,9 @@ pub fn get_all_variants(sender: Sender) -> impl AsyncCallable { Box::new(callback), ))) .expect("get_all_variants send failed"); - rx.recv().expect("get_all_variants callback recv failed") + let mut results = rx.recv().expect("get_all_variants callback recv failed"); + results.sort_by_key(|info| info.id_num); // sort by variant id + results } }; super::async_utils::AsyncIshGetter { @@ -446,7 +501,10 @@ pub fn get_all_variants(sender: Sender) -> impl AsyncCallable { trans_getter: |result| { let mut output = Vec::with_capacity(result.len()); for status in result.iter() { - output.push(Primitive::Json(serde_json::to_string(status).expect("Failed to serialize variant info to JSON"))); + output.push(Primitive::Json( + serde_json::to_string(status) + .expect("Failed to serialize variant info to JSON"), + )); } output }, @@ -460,8 +518,10 @@ pub fn get_current_variant(sender: Sender) -> impl AsyncCallable { let sender2 = sender.clone(); move || { let (tx, rx) = mpsc::channel(); - let callback = - move |variant: super::VariantInfo| tx.send(variant).expect("get_all_variants callback send failed"); + let callback = move |variant: super::VariantInfo| { + tx.send(variant) + .expect("get_all_variants callback send failed") + }; sender2 .lock() .unwrap() @@ -475,7 +535,9 @@ pub fn get_current_variant(sender: Sender) -> impl AsyncCallable { super::async_utils::AsyncIshGetter { set_get: getter, trans_getter: |result| { - vec![Primitive::Json(serde_json::to_string(&result).expect("Failed to serialize variant info to JSON"))] + vec![Primitive::Json( + serde_json::to_string(&result).expect("Failed to serialize variant info to JSON"), + )] }, } } diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index 95fba2c..194b179 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -66,7 +66,10 @@ impl BatteryMessage { /// Message instructs the driver to modify settings fn is_modify(&self) -> bool { - matches!(self, Self::SetChargeRate(_) | Self::SetChargeMode(_) | Self::SetChargeLimit(_)) + matches!( + self, + Self::SetChargeRate(_) | Self::SetChargeMode(_) | Self::SetChargeLimit(_) + ) } } @@ -231,7 +234,10 @@ pub enum GeneralMessage { GetPath(Callback), GetCurrentVariant(Callback), GetAllVariants(Callback>), - AddVariant(crate::persist::SettingsJson, Callback>), + AddVariant( + crate::persist::SettingsJson, + Callback>, + ), ApplyNow, } @@ -248,11 +254,14 @@ impl GeneralMessage { Self::AddVariant(variant, cb) => match settings.add_variant(variant) { Ok(variants) => cb(variants), Err(e) => { - print_errors("GeneralMessage::AddVariant => TGeneral::add_variant", vec![e]); + print_errors( + "GeneralMessage::AddVariant => TGeneral::add_variant", + vec![e], + ); cb(Vec::with_capacity(0)) - }, + } }, - Self::ApplyNow => {}, + Self::ApplyNow => {} } dirty } @@ -300,13 +309,21 @@ impl ApiMessageHandler { // save log::debug!("api_worker is saving..."); let is_persistent = *settings.general.persistent(); - let save_path = - crate::utility::settings_dir().join(settings.general.get_path()); + let save_path = crate::utility::settings_dir().join(settings.general.get_path()); if is_persistent { let settings_clone = settings.json(); let save_json: SettingsJson = settings_clone.into(); - if let Err(e) = crate::persist::FileJson::update_variant_or_create(&save_path, settings.general.get_app_id(), save_json, settings.general.get_name().to_owned()) { - log::error!("Failed to create/update settings file {}: {}", save_path.display(), e); + if let Err(e) = crate::persist::FileJson::update_variant_or_create( + &save_path, + settings.general.get_app_id(), + save_json, + settings.general.get_name().to_owned(), + ) { + log::error!( + "Failed to create/update settings file {}: {}", + save_path.display(), + e + ); } //unwrap_maybe_fatal(save_json.save(&save_path), "Failed to save settings"); log::debug!("Saved settings to {}", save_path.display()); @@ -393,8 +410,15 @@ impl ApiMessageHandler { ApiMessage::LoadVariant(variant_id, variant_name) => { let path = settings.general.get_path(); let app_id = settings.general.get_app_id(); - match settings.load_file(path.into(), app_id, settings.general.get_name().to_owned(), variant_id, variant_name, false) { - Ok(success) => log::info!("Loaded settings file? {}", success), + match settings.load_file( + path.into(), + app_id, + settings.general.get_name().to_owned(), + variant_id, + variant_name, + false, + ) { + Ok(success) => log::info!("Loaded variant settings file? {}", success), Err(e) => log::warn!("Load file err: {}", e), } true @@ -414,7 +438,11 @@ impl ApiMessageHandler { true } ApiMessage::LoadSystemSettings => { - settings.load_system_default(settings.general.get_name().to_owned(), settings.general.get_variant_id(), settings.general.get_variant_info().name); + settings.load_system_default( + settings.general.get_name().to_owned(), + settings.general.get_variant_id(), + settings.general.get_variant_info().name, + ); true } ApiMessage::GetLimits(cb) => { @@ -434,13 +462,18 @@ impl ApiMessageHandler { _ => settings.general.provider(), }); false - }, + } ApiMessage::UploadCurrentVariant(steam_id, steam_username) => { //TODO let steam_app_id = settings.general.get_app_id(); - super::web::upload_settings(steam_app_id, steam_id, steam_username, settings.json()); + super::web::upload_settings( + steam_app_id, + steam_id, + steam_username, + settings.json(), + ); false - }, + } } } diff --git a/backend/src/api/message.rs b/backend/src/api/message.rs index 3ac093e..0a7b435 100644 --- a/backend/src/api/message.rs +++ b/backend/src/api/message.rs @@ -1,14 +1,17 @@ -use std::sync::{atomic::{AtomicU64, Ordering}, Arc}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; use serde::{Deserialize, Serialize}; -use usdpl_back::AsyncCallable; use usdpl_back::core::serdes::Primitive; +use usdpl_back::AsyncCallable; use limits_core::json::DeveloperMessage; -use crate::MESSAGE_SEEN_ID_FILE; use crate::utility::settings_dir; +use crate::MESSAGE_SEEN_ID_FILE; #[derive(Serialize, Deserialize)] pub struct ApiMessage { @@ -34,7 +37,10 @@ impl std::convert::From for ApiMessage { } fn get_dev_messages() -> Vec { - crate::settings::get_dev_messages().drain(..).map(|msg| ApiMessage::from(msg)).collect() + crate::settings::get_dev_messages() + .drain(..) + .map(|msg| ApiMessage::from(msg)) + .collect() } pub struct MessageHandler { @@ -43,8 +49,12 @@ pub struct MessageHandler { impl MessageHandler { pub fn new() -> Self { - let last_seen_id = if let Ok(last_seen_id_bytes) = std::fs::read(settings_dir().join(MESSAGE_SEEN_ID_FILE)) { - if last_seen_id_bytes.len() >= 8 /* bytes in u64 */ { + let last_seen_id = if let Ok(last_seen_id_bytes) = + std::fs::read(settings_dir().join(MESSAGE_SEEN_ID_FILE)) + { + if last_seen_id_bytes.len() >= 8 + /* bytes in u64 */ + { u64::from_le_bytes([ last_seen_id_bytes[0], last_seen_id_bytes[1], @@ -73,7 +83,7 @@ impl MessageHandler { }, AsyncMessageDismisser { seen: self.seen.clone(), - } + }, ) } } @@ -83,8 +93,17 @@ pub struct AsyncMessageGetter { } impl AsyncMessageGetter { - fn remove_before_id(id: u64, messages: impl Iterator) -> impl Iterator { - messages.skip_while(move |msg| if let Some(msg_id) = msg.id { msg_id <= id } else { true }) + fn remove_before_id( + id: u64, + messages: impl Iterator, + ) -> impl Iterator { + messages.skip_while(move |msg| { + if let Some(msg_id) = msg.id { + msg_id <= id + } else { + true + } + }) } } diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index 81d5cb0..385ec92 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -14,12 +14,17 @@ pub fn search_by_app_id() -> impl AsyncCallable { let req_url = format!("{}/api/setting/by_app_id/{}", BASE_URL, steam_app_id); match ureq::get(&req_url).call() { Ok(response) => { - let json_res: std::io::Result> = response.into_json(); + let json_res: std::io::Result> = + response.into_json(); match json_res { Ok(search_results) => { // search results may be quite large, so let's do the JSON string conversion in the background (blocking) thread match serde_json::to_string(&search_results) { - Err(e) => log::error!("Cannot convert search results from `{}` to JSON: {}", req_url, e), + Err(e) => log::error!( + "Cannot convert search results from `{}` to JSON: {}", + req_url, + e + ), Ok(s) => return s, } } @@ -46,41 +51,58 @@ pub fn search_by_app_id() -> impl AsyncCallable { } } -fn web_config_to_settings_json(meta: community_settings_core::v1::Metadata) -> crate::persist::SettingsJson { +fn web_config_to_settings_json( + meta: community_settings_core::v1::Metadata, +) -> crate::persist::SettingsJson { crate::persist::SettingsJson { version: crate::persist::LATEST_VERSION, name: meta.name, variant: u64::MAX, // TODO maybe change this to use the 64 low bits of id (u64::MAX will cause it to generate a new id when added to file variant map persistent: true, - cpus: meta.config.cpus.into_iter().map(|cpu| crate::persist::CpuJson { - online: cpu.online, - clock_limits: cpu.clock_limits.map(|lim| crate::persist::MinMaxJson { - min: lim.min, - max: lim.max, - }), - governor: cpu.governor, - root: None, - }).collect(), + cpus: meta + .config + .cpus + .into_iter() + .map(|cpu| crate::persist::CpuJson { + online: cpu.online, + clock_limits: cpu.clock_limits.map(|lim| crate::persist::MinMaxJson { + min: lim.min, + max: lim.max, + }), + governor: cpu.governor, + root: None, + }) + .collect(), gpu: crate::persist::GpuJson { fast_ppt: meta.config.gpu.fast_ppt, slow_ppt: meta.config.gpu.slow_ppt, tdp: meta.config.gpu.tdp, tdp_boost: meta.config.gpu.tdp_boost, - clock_limits: meta.config.gpu.clock_limits.map(|lim| crate::persist::MinMaxJson { - min: lim.min, - max: lim.max, - }), + clock_limits: meta + .config + .gpu + .clock_limits + .map(|lim| crate::persist::MinMaxJson { + min: lim.min, + max: lim.max, + }), memory_clock: meta.config.gpu.memory_clock, root: None, }, battery: crate::persist::BatteryJson { charge_rate: meta.config.battery.charge_rate, charge_mode: meta.config.battery.charge_mode, - events: meta.config.battery.events.into_iter().map(|be| crate::persist::BatteryEventJson { - charge_rate: be.charge_rate, - charge_mode: be.charge_mode, - trigger: be.trigger, - }).collect(), + events: meta + .config + .battery + .events + .into_iter() + .map(|be| crate::persist::BatteryEventJson { + charge_rate: be.charge_rate, + charge_mode: be.charge_mode, + trigger: be.trigger, + }) + .collect(), root: None, }, provider: Some(crate::persist::DriverJson::AutoDetect), @@ -89,20 +111,33 @@ fn web_config_to_settings_json(meta: community_settings_core::v1::Metadata) -> c fn download_config(id: u128) -> std::io::Result { let req_url = format!("{}/api/setting/by_id/{}", BASE_URL, id); - let response = ureq::get(&req_url).call() - .map_err(|e| { - log::warn!("GET to {} failed: {}", req_url, e); - std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e) - })?; + let response = ureq::get(&req_url).call().map_err(|e| { + log::warn!("GET to {} failed: {}", req_url, e); + std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e) + })?; response.into_json() } -pub fn upload_settings(id: u64, user_id: String, username: String, settings: crate::persist::SettingsJson) { - log::info!("Uploading settings {} by {} ({})", settings.name, username, user_id); +pub fn upload_settings( + id: u64, + user_id: String, + username: String, + settings: crate::persist::SettingsJson, +) { + log::info!( + "Uploading settings {} by {} ({})", + settings.name, + username, + user_id + ); let user_id: u64 = match user_id.parse() { Ok(id) => id, Err(e) => { - log::error!("Failed to parse `{}` as u64: {} (aborted upload_settings very early)", user_id, e); + log::error!( + "Failed to parse `{}` as u64: {} (aborted upload_settings very early)", + user_id, + e + ); return; } }; @@ -112,7 +147,12 @@ pub fn upload_settings(id: u64, user_id: String, username: String, settings: cra } } -fn settings_to_web_config(app_id: u32, user_id: u64, username: String, settings: crate::persist::SettingsJson) -> community_settings_core::v1::Metadata { +fn settings_to_web_config( + app_id: u32, + user_id: u64, + username: String, + settings: crate::persist::SettingsJson, +) -> community_settings_core::v1::Metadata { community_settings_core::v1::Metadata { name: settings.name, steam_app_id: app_id, @@ -121,33 +161,46 @@ fn settings_to_web_config(app_id: u32, user_id: u64, username: String, settings: tags: vec!["wip".to_owned()], id: "".to_owned(), config: community_settings_core::v1::Config { - cpus: settings.cpus.into_iter().map(|cpu| community_settings_core::v1::Cpu { - online: cpu.online, - clock_limits: cpu.clock_limits.map(|lim| community_settings_core::v1::MinMax { - min: lim.min, - max: lim.max, - }), - governor: cpu.governor, - }).collect(), + cpus: settings + .cpus + .into_iter() + .map(|cpu| community_settings_core::v1::Cpu { + online: cpu.online, + clock_limits: cpu + .clock_limits + .map(|lim| community_settings_core::v1::MinMax { + min: lim.min, + max: lim.max, + }), + governor: cpu.governor, + }) + .collect(), gpu: community_settings_core::v1::Gpu { fast_ppt: settings.gpu.fast_ppt, slow_ppt: settings.gpu.slow_ppt, tdp: settings.gpu.tdp, tdp_boost: settings.gpu.tdp_boost, - clock_limits: settings.gpu.clock_limits.map(|lim| community_settings_core::v1::MinMax { - min: lim.min, - max: lim.max, + clock_limits: settings.gpu.clock_limits.map(|lim| { + community_settings_core::v1::MinMax { + min: lim.min, + max: lim.max, + } }), memory_clock: settings.gpu.memory_clock, }, battery: community_settings_core::v1::Battery { charge_rate: settings.battery.charge_rate, charge_mode: settings.battery.charge_mode, - events: settings.battery.events.into_iter().map(|batt_ev| community_settings_core::v1::BatteryEvent { - trigger: batt_ev.trigger, - charge_rate: batt_ev.charge_rate, - charge_mode: batt_ev.charge_mode, - }).collect(), + events: settings + .battery + .events + .into_iter() + .map(|batt_ev| community_settings_core::v1::BatteryEvent { + trigger: batt_ev.trigger, + charge_rate: batt_ev.charge_rate, + charge_mode: batt_ev.charge_mode, + }) + .collect(), }, }, } @@ -173,15 +226,20 @@ pub fn download_new_config(sender: Sender) -> impl AsyncCallable { match download_config(id) { Ok(meta) => { let (tx, rx) = mpsc::channel(); - let callback = - move |values: Vec| tx.send(values).expect("download_new_config callback send failed"); + let callback = move |values: Vec| { + tx.send(values) + .expect("download_new_config callback send failed") + }; sender2 .lock() .unwrap() - .send(ApiMessage::General(GeneralMessage::AddVariant(web_config_to_settings_json(meta), Box::new(callback)))) + .send(ApiMessage::General(GeneralMessage::AddVariant( + web_config_to_settings_json(meta), + Box::new(callback), + ))) .expect("download_new_config send failed"); return rx.recv().expect("download_new_config callback recv failed"); - }, + } Err(e) => { log::error!("Invalid response from download: {}", e); } @@ -194,7 +252,10 @@ pub fn download_new_config(sender: Sender) -> impl AsyncCallable { if let Some(Primitive::String(id)) = params.get(0) { match id.parse::() { Ok(id) => Ok(id), - Err(e) => Err(format!("download_new_config non-u128 string parameter 0: {} (got `{}`)", e, id)) + Err(e) => Err(format!( + "download_new_config non-u128 string parameter 0: {} (got `{}`)", + e, id + )), } } else { Err("download_new_config missing/invalid parameter 0".to_owned()) @@ -204,7 +265,10 @@ pub fn download_new_config(sender: Sender) -> impl AsyncCallable { trans_getter: |result| { let mut output = Vec::with_capacity(result.len()); for status in result.iter() { - output.push(Primitive::Json(serde_json::to_string(status).expect("Failed to serialize variant info to JSON"))); + output.push(Primitive::Json( + serde_json::to_string(status) + .expect("Failed to serialize variant info to JSON"), + )); } output }, @@ -238,8 +302,6 @@ pub fn upload_current_variant(sender: Sender) -> impl AsyncCallable } }, set_get: getter, - trans_getter: |result| { - vec![result.into()] - }, + trans_getter: |result| vec![result.into()], } } diff --git a/backend/src/consts.rs b/backend/src/consts.rs index 531ce20..502f6ce 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -10,5 +10,4 @@ pub const DEFAULT_SETTINGS_VARIANT_NAME: &str = "Primary"; pub const LIMITS_FILE: &str = "limits_cache.ron"; pub const LIMITS_OVERRIDE_FILE: &str = "limits_override.ron"; - pub const MESSAGE_SEEN_ID_FILE: &str = "seen_message.bin"; diff --git a/backend/src/main.rs b/backend/src/main.rs index 3a52085..0a245ca 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -76,14 +76,29 @@ fn main() -> Result<(), ()> { let mut loaded_settings = persist::FileJson::open(utility::settings_dir().join(DEFAULT_SETTINGS_FILE)) - .map(|mut file| file.variants.remove(&0) - .map(|settings| settings::Settings::from_json(DEFAULT_SETTINGS_NAME.into(), settings, DEFAULT_SETTINGS_FILE.into(), 0)) - .unwrap_or_else(|| settings::Settings::system_default( - DEFAULT_SETTINGS_FILE.into(), - 0, - DEFAULT_SETTINGS_NAME.into(), - 0, - DEFAULT_SETTINGS_VARIANT_NAME.into()))) + .map(|mut file| { + let mut keys: Vec = file.variants.keys().map(|x| *x).collect(); + keys.sort(); + keys.get(0) + .and_then(|id| file.variants.remove(id)) + .map(|settings| { + settings::Settings::from_json( + DEFAULT_SETTINGS_NAME.into(), + settings, + DEFAULT_SETTINGS_FILE.into(), + 0, + ) + }) + .unwrap_or_else(|| { + settings::Settings::system_default( + DEFAULT_SETTINGS_FILE.into(), + 0, + DEFAULT_SETTINGS_NAME.into(), + 0, + DEFAULT_SETTINGS_VARIANT_NAME.into(), + ) + }) + }) .unwrap_or_else(|_| { settings::Settings::system_default( DEFAULT_SETTINGS_FILE.into(), @@ -96,7 +111,8 @@ fn main() -> Result<(), ()> { log::info!( "Detected device automatically {:?}, using driver: {:?} (This can be overriden)", - crate::settings::auto_detect_provider(), loaded_settings.cpus.provider() + crate::settings::auto_detect_provider(), + loaded_settings.cpus.provider() ); log::debug!("Settings: {:?}", loaded_settings); @@ -307,29 +323,26 @@ fn main() -> Result<(), ()> { ) .register_async( "GENERAL_get_periodicals", - api::general::get_periodicals(api_sender.clone()) + api::general::get_periodicals(api_sender.clone()), ) .register_async( "GENERAL_get_all_variants", - api::general::get_all_variants(api_sender.clone()) + api::general::get_all_variants(api_sender.clone()), ) .register_async( "GENERAL_get_current_variant", - api::general::get_current_variant(api_sender.clone()) + api::general::get_current_variant(api_sender.clone()), ) .register_async("MESSAGE_get", message_getter) .register_async("MESSAGE_dismiss", message_dismisser) - .register_async( - "WEB_search_by_app", - api::web::search_by_app_id() - ) + .register_async("WEB_search_by_app", api::web::search_by_app_id()) .register_async( "WEB_download_new", - api::web::download_new_config(api_sender.clone()) + api::web::download_new_config(api_sender.clone()), ) .register_async( "WEB_upload_new", - api::web::upload_current_variant(api_sender.clone()) + api::web::upload_current_variant(api_sender.clone()), ); utility::ioperm_power_ec(); diff --git a/backend/src/persist/file.rs b/backend/src/persist/file.rs index c4a4010..0999517 100644 --- a/backend/src/persist/file.rs +++ b/backend/src/persist/file.rs @@ -22,7 +22,8 @@ impl FileJson { std::fs::create_dir_all(parent).map_err(SerdeError::Io)?; } let mut file = std::fs::File::create(path).map_err(SerdeError::Io)?; - ron::ser::to_writer_pretty(&mut file, &self, crate::utility::ron_pretty_config()).map_err(|e| SerdeError::Serde(e.into())) + ron::ser::to_writer_pretty(&mut file, &self, crate::utility::ron_pretty_config()) + .map_err(|e| SerdeError::Serde(e.into())) } else { if path.exists() { // remove settings file when persistence is turned off, to prevent it from be loaded next time. @@ -39,37 +40,61 @@ impl FileJson { } fn next_available_id(&self) -> u64 { - self.variants.keys() - .max() - .map(|k| k+1) - .unwrap_or(0) + self.variants.keys().max().map(|k| k + 1).unwrap_or(0) } - pub fn update_variant_or_create>(path: P, app_id: u64, mut setting: SettingsJson, given_name: String) -> Result { - if !setting.persistent { - return Self::open(path) - } + pub fn update_variant_or_create>( + path: P, + app_id: u64, + mut setting: SettingsJson, + app_name: String, + ) -> Result<(Self, SettingsJson), SerdeError> { + // returns (Self, updated/created variant id) let path = path.as_ref(); - - let file = if path.exists() { + if !setting.persistent { let mut file = Self::open(path)?; + + if file.variants.contains_key(&setting.variant) { + file.variants.remove(&setting.variant); + file.save(path)?; + } + return Ok((file, setting)); + } + + let (file, variant_id) = if path.exists() { + let mut file = Self::open(path)?; + // Generate new (available) id if max if setting.variant == u64::MAX { setting.variant = file.next_available_id(); } - file.variants.insert(setting.variant, setting); - file + // Generate new name if empty + if setting.name.is_empty() { + setting.name = format!("Variant {}", setting.variant); + } + log::debug!("Inserting setting variant `{}` ({}) for app `{}` ({})", setting.name, setting.variant, file.name, app_id); + file.variants.insert(setting.variant, setting.clone()); + (file, setting) } else { + // Generate new id if max + if setting.variant == u64::MAX { + setting.variant = 1; + } + // Generate new name if empty + if setting.name.is_empty() { + setting.name = format!("Variant {}", setting.variant); + } + log::debug!("Creating new setting variant `{}` ({}) for app `{}` ({})", setting.name, setting.variant, app_name, app_id); let mut setting_variants = HashMap::with_capacity(1); - setting_variants.insert(setting.variant, setting); - Self { + setting_variants.insert(setting.variant, setting.clone()); + (Self { version: 0, app_id: app_id, - name: given_name, + name: app_name, variants: setting_variants, - } + }, setting) }; file.save(path)?; - Ok(file) + Ok((file, variant_id)) } } diff --git a/backend/src/persist/general.rs b/backend/src/persist/general.rs index d3f7ab4..fe18898 100644 --- a/backend/src/persist/general.rs +++ b/backend/src/persist/general.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use super::{BatteryJson, CpuJson, DriverJson, GpuJson}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct SettingsJson { pub version: u64, pub name: String, diff --git a/backend/src/settings/detect/auto_detect.rs b/backend/src/settings/detect/auto_detect.rs index 31ded4d..5ee8e4b 100644 --- a/backend/src/settings/detect/auto_detect.rs +++ b/backend/src/settings/detect/auto_detect.rs @@ -5,7 +5,7 @@ use regex::RegexBuilder; use limits_core::json_v2::{BatteryLimitType, CpuLimitType, GpuLimitType, Limits}; use crate::persist::{DriverJson, SettingsJson}; -use crate::settings::{Driver, General, TBattery, TCpus, TGeneral, TGpu, ProviderBuilder}; +use crate::settings::{Driver, General, ProviderBuilder, TBattery, TCpus, TGeneral, TGpu}; fn get_limits() -> limits_core::json_v2::Base { let limits_path = super::utility::limits_path(); @@ -151,20 +151,22 @@ pub fn auto_detect0( if let Some(settings) = &settings_opt { *general_driver.persistent() = true; let cpu_driver: Box = match relevant_limits.cpu.provider { - CpuLimitType::SteamDeck => { - Box::new(crate::settings::steam_deck::Cpus::from_json_and_limits( + CpuLimitType::SteamDeck => Box::new( + crate::settings::steam_deck::Cpus::from_json_and_limits( settings.cpus.clone(), settings.version, relevant_limits.cpu.limits, - ).variant(super::super::steam_deck::Model::LCD)) - } - CpuLimitType::SteamDeckOLED => { - Box::new(crate::settings::steam_deck::Cpus::from_json_and_limits( + ) + .variant(super::super::steam_deck::Model::LCD), + ), + CpuLimitType::SteamDeckOLED => Box::new( + crate::settings::steam_deck::Cpus::from_json_and_limits( settings.cpus.clone(), settings.version, relevant_limits.cpu.limits, - ).variant(super::super::steam_deck::Model::OLED)) - } + ) + .variant(super::super::steam_deck::Model::OLED), + ), CpuLimitType::Generic => Box::new(crate::settings::generic::Cpus::< crate::settings::generic::Cpu, >::from_json_and_limits( @@ -172,20 +174,20 @@ pub fn auto_detect0( settings.version, relevant_limits.cpu.limits, )), - CpuLimitType::GenericAMD => Box::new( - crate::settings::generic_amd::Cpus::from_json_and_limits( + CpuLimitType::GenericAMD => { + Box::new(crate::settings::generic_amd::Cpus::from_json_and_limits( settings.cpus.clone(), settings.version, relevant_limits.cpu.limits, - ), - ), + )) + } CpuLimitType::Unknown => { Box::new(crate::settings::unknown::Cpus::from_json_and_limits( settings.cpus.clone(), settings.version, relevant_limits.cpu.limits, )) - }, + } CpuLimitType::DevMode => { Box::new(crate::settings::dev_mode::Cpus::from_json_and_limits( settings.cpus.clone(), @@ -196,20 +198,22 @@ pub fn auto_detect0( }; let gpu_driver: Box = match relevant_limits.gpu.provider { - GpuLimitType::SteamDeck => { - Box::new(crate::settings::steam_deck::Gpu::from_json_and_limits( + GpuLimitType::SteamDeck => Box::new( + crate::settings::steam_deck::Gpu::from_json_and_limits( settings.gpu.clone(), settings.version, relevant_limits.gpu.limits, - ).variant(super::super::steam_deck::Model::LCD)) - } - GpuLimitType::SteamDeckOLED => { - Box::new(crate::settings::steam_deck::Gpu::from_json_and_limits( + ) + .variant(super::super::steam_deck::Model::LCD), + ), + GpuLimitType::SteamDeckOLED => Box::new( + crate::settings::steam_deck::Gpu::from_json_and_limits( settings.gpu.clone(), settings.version, relevant_limits.gpu.limits, - ).variant(super::super::steam_deck::Model::OLED)) - } + ) + .variant(super::super::steam_deck::Model::OLED), + ), GpuLimitType::Generic => { Box::new(crate::settings::generic::Gpu::from_json_and_limits( settings.gpu.clone(), @@ -217,20 +221,20 @@ pub fn auto_detect0( relevant_limits.gpu.limits, )) } - GpuLimitType::GenericAMD => Box::new( - crate::settings::generic_amd::Gpu::from_json_and_limits( + GpuLimitType::GenericAMD => { + Box::new(crate::settings::generic_amd::Gpu::from_json_and_limits( settings.gpu.clone(), settings.version, relevant_limits.gpu.limits, - ), - ), + )) + } GpuLimitType::Unknown => { Box::new(crate::settings::unknown::Gpu::from_json_and_limits( settings.gpu.clone(), settings.version, relevant_limits.gpu.limits, )) - }, + } GpuLimitType::DevMode => { Box::new(crate::settings::dev_mode::Gpu::from_json_and_limits( settings.gpu.clone(), @@ -240,34 +244,36 @@ pub fn auto_detect0( } }; let battery_driver: Box = match relevant_limits.battery.provider { - BatteryLimitType::SteamDeck => { - Box::new(crate::settings::steam_deck::Battery::from_json_and_limits( + BatteryLimitType::SteamDeck => Box::new( + crate::settings::steam_deck::Battery::from_json_and_limits( settings.battery.clone(), settings.version, relevant_limits.battery.limits, - ).variant(super::super::steam_deck::Model::LCD)) - } - BatteryLimitType::SteamDeckOLED => { - Box::new(crate::settings::steam_deck::Battery::from_json_and_limits( - settings.battery.clone(), - settings.version, - relevant_limits.battery.limits, - ).variant(super::super::steam_deck::Model::OLED)) - } - BatteryLimitType::Generic => Box::new( - crate::settings::generic::Battery::from_json_and_limits( - settings.battery.clone(), - settings.version, - relevant_limits.battery.limits, - ), + ) + .variant(super::super::steam_deck::Model::LCD), ), + BatteryLimitType::SteamDeckOLED => Box::new( + crate::settings::steam_deck::Battery::from_json_and_limits( + settings.battery.clone(), + settings.version, + relevant_limits.battery.limits, + ) + .variant(super::super::steam_deck::Model::OLED), + ), + BatteryLimitType::Generic => { + Box::new(crate::settings::generic::Battery::from_json_and_limits( + settings.battery.clone(), + settings.version, + relevant_limits.battery.limits, + )) + } BatteryLimitType::Unknown => { Box::new(crate::settings::unknown::Battery::from_json_and_limits( settings.battery.clone(), settings.version, relevant_limits.battery.limits, )) - }, + } BatteryLimitType::DevMode => { Box::new(crate::settings::dev_mode::Battery::from_json_and_limits( settings.battery.clone(), @@ -285,62 +291,78 @@ pub fn auto_detect0( }; } else { let cpu_driver: Box = match relevant_limits.cpu.provider { - CpuLimitType::SteamDeck => { - Box::new(crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits).variant(super::super::steam_deck::Model::LCD)) - } - CpuLimitType::SteamDeckOLED => { - Box::new(crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits).variant(super::super::steam_deck::Model::OLED)) - } - CpuLimitType::Generic => { - Box::new(crate::settings::generic::Cpus::< - crate::settings::generic::Cpu, - >::from_limits(relevant_limits.cpu.limits)) - } - CpuLimitType::GenericAMD => { - Box::new(crate::settings::generic_amd::Cpus::from_limits(relevant_limits.cpu.limits)) - } - CpuLimitType::Unknown => { - Box::new(crate::settings::unknown::Cpus::from_limits(relevant_limits.cpu.limits)) - } - CpuLimitType::DevMode => { - Box::new(crate::settings::dev_mode::Cpus::from_limits(relevant_limits.cpu.limits)) - } + CpuLimitType::SteamDeck => Box::new( + crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits) + .variant(super::super::steam_deck::Model::LCD), + ), + CpuLimitType::SteamDeckOLED => Box::new( + crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits) + .variant(super::super::steam_deck::Model::OLED), + ), + CpuLimitType::Generic => Box::new(crate::settings::generic::Cpus::< + crate::settings::generic::Cpu, + >::from_limits( + relevant_limits.cpu.limits + )), + CpuLimitType::GenericAMD => Box::new( + crate::settings::generic_amd::Cpus::from_limits(relevant_limits.cpu.limits), + ), + CpuLimitType::Unknown => Box::new(crate::settings::unknown::Cpus::from_limits( + relevant_limits.cpu.limits, + )), + CpuLimitType::DevMode => Box::new( + crate::settings::dev_mode::Cpus::from_limits(relevant_limits.cpu.limits), + ), }; let gpu_driver: Box = match relevant_limits.gpu.provider { - GpuLimitType::SteamDeck => { - Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits).variant(super::super::steam_deck::Model::LCD)) - } - GpuLimitType::SteamDeckOLED => { - Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits).variant(super::super::steam_deck::Model::OLED)) - } - GpuLimitType::Generic => { - Box::new(crate::settings::generic::Gpu::from_limits(relevant_limits.gpu.limits)) - } - GpuLimitType::GenericAMD => { - Box::new(crate::settings::generic_amd::Gpu::from_limits(relevant_limits.gpu.limits)) - } - GpuLimitType::Unknown => { - Box::new(crate::settings::unknown::Gpu::from_limits(relevant_limits.gpu.limits)) - } - GpuLimitType::DevMode => { - Box::new(crate::settings::dev_mode::Gpu::from_limits(relevant_limits.gpu.limits)) - } + GpuLimitType::SteamDeck => Box::new( + crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits) + .variant(super::super::steam_deck::Model::LCD), + ), + GpuLimitType::SteamDeckOLED => Box::new( + crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits) + .variant(super::super::steam_deck::Model::OLED), + ), + GpuLimitType::Generic => Box::new(crate::settings::generic::Gpu::from_limits( + relevant_limits.gpu.limits, + )), + GpuLimitType::GenericAMD => Box::new( + crate::settings::generic_amd::Gpu::from_limits(relevant_limits.gpu.limits), + ), + GpuLimitType::Unknown => Box::new(crate::settings::unknown::Gpu::from_limits( + relevant_limits.gpu.limits, + )), + GpuLimitType::DevMode => Box::new(crate::settings::dev_mode::Gpu::from_limits( + relevant_limits.gpu.limits, + )), }; let battery_driver: Box = match relevant_limits.battery.provider { - BatteryLimitType::SteamDeck => { - Box::new(crate::settings::steam_deck::Battery::from_limits(relevant_limits.battery.limits).variant(super::super::steam_deck::Model::LCD)) - } - BatteryLimitType::SteamDeckOLED => { - Box::new(crate::settings::steam_deck::Battery::from_limits(relevant_limits.battery.limits).variant(super::super::steam_deck::Model::OLED)) - } + BatteryLimitType::SteamDeck => Box::new( + crate::settings::steam_deck::Battery::from_limits( + relevant_limits.battery.limits, + ) + .variant(super::super::steam_deck::Model::LCD), + ), + BatteryLimitType::SteamDeckOLED => Box::new( + crate::settings::steam_deck::Battery::from_limits( + relevant_limits.battery.limits, + ) + .variant(super::super::steam_deck::Model::OLED), + ), BatteryLimitType::Generic => { - Box::new(crate::settings::generic::Battery::from_limits(relevant_limits.battery.limits)) + Box::new(crate::settings::generic::Battery::from_limits( + relevant_limits.battery.limits, + )) } BatteryLimitType::Unknown => { - Box::new(crate::settings::unknown::Battery::from_limits(relevant_limits.battery.limits)) + Box::new(crate::settings::unknown::Battery::from_limits( + relevant_limits.battery.limits, + )) } BatteryLimitType::DevMode => { - Box::new(crate::settings::dev_mode::Battery::from_limits(relevant_limits.battery.limits)) + Box::new(crate::settings::dev_mode::Battery::from_limits( + relevant_limits.battery.limits, + )) } }; return Driver { diff --git a/backend/src/settings/detect/limits_worker.rs b/backend/src/settings/detect/limits_worker.rs index ad10f56..3665b11 100644 --- a/backend/src/settings/detect/limits_worker.rs +++ b/backend/src/settings/detect/limits_worker.rs @@ -93,14 +93,16 @@ pub fn get_limits_cached() -> Base { fn save_base(new_base: &Base, path: impl AsRef) { let limits_path = path.as_ref(); match std::fs::File::create(&limits_path) { - Ok(f) => match ron::ser::to_writer_pretty(f, &new_base, crate::utility::ron_pretty_config()) { - Ok(_) => log::info!("Successfully saved new limits to {}", limits_path.display()), - Err(e) => log::error!( - "Failed to save limits json to file `{}`: {}", - limits_path.display(), - e - ), - }, + Ok(f) => { + match ron::ser::to_writer_pretty(f, &new_base, crate::utility::ron_pretty_config()) { + Ok(_) => log::info!("Successfully saved new limits to {}", limits_path.display()), + Err(e) => log::error!( + "Failed to save limits json to file `{}`: {}", + limits_path.display(), + e + ), + } + } Err(e) => log::error!("Cannot create {}: {}", limits_path.display(), e), } } diff --git a/backend/src/settings/detect/utility.rs b/backend/src/settings/detect/utility.rs index 7f14b5f..27d37a8 100644 --- a/backend/src/settings/detect/utility.rs +++ b/backend/src/settings/detect/utility.rs @@ -1,4 +1,4 @@ -use limits_core::json::{DeveloperMessage, Base}; +use limits_core::json::{Base, DeveloperMessage}; pub fn limits_path() -> std::path::PathBuf { crate::utility::settings_dir().join(crate::consts::LIMITS_FILE) diff --git a/backend/src/settings/dev_mode/battery.rs b/backend/src/settings/dev_mode/battery.rs index ac7cf79..a57e736 100644 --- a/backend/src/settings/dev_mode/battery.rs +++ b/backend/src/settings/dev_mode/battery.rs @@ -3,8 +3,8 @@ use std::convert::Into; use limits_core::json_v2::GenericBatteryLimit; use crate::persist::BatteryJson; -use crate::settings::{TBattery, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError}; +use crate::settings::{ProviderBuilder, TBattery}; #[derive(Clone)] pub struct Battery { @@ -32,7 +32,11 @@ impl Into for Battery { } impl ProviderBuilder for Battery { - fn from_json_and_limits(persist: BatteryJson, version: u64, limits: GenericBatteryLimit) -> Self { + fn from_json_and_limits( + persist: BatteryJson, + version: u64, + limits: GenericBatteryLimit, + ) -> Self { Battery { persist, version, @@ -43,7 +47,12 @@ impl ProviderBuilder for Battery { fn from_limits(limits: GenericBatteryLimit) -> Self { Battery { - persist: BatteryJson { charge_rate: None, charge_mode: None, events: vec![], root: None }, + persist: BatteryJson { + charge_rate: None, + charge_mode: None, + events: vec![], + root: None, + }, version: 0, limits, charge_limit: None, @@ -71,10 +80,16 @@ impl TBattery for Battery { fn limits(&self) -> crate::api::BatteryLimits { log::debug!("dev_mode_Battery::limits(self) -> {{...}}"); crate::api::BatteryLimits { - charge_current: self.limits.charge_rate.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(11), max: lim.max.unwrap_or(1111) }), + charge_current: self.limits.charge_rate.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(11), + max: lim.max.unwrap_or(1111), + }), charge_current_step: 10, charge_modes: self.limits.charge_modes.clone(), - charge_limit: self.limits.charge_limit.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(2.0), max: lim.max.unwrap_or(98.0) }), + charge_limit: self.limits.charge_limit.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(2.0), + max: lim.max.unwrap_or(98.0), + }), charge_limit_step: 1.0, } } @@ -90,7 +105,10 @@ impl TBattery for Battery { } fn get_charge_rate(&self) -> Option { - log::debug!("dev_mode_Battery::get_charge_rate(self) -> {:?}", self.persist.charge_rate); + log::debug!( + "dev_mode_Battery::get_charge_rate(self) -> {:?}", + self.persist.charge_rate + ); self.persist.charge_rate } @@ -100,7 +118,10 @@ impl TBattery for Battery { } fn get_charge_mode(&self) -> Option { - log::debug!("dev_mode_Battery::get_charge_mode(self) -> {:?}", self.persist.charge_mode); + log::debug!( + "dev_mode_Battery::get_charge_mode(self) -> {:?}", + self.persist.charge_mode + ); self.persist.charge_mode.clone() } @@ -135,7 +156,10 @@ impl TBattery for Battery { } fn get_charge_limit(&self) -> Option { - log::debug!("dev_mode_Battery::get_charge_limit(self) -> {:?}", self.charge_limit); + log::debug!( + "dev_mode_Battery::get_charge_limit(self) -> {:?}", + self.charge_limit + ); self.charge_limit } diff --git a/backend/src/settings/dev_mode/cpu.rs b/backend/src/settings/dev_mode/cpu.rs index c278fe1..a505b69 100644 --- a/backend/src/settings/dev_mode/cpu.rs +++ b/backend/src/settings/dev_mode/cpu.rs @@ -1,11 +1,11 @@ use std::convert::Into; -use limits_core::json_v2::{GenericCpusLimit, GenericCpuLimit}; +use limits_core::json_v2::{GenericCpuLimit, GenericCpusLimit}; use crate::persist::CpuJson; use crate::settings::MinMax; use crate::settings::{OnResume, OnSet, SettingError}; -use crate::settings::{TCpu, TCpus, ProviderBuilder}; +use crate::settings::{ProviderBuilder, TCpu, TCpus}; #[derive(Debug, Clone)] pub struct Cpus { @@ -40,13 +40,22 @@ impl OnResume for Cpus { impl crate::settings::OnPowerEvent for Cpus {} impl ProviderBuilder, GenericCpusLimit> for Cpus { - fn from_json_and_limits(persistent: Vec, version: u64, limits: GenericCpusLimit) -> Self { + fn from_json_and_limits( + persistent: Vec, + version: u64, + limits: GenericCpusLimit, + ) -> Self { let mut cpus = Vec::with_capacity(persistent.len()); for (i, cpu) in persistent.iter().enumerate() { - cpus.push(Cpu::from_json_and_limits(cpu.to_owned(), version, i, limits.cpus.get(i).map(|x| x.to_owned()).unwrap_or_else(|| { - log::warn!("No cpu limit for index {}, using default", i); - Default::default() - }))); + cpus.push(Cpu::from_json_and_limits( + cpu.to_owned(), + version, + i, + limits.cpus.get(i).map(|x| x.to_owned()).unwrap_or_else(|| { + log::warn!("No cpu limit for index {}, using default", i); + Default::default() + }), + )); } let smt_guess = crate::settings::util::guess_smt(&persistent); Self { @@ -78,7 +87,12 @@ impl TCpus for Cpus { cpus: self.cpus.iter().map(|x| x.limits()).collect(), count: self.cpus.len(), smt_capable: true, - governors: vec!["this".to_owned(), "is".to_owned(), "dev".to_owned(), "mode".to_owned()], + governors: vec![ + "this".to_owned(), + "is".to_owned(), + "dev".to_owned(), + "mode".to_owned(), + ], } } @@ -130,8 +144,16 @@ impl std::fmt::Debug for Cpu { impl Cpu { #[inline] - pub fn from_json_and_limits(other: CpuJson, version: u64, i: usize, limits: GenericCpuLimit) -> Self { - let clock_limits = other.clock_limits.clone().map(|lim| MinMax { min: lim.min, max: lim.max }); + pub fn from_json_and_limits( + other: CpuJson, + version: u64, + i: usize, + limits: GenericCpuLimit, + ) -> Self { + let clock_limits = other.clock_limits.clone().map(|lim| MinMax { + min: lim.min, + max: lim.max, + }); match version { 0 => Self { persist: other, @@ -153,7 +175,12 @@ impl Cpu { #[inline] pub fn from_limits(i: usize, limits: GenericCpuLimit) -> Self { Self { - persist: CpuJson { online: true, clock_limits: None, governor: "".to_owned(), root: None }, + persist: CpuJson { + online: true, + clock_limits: None, + governor: "".to_owned(), + root: None, + }, version: 0, index: i, limits, @@ -163,10 +190,21 @@ impl Cpu { fn limits(&self) -> crate::api::CpuLimits { crate::api::CpuLimits { - clock_min_limits: self.limits.clock_min.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(1100), max: lim.max.unwrap_or(6900) }), - clock_max_limits: self.limits.clock_max.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(4200), max: lim.max.unwrap_or(4300) }), + clock_min_limits: self.limits.clock_min.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(1100), + max: lim.max.unwrap_or(6900), + }), + clock_max_limits: self.limits.clock_max.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(4200), + max: lim.max.unwrap_or(4300), + }), clock_step: self.limits.clock_step.unwrap_or(11), - governors: vec!["this".to_owned(), "is".to_owned(), "dev".to_owned(), "mode".to_owned()], + governors: vec![ + "this".to_owned(), + "is".to_owned(), + "dev".to_owned(), + "mode".to_owned(), + ], } } } @@ -211,11 +249,20 @@ impl TCpu for Cpu { fn clock_limits(&mut self, limits: Option>) { log::debug!("dev_mode_Cpu::clock_limits(self, {:?})", limits); self.clock_limits = limits; - self.persist.clock_limits = self.clock_limits.clone().map(|lim| crate::persist::MinMaxJson { max: lim.max, min: lim.min }); + self.persist.clock_limits = + self.clock_limits + .clone() + .map(|lim| crate::persist::MinMaxJson { + max: lim.max, + min: lim.min, + }); } fn get_clock_limits(&self) -> Option<&MinMax> { - log::debug!("dev_mode_Cpu::get_clock_limits(self) -> {:?}", self.clock_limits.as_ref()); + log::debug!( + "dev_mode_Cpu::get_clock_limits(self) -> {:?}", + self.clock_limits.as_ref() + ); self.clock_limits.as_ref() } } diff --git a/backend/src/settings/dev_mode/gpu.rs b/backend/src/settings/dev_mode/gpu.rs index b9f6e76..919d905 100644 --- a/backend/src/settings/dev_mode/gpu.rs +++ b/backend/src/settings/dev_mode/gpu.rs @@ -4,8 +4,8 @@ use limits_core::json_v2::GenericGpuLimit; use crate::persist::GpuJson; use crate::settings::MinMax; -use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError}; +use crate::settings::{ProviderBuilder, TGpu}; #[derive(Clone)] pub struct Gpu { @@ -27,7 +27,10 @@ impl std::fmt::Debug for Gpu { impl ProviderBuilder for Gpu { fn from_json_and_limits(persist: GpuJson, version: u64, limits: GenericGpuLimit) -> Self { - let clock_limits = persist.clock_limits.clone().map(|lim| MinMax { min: lim.min, max: lim.max }); + let clock_limits = persist.clock_limits.clone().map(|lim| MinMax { + min: lim.min, + max: lim.max, + }); Self { persist, version, @@ -83,16 +86,37 @@ impl TGpu for Gpu { let ppt_divisor = self.limits.ppt_divisor.unwrap_or(1_000_000); let tdp_divisor = self.limits.tdp_divisor.unwrap_or(1_000_000); crate::api::GpuLimits { - fast_ppt_limits: self.limits.fast_ppt.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(11_000_000) / ppt_divisor, max: lim.max.unwrap_or(42_000_000) / ppt_divisor }), - slow_ppt_limits: self.limits.slow_ppt.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(7_000_000) / ppt_divisor, max: lim.max.unwrap_or(69_000_000) / ppt_divisor }), + fast_ppt_limits: self.limits.fast_ppt.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(11_000_000) / ppt_divisor, + max: lim.max.unwrap_or(42_000_000) / ppt_divisor, + }), + slow_ppt_limits: self.limits.slow_ppt.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(7_000_000) / ppt_divisor, + max: lim.max.unwrap_or(69_000_000) / ppt_divisor, + }), ppt_step: self.limits.ppt_step.unwrap_or(1), - tdp_limits: self.limits.tdp.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(11_000_000) / tdp_divisor, max: lim.max.unwrap_or(69_000_000) / tdp_divisor }), - tdp_boost_limits: self.limits.tdp_boost.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(7_000_000) / tdp_divisor, max: lim.max.unwrap_or(69_000_000) / tdp_divisor }), + tdp_limits: self.limits.tdp.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(11_000_000) / tdp_divisor, + max: lim.max.unwrap_or(69_000_000) / tdp_divisor, + }), + tdp_boost_limits: self.limits.tdp_boost.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(7_000_000) / tdp_divisor, + max: lim.max.unwrap_or(69_000_000) / tdp_divisor, + }), tdp_step: self.limits.tdp_step.unwrap_or(1), - clock_min_limits: self.limits.clock_min.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(1100), max: lim.max.unwrap_or(6900) }), - clock_max_limits: self.limits.clock_max.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(1100), max: lim.max.unwrap_or(4200) }), + clock_min_limits: self.limits.clock_min.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(1100), + max: lim.max.unwrap_or(6900), + }), + clock_max_limits: self.limits.clock_max.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(1100), + max: lim.max.unwrap_or(4200), + }), clock_step: self.limits.clock_step.unwrap_or(100), - memory_control: self.limits.memory_clock.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(100), max: lim.max.unwrap_or(1100) }), + memory_control: self.limits.memory_clock.map(|lim| crate::api::RangeLimit { + min: lim.min.unwrap_or(100), + max: lim.max.unwrap_or(1100), + }), memory_step: self.limits.memory_clock_step.unwrap_or(400), } } @@ -103,24 +127,41 @@ impl TGpu for Gpu { } fn ppt(&mut self, fast: Option, slow: Option) { - log::debug!("dev_mode_Gpu::ppt(self, fast: {:?}, slow: {:?})", fast, slow); + log::debug!( + "dev_mode_Gpu::ppt(self, fast: {:?}, slow: {:?})", + fast, + slow + ); self.persist.fast_ppt = fast; self.persist.slow_ppt = slow; } fn get_ppt(&self) -> (Option, Option) { - log::debug!("dev_mode_Gpu::get_ppt(self) -> (fast: {:?}, slow: {:?})", self.persist.fast_ppt, self.persist.slow_ppt); + log::debug!( + "dev_mode_Gpu::get_ppt(self) -> (fast: {:?}, slow: {:?})", + self.persist.fast_ppt, + self.persist.slow_ppt + ); (self.persist.fast_ppt, self.persist.slow_ppt) } fn clock_limits(&mut self, limits: Option>) { log::debug!("dev_mode_Gpu::clock_limits(self, {:?})", limits); self.clock_limits = limits; - self.persist.clock_limits = self.clock_limits.clone().map(|lim| crate::persist::MinMaxJson { max: lim.max, min: lim.min }); + self.persist.clock_limits = + self.clock_limits + .clone() + .map(|lim| crate::persist::MinMaxJson { + max: lim.max, + min: lim.min, + }); } fn get_clock_limits(&self) -> Option<&MinMax> { - log::debug!("dev_mode_Gpu::get_clock_limits(self) -> {:?}", self.clock_limits.as_ref()); + log::debug!( + "dev_mode_Gpu::get_clock_limits(self) -> {:?}", + self.clock_limits.as_ref() + ); self.clock_limits.as_ref() } @@ -130,7 +171,10 @@ impl TGpu for Gpu { } fn get_memory_clock(&self) -> Option { - log::debug!("dev_mode_Gpu::memory_clock(self) -> {:?}", self.persist.memory_clock); + log::debug!( + "dev_mode_Gpu::memory_clock(self) -> {:?}", + self.persist.memory_clock + ); self.persist.memory_clock } diff --git a/backend/src/settings/dev_mode/mod.rs b/backend/src/settings/dev_mode/mod.rs index 200c1ea..58b6ac1 100644 --- a/backend/src/settings/dev_mode/mod.rs +++ b/backend/src/settings/dev_mode/mod.rs @@ -9,7 +9,15 @@ pub use gpu::Gpu; fn _impl_checker() { fn impl_provider_builder, J, L>() {} - impl_provider_builder::(); - impl_provider_builder::, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::< + Battery, + crate::persist::BatteryJson, + limits_core::json_v2::GenericBatteryLimit, + >(); + impl_provider_builder::< + Cpus, + Vec, + limits_core::json_v2::GenericCpusLimit, + >(); impl_provider_builder::(); } diff --git a/backend/src/settings/driver.rs b/backend/src/settings/driver.rs index adae096..128cf30 100644 --- a/backend/src/settings/driver.rs +++ b/backend/src/settings/driver.rs @@ -20,7 +20,13 @@ impl Driver { auto_detect0(Some(settings), json_path, app_id, name, id_bup, name_bup) } - pub fn system_default(json_path: std::path::PathBuf, app_id: u64, name: String, variant_id: u64, variant_name: String) -> Self { + pub fn system_default( + json_path: std::path::PathBuf, + app_id: u64, + name: String, + variant_id: u64, + variant_name: String, + ) -> Self { auto_detect0(None, json_path, app_id, name, variant_id, variant_name) } } diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index d1d913a..b873b0a 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; //use super::{Battery, Cpus, Gpu}; use super::{OnResume, OnSet, SettingError}; use super::{TBattery, TCpus, TGeneral, TGpu}; -use crate::persist::{SettingsJson, FileJson}; +use crate::persist::{FileJson, SettingsJson}; //use crate::utility::unwrap_lock; const LATEST_VERSION: u64 = 0; @@ -103,11 +103,14 @@ impl TGeneral for General { } fn get_variants(&self) -> Vec { - if let Ok(file) = crate::persist::FileJson::open(self.get_path()) { - file.variants.into_iter() + let json_path = crate::utility::settings_dir().join(self.get_path()); + if let Ok(file) = crate::persist::FileJson::open(json_path) { + file.variants + .into_iter() .map(|(id, conf)| crate::api::VariantInfo { id: id.to_string(), name: conf.name, + id_num: id, }) .collect() } else { @@ -115,25 +118,40 @@ impl TGeneral for General { } } - fn add_variant(&self, variant: crate::persist::SettingsJson) -> Result, SettingError> { + fn add_variant( + &self, + variant: crate::persist::SettingsJson, + ) -> Result, SettingError> { let variant_name = variant.name.clone(); - crate::persist::FileJson::update_variant_or_create(self.get_path(), self.get_app_id(), variant, variant_name) - .map_err(|e| SettingError { - msg: format!("failed to add variant: {}", e), - setting: SettingVariant::General, - }) - .map(|file| file.variants.into_iter() + let json_path = crate::utility::settings_dir().join(self.get_path()); + crate::persist::FileJson::update_variant_or_create( + json_path, + self.get_app_id(), + variant, + variant_name, + ) + .map_err(|e| SettingError { + msg: format!("failed to add variant: {}", e), + setting: SettingVariant::General, + }) + .map(|file| { + file.0.variants + .into_iter() .map(|(id, conf)| crate::api::VariantInfo { id: id.to_string(), name: conf.name, + id_num: id, }) - .collect()) + .collect() + }) } fn get_variant_info(&self) -> crate::api::VariantInfo { + log::debug!("Current variant `{}` ({})", self.variant_name, self.variant_id); crate::api::VariantInfo { id: self.variant_id.to_string(), name: self.variant_name.clone(), + id_num: self.variant_id, } } @@ -199,8 +217,15 @@ impl Settings { } } - pub fn system_default(json_path: PathBuf, app_id: u64, name: String, variant_id: u64, variant_name: String) -> Self { - let driver = super::Driver::system_default(json_path, app_id, name, variant_id, variant_name); + pub fn system_default( + json_path: PathBuf, + app_id: u64, + name: String, + variant_id: u64, + variant_name: String, + ) -> Self { + let driver = + super::Driver::system_default(json_path, app_id, name, variant_id, variant_name); Self { general: driver.general, cpus: driver.cpus, @@ -210,19 +235,47 @@ impl Settings { } pub fn load_system_default(&mut self, name: String, variant_id: u64, variant_name: String) { - let driver = super::Driver::system_default(self.general.get_path().to_owned(), self.general.get_app_id(), name, variant_id, variant_name); + let driver = super::Driver::system_default( + self.general.get_path().to_owned(), + self.general.get_app_id(), + name, + variant_id, + variant_name, + ); self.cpus = driver.cpus; self.gpu = driver.gpu; self.battery = driver.battery; self.general = driver.general; } - pub fn get_variant<'a>(settings_file: &'a FileJson, variant_id: u64, variant_name: String) -> Result<&'a SettingsJson, SettingError> { + pub fn get_variant<'a>( + settings_file: &'a FileJson, + variant_id: u64, + variant_name: String, + ) -> Result<&'a SettingsJson, SettingError> { if let Some(variant) = settings_file.variants.get(&variant_id) { Ok(variant) + } else if variant_id == 0 { + // special case: requesting primary variant for settings with non-persistent primary + let mut valid_ids: Vec<&u64> = settings_file.variants.keys().collect(); + valid_ids.sort(); + if let Some(id) = valid_ids.get(0) { + Ok(settings_file.variants.get(id).expect("variant id key magically disappeared")) + } else { + Err(SettingError { + msg: format!( + "Cannot get variant `{}` (id:{}) from empty settings file", + variant_name, variant_id + ), + setting: SettingVariant::General, + }) + } } else { Err(SettingError { - msg: format!("Cannot get non-existent variant `{}` (id:{})", variant_name, variant_id), + msg: format!( + "Cannot get non-existent variant `{}` (id:{})", + variant_name, variant_id + ), setting: SettingVariant::General, }) } @@ -240,13 +293,8 @@ impl Settings { let json_path = crate::utility::settings_dir().join(&filename); if json_path.exists() { if variant == u64::MAX { - *self.general.persistent() = true; - let file_json = FileJson::update_variant_or_create(&json_path, app_id, self.json(), variant_name.clone()).map_err(|e| SettingError { - msg: format!("Failed to open settings {}: {}", json_path.display(), e), - setting: SettingVariant::General, - })?; - self.general.variant_id(file_json.variants.iter().find(|(_key, val)| val.name == variant_name).map(|(key, _val)| *key).expect("Setting variant was not added properly")); - self.general.variant_name(variant_name); + log::debug!("Creating new variant `{}` in existing settings file {}", variant_name, json_path.display()); + self.create_and_load_variant(&json_path, app_id, variant_name)?; } else { let file_json = FileJson::open(&json_path).map_err(|e| SettingError { msg: format!("Failed to open settings {}: {}", json_path.display(), e), @@ -261,31 +309,61 @@ impl Settings { ); *self.general.persistent() = false; self.general.name(name); + self.general.variant_name(settings_json.name.clone()); + self.general.variant_id(settings_json.variant); } else { let x = super::Driver::init(name, settings_json, json_path.clone(), app_id); - log::info!("Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), x.cpus.provider(), x.gpu.provider(), x.battery.provider()); + log::info!( + "Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", + x.general.provider(), + x.cpus.provider(), + x.gpu.provider(), + x.battery.provider() + ); self.general = x.general; self.cpus = x.cpus; self.gpu = x.gpu; self.battery = x.battery; } } - } else { if system_defaults { - self.load_system_default(name, variant, variant_name); + self.load_system_default(name, variant, variant_name.clone()); } else { self.general.name(name); - self.general.variant_name(variant_name); + self.general.variant_name(variant_name.clone()); + self.general.variant_id(variant); } *self.general.persistent() = false; + if variant == u64::MAX { + log::debug!("Creating new variant `{}` in new settings file {}", variant_name, json_path.display()); + self.create_and_load_variant(&json_path, app_id, variant_name)?; + } } *self.general.app_id() = app_id; self.general.path(filename); - self.general.variant_id(variant); Ok(*self.general.persistent()) } + fn create_and_load_variant(&mut self, json_path: &PathBuf, app_id: u64, variant_name: String) -> Result<(), SettingError> { + *self.general.persistent() = true; + self.general.variant_id(u64::MAX); + self.general.variant_name(variant_name.clone()); + let (_file_json, new_variant) = FileJson::update_variant_or_create( + json_path, + app_id, + self.json(), + self.general.get_name().to_owned(), + ) + .map_err(|e| SettingError { + msg: format!("Failed to open settings {}: {}", json_path.display(), e), + setting: SettingVariant::General, + })?; + self.general.variant_id(new_variant.variant); + self.general.variant_name(new_variant.name); + Ok(()) + } + /* pub fn load_file(&mut self, filename: PathBuf, name: String, system_defaults: bool) -> Result { let json_path = crate::utility::settings_dir().join(filename); diff --git a/backend/src/settings/generic/battery.rs b/backend/src/settings/generic/battery.rs index af08b11..cfe201a 100644 --- a/backend/src/settings/generic/battery.rs +++ b/backend/src/settings/generic/battery.rs @@ -4,8 +4,8 @@ use limits_core::json_v2::GenericBatteryLimit; use sysfuss::{SysEntity, SysEntityAttributesExt}; use crate::persist::BatteryJson; -use crate::settings::{TBattery, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError}; +use crate::settings::{ProviderBuilder, TBattery}; #[derive(Debug, Clone)] pub struct Battery { @@ -21,7 +21,10 @@ impl Into for Battery { charge_rate: None, charge_mode: None, events: Vec::default(), - root: self.sysfs.root().and_then(|p| p.as_ref().to_str().map(|s| s.to_owned())), + root: self + .sysfs + .root() + .and_then(|p| p.as_ref().to_str().map(|s| s.to_owned())), } } } @@ -59,16 +62,26 @@ impl Battery { } fn get_design_voltage(&self) -> Option { - match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::VoltageMax) { - Ok(x) => Some(x/1000000.0), + match self + .sysfs + .attribute::(sysfuss::PowerSupplyAttribute::VoltageMax) + { + Ok(x) => Some(x / 1000000.0), Err(e) => { log::debug!("get_design_voltage voltage_max err: {}", e); - match sysfuss::SysEntityRawExt::attribute::<_, f64, _>(&self.sysfs, "voltage_min_design".to_owned()) { // Framework 13 AMD - Ok(x) => Some(x/1000000.0), + match sysfuss::SysEntityRawExt::attribute::<_, f64, _>( + &self.sysfs, + "voltage_min_design".to_owned(), + ) { + // Framework 13 AMD + Ok(x) => Some(x / 1000000.0), Err(e) => { log::debug!("get_design_voltage voltage_min_design err: {}", e); - match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::VoltageMin) { - Ok(x) => Some(x/1000000.0), + match self + .sysfs + .attribute::(sysfuss::PowerSupplyAttribute::VoltageMin) + { + Ok(x) => Some(x / 1000000.0), Err(e) => { log::debug!("get_design_voltage voltage_min err: {}", e); None @@ -90,7 +103,7 @@ impl ProviderBuilder for Battery { // TODO Self { limits, - sysfs: Self::find_psu_sysfs(persistent.root) + sysfs: Self::find_psu_sysfs(persistent.root), } } @@ -148,8 +161,11 @@ impl TBattery for Battery { fn read_charge_full(&self) -> Option { if let Some(battery_voltage) = self.get_design_voltage() { - match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::ChargeFull) { - Ok(x) => Some(x/1000000.0 * battery_voltage), + match self + .sysfs + .attribute::(sysfuss::PowerSupplyAttribute::ChargeFull) + { + Ok(x) => Some(x / 1000000.0 * battery_voltage), Err(e) => { log::warn!("read_charge_full err: {}", e); None @@ -162,8 +178,11 @@ impl TBattery for Battery { fn read_charge_now(&self) -> Option { if let Some(battery_voltage) = self.get_design_voltage() { - match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::ChargeNow) { - Ok(x) => Some(x/1000000.0 * battery_voltage), + match self + .sysfs + .attribute::(sysfuss::PowerSupplyAttribute::ChargeNow) + { + Ok(x) => Some(x / 1000000.0 * battery_voltage), Err(e) => { log::warn!("read_charge_now err: {}", e); None @@ -176,8 +195,11 @@ impl TBattery for Battery { fn read_charge_design(&self) -> Option { if let Some(battery_voltage) = self.get_design_voltage() { - match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::ChargeFullDesign) { - Ok(x) => Some(x/1000000.0 * battery_voltage), + match self + .sysfs + .attribute::(sysfuss::PowerSupplyAttribute::ChargeFullDesign) + { + Ok(x) => Some(x / 1000000.0 * battery_voltage), Err(e) => { log::warn!("read_charge_design err: {}", e); None @@ -189,8 +211,11 @@ impl TBattery for Battery { } fn read_current_now(&self) -> Option { - match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::CurrentNow) { - Ok(x) => Some(x/1000.0), // expects mA, reads uA + match self + .sysfs + .attribute::(sysfuss::PowerSupplyAttribute::CurrentNow) + { + Ok(x) => Some(x / 1000.0), // expects mA, reads uA Err(e) => { log::warn!("read_current_now err: {}", e); None diff --git a/backend/src/settings/generic/cpu.rs b/backend/src/settings/generic/cpu.rs index 75d0d1e..0083028 100644 --- a/backend/src/settings/generic/cpu.rs +++ b/backend/src/settings/generic/cpu.rs @@ -1,13 +1,13 @@ use std::convert::{AsMut, AsRef, Into}; -use limits_core::json_v2::{GenericCpusLimit, GenericCpuLimit}; +use limits_core::json_v2::{GenericCpuLimit, GenericCpusLimit}; use super::FromGenericCpuInfo; use crate::api::RangeLimit; use crate::persist::CpuJson; use crate::settings::{min_max_from_json, MinMax}; use crate::settings::{OnResume, OnSet, SettingError}; -use crate::settings::{TCpu, TCpus, ProviderBuilder}; +use crate::settings::{ProviderBuilder, TCpu, TCpus}; const CPU_PRESENT_PATH: &str = "/sys/devices/system/cpu/present"; const CPU_SMT_PATH: &str = "/sys/devices/system/cpu/smt/control"; @@ -89,8 +89,14 @@ impl + AsRef + TCpu + FromGenericCpuInfo> Cpus { } } -impl + AsRef + TCpu + FromGenericCpuInfo> ProviderBuilder, GenericCpusLimit> for Cpus { - fn from_json_and_limits(mut other: Vec, version: u64, limits: GenericCpusLimit) -> Self { +impl + AsRef + TCpu + FromGenericCpuInfo> + ProviderBuilder, GenericCpusLimit> for Cpus +{ + fn from_json_and_limits( + mut other: Vec, + version: u64, + limits: GenericCpusLimit, + ) -> Self { let (_, can_smt) = Self::system_smt_capabilities(); let mut result = Vec::with_capacity(other.len()); let max_cpus = Self::cpu_count(); @@ -103,9 +109,12 @@ impl + AsRef + TCpu + FromGenericCpuInfo> ProviderBuilder + AsRef + TCpu + FromGenericCpuInfo> ProviderBuilder String { - usdpl_back::api::files::read_single(cpu_governor_path(index)).unwrap_or_else(|_| "schedutil".to_owned()) + usdpl_back::api::files::read_single(cpu_governor_path(index)) + .unwrap_or_else(|_| "schedutil".to_owned()) } } diff --git a/backend/src/settings/generic/gpu.rs b/backend/src/settings/generic/gpu.rs index 3e2b5cf..1cc61db 100644 --- a/backend/src/settings/generic/gpu.rs +++ b/backend/src/settings/generic/gpu.rs @@ -5,9 +5,9 @@ use sysfuss::{BasicEntityPath, SysEntity}; use crate::api::RangeLimit; use crate::persist::GpuJson; -use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{min_max_from_json, MinMax}; use crate::settings::{OnResume, OnSet, SettingError}; +use crate::settings::{ProviderBuilder, TGpu}; #[derive(Debug, Clone)] pub struct Gpu { @@ -38,15 +38,17 @@ impl Gpu { fn find_card_sysfs(root: Option>) -> BasicEntityPath { let root = crate::settings::util::root_or_default_sysfs(root); match root.class("drm", crate::settings::util::always_satisfied) { - Ok(mut iter) => { - iter.next() - .unwrap_or_else(|| { - log::error!("Failed to find generic gpu drm in sysfs (no results), using naive fallback"); - BasicEntityPath::new(root.as_ref().join("sys/class/drm/card0")) - }) - }, + Ok(mut iter) => iter.next().unwrap_or_else(|| { + log::error!( + "Failed to find generic gpu drm in sysfs (no results), using naive fallback" + ); + BasicEntityPath::new(root.as_ref().join("sys/class/drm/card0")) + }), Err(e) => { - log::error!("Failed to find generic gpu drm in sysfs ({}), using naive fallback", e); + log::error!( + "Failed to find generic gpu drm in sysfs ({}), using naive fallback", + e + ); BasicEntityPath::new(root.as_ref().join("sys/class/drm/card0")) } } @@ -56,7 +58,9 @@ impl Gpu { impl ProviderBuilder for Gpu { fn from_json_and_limits(persistent: GpuJson, version: u64, limits: GenericGpuLimit) -> Self { let clock_lims = if limits.clock_min.is_some() && limits.clock_max.is_some() { - persistent.clock_limits.map(|x| min_max_from_json(x, version)) + persistent + .clock_limits + .map(|x| min_max_from_json(x, version)) } else { None }; @@ -84,7 +88,7 @@ impl ProviderBuilder for Gpu { }, clock_limits: clock_lims, limits, - sysfs: Self::find_card_sysfs(persistent.root) + sysfs: Self::find_card_sysfs(persistent.root), } } @@ -112,7 +116,10 @@ impl Into for Gpu { tdp_boost: self.tdp_boost, clock_limits: self.clock_limits.map(|x| x.into()), memory_clock: None, - root: self.sysfs.root().and_then(|p| p.as_ref().to_str().map(|s| s.to_owned())) + root: self + .sysfs + .root() + .and_then(|p| p.as_ref().to_str().map(|s| s.to_owned())), } } } @@ -139,21 +146,29 @@ impl TGpu for Gpu { .fast_ppt .clone() .map(|x| RangeLimit::new(x.min.unwrap_or(0), x.max.unwrap_or(15))) - .map(|mut x| if let Some(ppt_divisor) = self.limits.ppt_divisor { - x.min /= ppt_divisor; - x.max /= ppt_divisor; - x - } else {x}), + .map(|mut x| { + if let Some(ppt_divisor) = self.limits.ppt_divisor { + x.min /= ppt_divisor; + x.max /= ppt_divisor; + x + } else { + x + } + }), slow_ppt_limits: self .limits .slow_ppt .clone() .map(|x| RangeLimit::new(x.min.unwrap_or(0), x.max.unwrap_or(15))) - .map(|mut x| if let Some(ppt_divisor) = self.limits.ppt_divisor { - x.min /= ppt_divisor; - x.max /= ppt_divisor; - x - } else {x}), + .map(|mut x| { + if let Some(ppt_divisor) = self.limits.ppt_divisor { + x.min /= ppt_divisor; + x.max /= ppt_divisor; + x + } else { + x + } + }), ppt_step: self.limits.ppt_step.unwrap_or(1), tdp_limits: self .limits @@ -188,28 +203,56 @@ impl TGpu for Gpu { fn ppt(&mut self, fast: Option, slow: Option) { if let Some(fast_lims) = &self.limits.fast_ppt { - self.fast_ppt = fast.map(|x| if let Some(ppt_divisor) = self.limits.ppt_divisor { x * ppt_divisor } else { x }) - .map(|x| { - x.clamp( - fast_lims.min.unwrap_or(0), - fast_lims.max.unwrap_or(u64::MAX), - ) - }); + self.fast_ppt = fast + .map(|x| { + if let Some(ppt_divisor) = self.limits.ppt_divisor { + x * ppt_divisor + } else { + x + } + }) + .map(|x| { + x.clamp( + fast_lims.min.unwrap_or(0), + fast_lims.max.unwrap_or(u64::MAX), + ) + }); } if let Some(slow_lims) = &self.limits.slow_ppt { - self.slow_ppt = slow.map(|x| if let Some(ppt_divisor) = self.limits.ppt_divisor { x * ppt_divisor } else { x }) - .map(|x| { - x.clamp( - slow_lims.min.unwrap_or(0), - slow_lims.max.unwrap_or(u64::MAX), - ) - }); + self.slow_ppt = slow + .map(|x| { + if let Some(ppt_divisor) = self.limits.ppt_divisor { + x * ppt_divisor + } else { + x + } + }) + .map(|x| { + x.clamp( + slow_lims.min.unwrap_or(0), + slow_lims.max.unwrap_or(u64::MAX), + ) + }); } } fn get_ppt(&self) -> (Option, Option) { - (self.fast_ppt.map(|x| if let Some(ppt_divisor) = self.limits.ppt_divisor { x / ppt_divisor } else { x }), - self.slow_ppt.map(|x| if let Some(ppt_divisor) = self.limits.ppt_divisor { x / ppt_divisor } else { x })) + ( + self.fast_ppt.map(|x| { + if let Some(ppt_divisor) = self.limits.ppt_divisor { + x / ppt_divisor + } else { + x + } + }), + self.slow_ppt.map(|x| { + if let Some(ppt_divisor) = self.limits.ppt_divisor { + x / ppt_divisor + } else { + x + } + }), + ) } fn clock_limits(&mut self, limits: Option>) { diff --git a/backend/src/settings/generic/mod.rs b/backend/src/settings/generic/mod.rs index 9a99d47..1abd444 100644 --- a/backend/src/settings/generic/mod.rs +++ b/backend/src/settings/generic/mod.rs @@ -11,7 +11,15 @@ pub use traits::FromGenericCpuInfo; fn _impl_checker() { fn impl_provider_builder, J, L>() {} - impl_provider_builder::(); - impl_provider_builder::, Vec, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::< + Battery, + crate::persist::BatteryJson, + limits_core::json_v2::GenericBatteryLimit, + >(); + impl_provider_builder::< + Cpus, + Vec, + limits_core::json_v2::GenericCpusLimit, + >(); impl_provider_builder::(); } diff --git a/backend/src/settings/generic_amd/cpu.rs b/backend/src/settings/generic_amd/cpu.rs index 07aef41..2f618b2 100644 --- a/backend/src/settings/generic_amd/cpu.rs +++ b/backend/src/settings/generic_amd/cpu.rs @@ -2,7 +2,7 @@ use crate::persist::CpuJson; use crate::settings::generic::{Cpu as GenericCpu, Cpus as GenericCpus, FromGenericCpuInfo}; use crate::settings::MinMax; use crate::settings::{OnResume, OnSet, SettingError}; -use crate::settings::{TCpu, TCpus, ProviderBuilder}; +use crate::settings::{ProviderBuilder, TCpu, TCpus}; #[derive(Debug)] pub struct Cpus { diff --git a/backend/src/settings/generic_amd/gpu.rs b/backend/src/settings/generic_amd/gpu.rs index 0a1331a..4b5066e 100644 --- a/backend/src/settings/generic_amd/gpu.rs +++ b/backend/src/settings/generic_amd/gpu.rs @@ -4,10 +4,14 @@ use std::sync::Mutex; use crate::persist::GpuJson; use crate::settings::generic::Gpu as GenericGpu; use crate::settings::MinMax; -use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError, SettingVariant}; +use crate::settings::{ProviderBuilder, TGpu}; -fn msg_or_err(output: &mut String, msg: &str, result: Result) { +fn msg_or_err( + output: &mut String, + msg: &str, + result: Result, +) { use std::fmt::Write; match result { Ok(val) => writeln!(output, "{}: {}", msg, val).unwrap(), @@ -16,20 +20,41 @@ fn msg_or_err(output: &mut String, m } fn log_capabilities(ryzenadj: &RyzenAdj) { - log::info!("RyzenAdj v{}.{}.{}", libryzenadj::libryzenadj_sys::RYZENADJ_REVISION_VER, libryzenadj::libryzenadj_sys::RYZENADJ_MAJOR_VER, libryzenadj::libryzenadj_sys::RYZENADJ_MINIOR_VER); + log::info!( + "RyzenAdj v{}.{}.{}", + libryzenadj::libryzenadj_sys::RYZENADJ_REVISION_VER, + libryzenadj::libryzenadj_sys::RYZENADJ_MAJOR_VER, + libryzenadj::libryzenadj_sys::RYZENADJ_MINIOR_VER + ); #[cfg(feature = "experimental")] if let Some(x) = ryzenadj.get_init_table_err() { log::warn!("RyzenAdj table init error: {}", x); } let mut log_msg = String::new(); msg_or_err(&mut log_msg, "bios version", ryzenadj.get_bios_if_ver()); - msg_or_err(&mut log_msg, "refresh", ryzenadj.refresh().map(|_| "success")); - msg_or_err(&mut log_msg, "CPU family", ryzenadj.get_cpu_family().map(|fam| { - let fam_dbg = format!("{:?}", fam); - format!("{} (#{})", fam_dbg, fam as i32) - })); - msg_or_err(&mut log_msg, "get_fast_value (PPT)", ryzenadj.get_fast_value()); - msg_or_err(&mut log_msg, "get_slow_value (PPT)", ryzenadj.get_slow_value()); + msg_or_err( + &mut log_msg, + "refresh", + ryzenadj.refresh().map(|_| "success"), + ); + msg_or_err( + &mut log_msg, + "CPU family", + ryzenadj.get_cpu_family().map(|fam| { + let fam_dbg = format!("{:?}", fam); + format!("{} (#{})", fam_dbg, fam as i32) + }), + ); + msg_or_err( + &mut log_msg, + "get_fast_value (PPT)", + ryzenadj.get_fast_value(), + ); + msg_or_err( + &mut log_msg, + "get_slow_value (PPT)", + ryzenadj.get_slow_value(), + ); msg_or_err(&mut log_msg, "get_gfx_clk", ryzenadj.get_gfx_clk()); msg_or_err(&mut log_msg, "get_gfx_volt", ryzenadj.get_gfx_volt()); @@ -41,7 +66,7 @@ fn ryzen_adj_or_log() -> Option> { Ok(x) => { log_capabilities(&x); Some(Mutex::new(x)) - }, + } Err(e) => { log::error!("RyzenAdj init error: {}", e); None @@ -90,7 +115,6 @@ impl ProviderBuilder for Gpu { } impl Gpu { - fn set_all(&mut self) -> Result<(), Vec> { let mutex = match &self.implementor { Some(x) => x, diff --git a/backend/src/settings/generic_amd/mod.rs b/backend/src/settings/generic_amd/mod.rs index 89a2292..c395fc1 100644 --- a/backend/src/settings/generic_amd/mod.rs +++ b/backend/src/settings/generic_amd/mod.rs @@ -7,6 +7,10 @@ pub use gpu::Gpu; fn _impl_checker() { fn impl_provider_builder, J, L>() {} - impl_provider_builder::, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::< + Cpus, + Vec, + limits_core::json_v2::GenericCpusLimit, + >(); impl_provider_builder::(); } diff --git a/backend/src/settings/mod.rs b/backend/src/settings/mod.rs index e18fa4b..91e4662 100644 --- a/backend/src/settings/mod.rs +++ b/backend/src/settings/mod.rs @@ -6,25 +6,37 @@ mod min_max; mod traits; mod util; +pub mod dev_mode; pub mod generic; pub mod generic_amd; pub mod steam_deck; pub mod unknown; -pub mod dev_mode; -pub use detect::{auto_detect0, auto_detect_provider, limits_worker::spawn as limits_worker_spawn, get_dev_messages}; +pub use detect::{ + auto_detect0, auto_detect_provider, get_dev_messages, + limits_worker::spawn as limits_worker_spawn, +}; pub use driver::Driver; pub use general::{General, SettingVariant, Settings}; pub use min_max::{min_max_from_json, MinMax}; pub use error::SettingError; -pub use traits::{OnPowerEvent, OnResume, OnSet, PowerMode, TBattery, TCpu, TCpus, TGeneral, TGpu, ProviderBuilder}; +pub use traits::{ + OnPowerEvent, OnResume, OnSet, PowerMode, ProviderBuilder, TBattery, TCpu, TCpus, TGeneral, + TGpu, +}; #[cfg(test)] mod tests { #[test] fn system_defaults_test() { - let settings = super::Settings::system_default("idc".into(), 0, "Cool name".into(), 0, "Variant 0".into()); + let settings = super::Settings::system_default( + "idc".into(), + 0, + "Cool name".into(), + 0, + "Variant 0".into(), + ); println!("Loaded system settings: {:?}", settings); } } diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index c0325eb..f47d9bd 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -1,16 +1,22 @@ use std::convert::Into; use std::sync::{Arc, Mutex}; -use sysfuss::{PowerSupplyAttribute, PowerSupplyPath, HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, SysEntity, SysEntityAttributesExt, SysAttribute}; use sysfuss::capability::attributes; +use sysfuss::{ + HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, PowerSupplyAttribute, + PowerSupplyPath, SysAttribute, SysEntity, SysEntityAttributesExt, +}; use limits_core::json_v2::GenericBatteryLimit; -use smokepatio::ec::{ControllerSet, unnamed_power::{UnnamedPowerEC, ChargeMode}}; use crate::api::RangeLimit; use crate::persist::{BatteryEventJson, BatteryJson}; -use crate::settings::{TBattery, ProviderBuilder}; use crate::settings::{OnPowerEvent, OnResume, OnSet, PowerMode, SettingError}; +use crate::settings::{ProviderBuilder, TBattery}; +use smokepatio::ec::{ + unnamed_power::{ChargeMode, UnnamedPowerEC}, + ControllerSet, +}; #[derive(Debug, Clone)] pub struct Battery { @@ -121,7 +127,12 @@ impl EventInstruction { } } - fn from_json(other: BatteryEventJson, _version: u64, hwmon: Arc, ec: Arc>) -> Self { + fn from_json( + other: BatteryEventJson, + _version: u64, + hwmon: Arc, + ec: Arc>, + ) -> Self { Self { trigger: Self::str_to_trigger(&other.trigger).unwrap_or(EventTrigger::Ignored), charge_rate: other.charge_rate, @@ -137,7 +148,10 @@ impl EventInstruction { fn set_charge_mode(&self) -> Result<(), SettingError> { if let Some(charge_mode) = self.charge_mode { - let mut lock = self.bat_ec.lock().expect("failed to lock battery controller"); + let mut lock = self + .bat_ec + .lock() + .expect("failed to lock battery controller"); lock.set(charge_mode).map_err(|_| SettingError { msg: format!("Failed to set charge mode"), setting: crate::settings::SettingVariant::Battery, @@ -149,12 +163,15 @@ impl EventInstruction { fn set_charge_rate(&self) -> Result<(), SettingError> { if let Some(charge_rate) = self.charge_rate { - self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_RATE_ATTR, charge_rate).map_err( - |e| SettingError { - msg: format!("Failed to write to `{:?}`: {}", MAX_BATTERY_CHARGE_RATE_ATTR, e), + self.sysfs_hwmon + .set(MAX_BATTERY_CHARGE_RATE_ATTR, charge_rate) + .map_err(|e| SettingError { + msg: format!( + "Failed to write to `{:?}`: {}", + MAX_BATTERY_CHARGE_RATE_ATTR, e + ), setting: crate::settings::SettingVariant::Battery, - }, - ) + }) } else { Ok(()) } @@ -194,7 +211,6 @@ const BATTERY_CHARGE_DESIGN_PATH: &str = "/sys/class/power_supply/BAT1/charge_fu const USB_PD_IN_MVOLTAGE_PATH: &str = "/sys/class/hwmon/hwmon5/in0_input"; // read-only const USB_PD_IN_CURRENT_PATH: &str = "/sys/class/hwmon/hwmon5/curr1_input"; // read-only*/ - const BATTERY_NEEDS: &[PowerSupplyAttribute] = &[ PowerSupplyAttribute::Type, PowerSupplyAttribute::CurrentNow, @@ -213,8 +229,10 @@ const HWMON_NEEDS: &[HwMonAttribute] = &[ //HwMonAttribute::custom("maximum_battery_charge_rate"), // NOTE: Cannot filter by custom capabilities ]; -const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute = HwMonAttribute::custom("maximum_battery_charge_rate"); -const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute = HwMonAttribute::custom("max_battery_charge_level"); +const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute = + HwMonAttribute::custom("maximum_battery_charge_rate"); +const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute = + HwMonAttribute::custom("max_battery_charge_level"); const MAX_CHARGE_RATE: u64 = 2500; const MIN_CHARGE_RATE: u64 = 250; @@ -229,9 +247,12 @@ impl Battery { log::error!("Failed to find SteamDeck battery power_supply in sysfs (no results), using naive fallback"); root.power_supply_by_name("BAT1") }); - log::info!("Found SteamDeck battery power_supply in sysfs: {}", psu.as_ref().display()); + log::info!( + "Found SteamDeck battery power_supply in sysfs: {}", + psu.as_ref().display() + ); psu - }, + } Err(e) => { log::error!("Failed to find SteamDeck battery power_supply in sysfs ({}), using naive fallback", e); root.power_supply_by_name("BAT1") @@ -245,11 +266,15 @@ impl Battery { Ok(hwmon) => { if !hwmon.capable(attributes(HWMON_NEEDS.into_iter().copied())) { log::warn!("Found incapable SteamDeck battery hwmon in sysfs (hwmon by name {} exists but missing attributes), persevering because ignorance is bliss", super::util::JUPITER_HWMON_NAME); - } else { - log::info!("Found SteamDeck battery hwmon {} in sysfs: {}", super::util::JUPITER_HWMON_NAME, hwmon.as_ref().display()); + } else { + log::info!( + "Found SteamDeck battery hwmon {} in sysfs: {}", + super::util::JUPITER_HWMON_NAME, + hwmon.as_ref().display() + ); } hwmon - }, + } Err(e) => { log::warn!("Failed to find SteamDeck battery hwmon {} in sysfs ({}), trying alternate name", super::util::JUPITER_HWMON_NAME, e); @@ -258,10 +283,14 @@ impl Battery { if !hwmon.capable(attributes(HWMON_NEEDS.into_iter().copied())) { log::warn!("Found incapable SteamDeck battery hwmon in sysfs (hwmon by name {} exists but missing attributes), persevering because ignorance is bliss", super::util::STEAMDECK_HWMON_NAME); } else { - log::info!("Found SteamDeck battery hwmon {} in sysfs: {}", super::util::STEAMDECK_HWMON_NAME, hwmon.as_ref().display()); + log::info!( + "Found SteamDeck battery hwmon {} in sysfs: {}", + super::util::STEAMDECK_HWMON_NAME, + hwmon.as_ref().display() + ); } hwmon - }, + } Err(e) => { log::error!("Failed to find SteamDeck battery hwmon {} in sysfs ({}), using naive fallback", super::util::STEAMDECK_HWMON_NAME, e); root.hwmon_by_index(5) @@ -295,21 +324,27 @@ impl Battery { if let Some(charge_rate) = self.charge_rate { self.state.charge_rate_set = true; let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon); - self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_RATE_ATTR, charge_rate).map_err( - |e| SettingError { + self.sysfs_hwmon + .set(MAX_BATTERY_CHARGE_RATE_ATTR, charge_rate) + .map_err(|e| SettingError { msg: format!("Failed to write to `{}`: {}", path.display(), e), setting: crate::settings::SettingVariant::Battery, - }, - ) + }) } else if self.state.charge_rate_set { self.state.charge_rate_set = false; let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon); - self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_RATE_ATTR, self.limits.charge_rate.and_then(|lim| lim.max).unwrap_or(2500)).map_err( - |e| SettingError { + self.sysfs_hwmon + .set( + MAX_BATTERY_CHARGE_RATE_ATTR, + self.limits + .charge_rate + .and_then(|lim| lim.max) + .unwrap_or(2500), + ) + .map_err(|e| SettingError { msg: format!("Failed to write to `{}`: {}", path.display(), e), setting: crate::settings::SettingVariant::Battery, - }, - ) + }) } else { Ok(()) } @@ -318,14 +353,20 @@ impl Battery { fn set_charge_mode(&mut self) -> Result<(), SettingError> { if let Some(charge_mode) = self.charge_mode { self.state.charge_mode_set = true; - let mut lock = self.bat_ec.lock().expect("Failed to lock battery controller"); + let mut lock = self + .bat_ec + .lock() + .expect("Failed to lock battery controller"); lock.set(charge_mode).map_err(|_| SettingError { msg: format!("Failed to set charge mode"), setting: crate::settings::SettingVariant::Battery, }) } else if self.state.charge_mode_set { self.state.charge_mode_set = false; - let mut lock = self.bat_ec.lock().expect("Failed to lock battery controller"); + let mut lock = self + .bat_ec + .lock() + .expect("Failed to lock battery controller"); lock.set(ChargeMode::Normal).map_err(|_| SettingError { msg: format!("Failed to set charge mode"), setting: crate::settings::SettingVariant::Battery, @@ -337,23 +378,35 @@ impl Battery { fn set_charge_limit(&mut self) -> Result<(), SettingError> { let attr_exists = MAX_BATTERY_CHARGE_LEVEL_ATTR.exists(&*self.sysfs_hwmon); - log::debug!("Does battery limit attribute (max_battery_charge_level) exist? {}", attr_exists); + log::debug!( + "Does battery limit attribute (max_battery_charge_level) exist? {}", + attr_exists + ); if let Some(charge_limit) = self.charge_limit { self.state.charge_limit_set = true; - self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_LEVEL_ATTR, (charge_limit * 100.0).round() as u64) - .map_err(|e| SettingError { - msg: format!("Failed to write to {:?}: {}", MAX_BATTERY_CHARGE_LEVEL_ATTR, e), - setting: crate::settings::SettingVariant::Battery, - } + self.sysfs_hwmon + .set( + MAX_BATTERY_CHARGE_LEVEL_ATTR, + (charge_limit * 100.0).round() as u64, ) + .map_err(|e| SettingError { + msg: format!( + "Failed to write to {:?}: {}", + MAX_BATTERY_CHARGE_LEVEL_ATTR, e + ), + setting: crate::settings::SettingVariant::Battery, + }) } else if self.state.charge_limit_set { self.state.charge_limit_set = false; - self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_LEVEL_ATTR, 0) + self.sysfs_hwmon + .set(MAX_BATTERY_CHARGE_LEVEL_ATTR, 0) .map_err(|e| SettingError { - msg: format!("Failed to reset (write to) {:?}: {}", MAX_BATTERY_CHARGE_LEVEL_ATTR, e), - setting: crate::settings::SettingVariant::Battery, - } - ) + msg: format!( + "Failed to reset (write to) {:?}: {}", + MAX_BATTERY_CHARGE_LEVEL_ATTR, e + ), + setting: crate::settings::SettingVariant::Battery, + }) } else { Ok(()) } @@ -373,8 +426,16 @@ impl Battery { fn clamp_all(&mut self) { if let Some(charge_rate) = &mut self.charge_rate { - *charge_rate = - (*charge_rate).clamp(self.limits.charge_rate.and_then(|lim| lim.min).unwrap_or(MIN_CHARGE_RATE), self.limits.charge_rate.and_then(|lim| lim.max).unwrap_or(MAX_CHARGE_RATE)); + *charge_rate = (*charge_rate).clamp( + self.limits + .charge_rate + .and_then(|lim| lim.min) + .unwrap_or(MIN_CHARGE_RATE), + self.limits + .charge_rate + .and_then(|lim| lim.max) + .unwrap_or(MAX_CHARGE_RATE), + ); } } @@ -558,13 +619,21 @@ impl Into for Battery { charge_rate: self.charge_rate, charge_mode: self.charge_mode.map(Self::charge_mode_to_str), events: events.into_iter().map(|x| x.into()).collect(), - root: self.sysfs_bat.root().or(self.sysfs_hwmon.root()).and_then(|p| p.as_ref().to_str().map(|x| x.to_owned())) + root: self + .sysfs_bat + .root() + .or(self.sysfs_hwmon.root()) + .and_then(|p| p.as_ref().to_str().map(|x| x.to_owned())), } } } impl ProviderBuilder for Battery { - fn from_json_and_limits(persistent: BatteryJson, version: u64, limits: GenericBatteryLimit) -> Self { + fn from_json_and_limits( + persistent: BatteryJson, + version: u64, + limits: GenericBatteryLimit, + ) -> Self { let hwmon_sys = Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)); let ec = Arc::new(Mutex::new(UnnamedPowerEC::new())); match version { @@ -586,14 +655,15 @@ impl ProviderBuilder for Battery { sysfs_hwmon: hwmon_sys, bat_ec: ec, variant: super::Model::LCD, - }.remove_charge_limit_instructions(), + } + .remove_charge_limit_instructions(), _ => Self { charge_rate: persistent.charge_rate, charge_mode: persistent .charge_mode .map(|x| Self::str_to_charge_mode(&x)) .flatten(), - charge_limit: None, + charge_limit: None, events: persistent .events .into_iter() @@ -605,7 +675,8 @@ impl ProviderBuilder for Battery { sysfs_hwmon: hwmon_sys, bat_ec: ec, variant: super::Model::LCD, - }.remove_charge_limit_instructions(), + } + .remove_charge_limit_instructions(), } } @@ -621,7 +692,8 @@ impl ProviderBuilder for Battery { sysfs_hwmon: Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)), bat_ec: Arc::new(Mutex::new(UnnamedPowerEC::new())), variant: super::Model::LCD, - }.remove_charge_limit_instructions() + } + .remove_charge_limit_instructions() } } @@ -675,8 +747,16 @@ impl TBattery for Battery { fn limits(&self) -> crate::api::BatteryLimits { crate::api::BatteryLimits { charge_current: Some(RangeLimit { - min: self.limits.charge_rate.and_then(|lim| lim.min).unwrap_or(MIN_CHARGE_RATE), - max: self.limits.charge_rate.and_then(|lim| lim.max).unwrap_or(MAX_CHARGE_RATE), + min: self + .limits + .charge_rate + .and_then(|lim| lim.min) + .unwrap_or(MIN_CHARGE_RATE), + max: self + .limits + .charge_rate + .and_then(|lim| lim.max) + .unwrap_or(MAX_CHARGE_RATE), }), charge_current_step: 50, charge_modes: vec![ diff --git a/backend/src/settings/steam_deck/cpu.rs b/backend/src/settings/steam_deck/cpu.rs index 34671c5..a1c78b6 100644 --- a/backend/src/settings/steam_deck/cpu.rs +++ b/backend/src/settings/steam_deck/cpu.rs @@ -2,22 +2,20 @@ use std::convert::Into; use sysfuss::{BasicEntityPath, SysEntity, SysEntityAttributesExt}; -use limits_core::json_v2::{GenericCpusLimit, GenericCpuLimit}; +use limits_core::json_v2::{GenericCpuLimit, GenericCpusLimit}; -use super::POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT; use super::util::{range_max_or_fallback, range_min_or_fallback}; +use super::POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT; use crate::api::RangeLimit; use crate::persist::CpuJson; use crate::settings::{min_max_from_json, MinMax}; use crate::settings::{OnResume, OnSet, SettingError}; -use crate::settings::{TCpu, TCpus, ProviderBuilder}; +use crate::settings::{ProviderBuilder, TCpu, TCpus}; const CPU_PRESENT_PATH: &str = "/sys/devices/system/cpu/present"; const CPU_SMT_PATH: &str = "/sys/devices/system/cpu/smt/control"; -const CARD_EXTENSIONS: &[&'static str] = &[ - super::DPM_FORCE_LIMITS_ATTRIBUTE -]; +const CARD_EXTENSIONS: &[&'static str] = &[super::DPM_FORCE_LIMITS_ATTRIBUTE]; const MAX_CLOCK: u64 = 3500; const MIN_MAX_CLOCK: u64 = 200; // minimum value allowed for maximum CPU clock, MHz @@ -109,7 +107,11 @@ impl Cpus { } impl ProviderBuilder, GenericCpusLimit> for Cpus { - fn from_json_and_limits(mut persistent: Vec, version: u64, limits: GenericCpusLimit) -> Self { + fn from_json_and_limits( + mut persistent: Vec, + version: u64, + limits: GenericCpusLimit, + ) -> Self { POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.reset(); let (_, can_smt) = Self::system_smt_capabilities(); let mut result = Vec::with_capacity(persistent.len()); @@ -123,18 +125,9 @@ impl ProviderBuilder, GenericCpusLimit> for Cpus { } } let new_cpu = if let Some(cpu_limit) = limits.cpus.get(i) { - Cpu::from_json_and_limits( - cpu, - version, - i, - cpu_limit.to_owned() - ) + Cpu::from_json_and_limits(cpu, version, i, cpu_limit.to_owned()) } else { - Cpu::from_json( - cpu, - version, - i, - ) + Cpu::from_json(cpu, version, i) }; result.push(new_cpu); } @@ -160,10 +153,7 @@ impl ProviderBuilder, GenericCpusLimit> for Cpus { let mut sys_cpus = Vec::with_capacity(max_cpu); for i in 0..max_cpu { let new_cpu = if let Some(cpu_limit) = limits.cpus.get(i) { - Cpu::from_limits( - i, - cpu_limit.to_owned() - ) + Cpu::from_limits(i, cpu_limit.to_owned()) } else { Cpu::system_default(i) }; @@ -255,7 +245,12 @@ enum ClockType { impl Cpu { #[inline] - fn from_json_and_limits(other: CpuJson, version: u64, i: usize, oc_limits: GenericCpuLimit) -> Self { + fn from_json_and_limits( + other: CpuJson, + version: u64, + i: usize, + oc_limits: GenericCpuLimit, + ) -> Self { match version { 0 => Self { online: other.online, @@ -280,13 +275,21 @@ impl Cpu { #[inline] fn from_json(other: CpuJson, version: u64, i: usize) -> Self { - let oc_limits = GenericCpuLimit::default_for(&limits_core::json_v2::CpuLimitType::SteamDeck, i); + let oc_limits = + GenericCpuLimit::default_for(&limits_core::json_v2::CpuLimitType::SteamDeck, i); Self::from_json_and_limits(other, version, i, oc_limits) } fn find_card_sysfs(root: Option>) -> BasicEntityPath { let root = crate::settings::util::root_or_default_sysfs(root); - match root.class("drm", sysfuss::capability::attributes(crate::settings::util::CARD_NEEDS.into_iter().map(|s| s.to_string()))) { + match root.class( + "drm", + sysfuss::capability::attributes( + crate::settings::util::CARD_NEEDS + .into_iter() + .map(|s| s.to_string()), + ), + ) { Ok(iter) => { let card = iter .filter(|ent| if let Ok(name) = ent.name() { name.starts_with("card")} else { false }) @@ -298,37 +301,45 @@ impl Cpu { }); log::info!("Found SteamDeck drm in sysfs: {}", card.as_ref().display()); card - }, + } Err(e) => { - log::error!("Failed to find SteamDeck drm in sysfs ({}), using naive fallback", e); + log::error!( + "Failed to find SteamDeck drm in sysfs ({}), using naive fallback", + e + ); BasicEntityPath::new(root.as_ref().join("sys/class/drm/card0")) } } } - fn set_clock_limit(&self, index: usize, speed: u64, mode: ClockType) -> Result<(), SettingError> { + fn set_clock_limit( + &self, + index: usize, + speed: u64, + mode: ClockType, + ) -> Result<(), SettingError> { let payload = format!("p {} {} {}\n", index / 2, mode as u8, speed); - self.sysfs.set(CPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), &payload).map_err(|e| { - SettingError { + self.sysfs + .set(CPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), &payload) + .map_err(|e| SettingError { msg: format!( "Failed to write `{}` to `{}`: {}", &payload, CPU_CLOCK_LIMITS_ATTRIBUTE, e ), setting: crate::settings::SettingVariant::Cpu, - } - }) + }) } fn reset_clock_limits(&self) -> Result<(), SettingError> { - self.sysfs.set(CPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), "r\n").map_err(|e| { - SettingError { + self.sysfs + .set(CPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), "r\n") + .map_err(|e| SettingError { msg: format!( "Failed to write `r` to `{}`: {}", CPU_CLOCK_LIMITS_ATTRIBUTE, e ), setting: crate::settings::SettingVariant::Cpu, - } - }) + }) } fn set_clock_limits(&mut self) -> Result<(), Vec> { @@ -350,11 +361,12 @@ impl Cpu { } // min clock if let Some(min) = clock_limits.min { - let valid_min = if min < range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK) { - range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK) - } else { - min - }; + let valid_min = + if min < range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK) { + range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK) + } else { + min + }; self.set_clock_limit(self.index, valid_min, ClockType::Min) .unwrap_or_else(|e| errors.push(e)); } @@ -371,22 +383,30 @@ impl Cpu { let mut errors = Vec::new(); self.state.clock_limits_set = false; POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_cpu(false, self.index); - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT - .enforce_level(&self.sysfs)?; + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.enforce_level(&self.sysfs)?; if POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.needs_manual() { // always set clock speeds, since it doesn't reset correctly (kernel/hardware bug) POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.enforce_level(&self.sysfs)?; // disable manual clock limits log::debug!("Setting CPU {} to default clockspeed", self.index); // max clock - self.set_clock_limit(self.index, range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), ClockType::Max) - .unwrap_or_else(|e| errors.push(e)); + self.set_clock_limit( + self.index, + range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), + ClockType::Max, + ) + .unwrap_or_else(|e| errors.push(e)); // min clock - self.set_clock_limit(self.index, range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK), ClockType::Min) - .unwrap_or_else(|e| errors.push(e)); + self.set_clock_limit( + self.index, + range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK), + ClockType::Min, + ) + .unwrap_or_else(|e| errors.push(e)); } // TODO remove this when it's no longer needed - self.clock_unset_workaround().unwrap_or_else(|mut e| errors.append(&mut e)); + self.clock_unset_workaround() + .unwrap_or_else(|mut e| errors.append(&mut e)); if errors.is_empty() { Ok(()) } else { @@ -407,11 +427,19 @@ impl Cpu { // disable manual clock limits log::debug!("Setting CPU {} to default clockspeed", self.index); // max clock - self.set_clock_limit(self.index, range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), ClockType::Max) - .unwrap_or_else(|e| errors.push(e)); + self.set_clock_limit( + self.index, + range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), + ClockType::Max, + ) + .unwrap_or_else(|e| errors.push(e)); // min clock - self.set_clock_limit(self.index, range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK), ClockType::Min) - .unwrap_or_else(|e| errors.push(e)); + self.set_clock_limit( + self.index, + range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK), + ClockType::Min, + ) + .unwrap_or_else(|e| errors.push(e)); self.set_confirm().unwrap_or_else(|e| errors.push(e)); @@ -430,12 +458,15 @@ impl Cpu { } fn set_confirm(&self) -> Result<(), SettingError> { - self.sysfs.set(CPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), "c\n").map_err(|e| { - SettingError { - msg: format!("Failed to write `c` to `{}`: {}", CPU_CLOCK_LIMITS_ATTRIBUTE, e), + self.sysfs + .set(CPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), "c\n") + .map_err(|e| SettingError { + msg: format!( + "Failed to write `c` to `{}`: {}", + CPU_CLOCK_LIMITS_ATTRIBUTE, e + ), setting: crate::settings::SettingVariant::Cpu, - } - }) + }) } fn set_force_performance_related(&mut self) -> Result<(), Vec> { @@ -504,12 +535,16 @@ impl Cpu { fn clamp_all(&mut self) { if let Some(clock_limits) = &mut self.clock_limits { if let Some(min) = clock_limits.min { - clock_limits.min = - Some(min.clamp(range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), range_max_or_fallback(&self.limits.clock_min, MAX_CLOCK))); + clock_limits.min = Some(min.clamp( + range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), + range_max_or_fallback(&self.limits.clock_min, MAX_CLOCK), + )); } if let Some(max) = clock_limits.max { - clock_limits.max = - Some(max.clamp(range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK))); + clock_limits.max = Some(max.clamp( + range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), + range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), + )); } } } @@ -534,12 +569,15 @@ impl Cpu { limits: oc_limits, index: cpu_index, state: crate::state::steam_deck::Cpu::default(), - sysfs: Self::find_card_sysfs(None::<&'static str>) + sysfs: Self::find_card_sysfs(None::<&'static str>), } } fn system_default(cpu_index: usize) -> Self { - Self::from_limits(cpu_index, GenericCpuLimit::default_for(&limits_core::json_v2::CpuLimitType::SteamDeck, cpu_index)) + Self::from_limits( + cpu_index, + GenericCpuLimit::default_for(&limits_core::json_v2::CpuLimitType::SteamDeck, cpu_index), + ) } fn limits(&self) -> crate::api::CpuLimits { @@ -578,7 +616,10 @@ impl Into for Cpu { online: self.online, clock_limits: self.clock_limits.map(|x| x.into()), governor: self.governor, - root: self.sysfs.root().and_then(|p| p.as_ref().to_str().map(|r| r.to_owned())) + root: self + .sysfs + .root() + .and_then(|p| p.as_ref().to_str().map(|r| r.to_owned())), } } } diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index e8aafb0..8ca3faa 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -1,15 +1,18 @@ use std::convert::Into; -use sysfuss::{BasicEntityPath, HwMonPath, SysEntity, capability::attributes, SysEntityAttributes, SysEntityAttributesExt, SysAttribute}; +use sysfuss::{ + capability::attributes, BasicEntityPath, HwMonPath, SysAttribute, SysEntity, + SysEntityAttributes, SysEntityAttributesExt, +}; use limits_core::json_v2::GenericGpuLimit; use super::POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT; use crate::api::RangeLimit; use crate::persist::GpuJson; -use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{min_max_from_json, MinMax}; use crate::settings::{OnResume, OnSet, SettingError}; +use crate::settings::{ProviderBuilder, TGpu}; // usually in /sys/class/hwmon/hwmon4/ const SLOW_PPT_ATTRIBUTE: sysfuss::HwMonAttribute = sysfuss::HwMonAttribute::custom("power1_cap"); @@ -60,7 +63,14 @@ const PPT_DIVISOR: u64 = 1_000; impl Gpu { fn find_card_sysfs(root: Option>) -> BasicEntityPath { let root = crate::settings::util::root_or_default_sysfs(root); - match root.class("drm", attributes(crate::settings::util::CARD_NEEDS.into_iter().map(|s| s.to_string()))) { + match root.class( + "drm", + attributes( + crate::settings::util::CARD_NEEDS + .into_iter() + .map(|s| s.to_string()), + ), + ) { Ok(iter) => { let card = iter .filter(|ent| if let Ok(name) = ent.name() { name.starts_with("card")} else { false }) @@ -70,11 +80,17 @@ impl Gpu { log::error!("Failed to find SteamDeck gpu drm in sysfs (no results), using naive fallback"); BasicEntityPath::new(root.as_ref().join("sys/class/drm/card0")) }); - log::info!("Found SteamDeck gpu drm in sysfs: {}", card.as_ref().display()); + log::info!( + "Found SteamDeck gpu drm in sysfs: {}", + card.as_ref().display() + ); card - }, + } Err(e) => { - log::error!("Failed to find SteamDeck gpu drm in sysfs ({}), using naive fallback", e); + log::error!( + "Failed to find SteamDeck gpu drm in sysfs ({}), using naive fallback", + e + ); BasicEntityPath::new(root.as_ref().join("sys/class/drm/card0")) } } @@ -82,33 +98,47 @@ impl Gpu { fn find_hwmon_sysfs(root: Option>) -> HwMonPath { let root = crate::settings::util::root_or_default_sysfs(root); - let hwmon = root.hwmon_by_name(super::util::GPU_HWMON_NAME).unwrap_or_else(|e| { - log::error!("Failed to find SteamDeck gpu hwmon in sysfs ({}), using naive fallback", e); - root.hwmon_by_index(4) - }); - log::info!("Found SteamDeck gpu hwmon {} in sysfs: {}", super::util::GPU_HWMON_NAME, hwmon.as_ref().display()); + let hwmon = root + .hwmon_by_name(super::util::GPU_HWMON_NAME) + .unwrap_or_else(|e| { + log::error!( + "Failed to find SteamDeck gpu hwmon in sysfs ({}), using naive fallback", + e + ); + root.hwmon_by_index(4) + }); + log::info!( + "Found SteamDeck gpu hwmon {} in sysfs: {}", + super::util::GPU_HWMON_NAME, + hwmon.as_ref().display() + ); hwmon } fn set_clock_limit(&self, speed: u64, mode: ClockType) -> Result<(), SettingError> { let payload = format!("s {} {}\n", mode as u8, speed); let path = GPU_CLOCK_LIMITS_ATTRIBUTE.path(&self.sysfs_card); - self.sysfs_card.set(GPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), &payload).map_err(|e| { - SettingError { - msg: format!("Failed to write `{}` to `{}`: {}", &payload, path.display(), e), + self.sysfs_card + .set(GPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), &payload) + .map_err(|e| SettingError { + msg: format!( + "Failed to write `{}` to `{}`: {}", + &payload, + path.display(), + e + ), setting: crate::settings::SettingVariant::Gpu, - } - }) + }) } fn set_confirm(&self) -> Result<(), SettingError> { let path = GPU_CLOCK_LIMITS_ATTRIBUTE.path(&self.sysfs_card); - self.sysfs_card.set(GPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), "c\n").map_err(|e| { - SettingError { + self.sysfs_card + .set(GPU_CLOCK_LIMITS_ATTRIBUTE.to_owned(), "c\n") + .map_err(|e| SettingError { msg: format!("Failed to write `c` to `{}`: {}", path.display(), e), setting: crate::settings::SettingVariant::Gpu, - } - }) + }) } fn is_memory_clock_maxed(&self) -> bool { @@ -116,7 +146,12 @@ impl Gpu { if let Some(limit) = &self.limits.memory_clock { if let Some(limit) = &limit.max { if let Some(step) = &self.limits.memory_clock_step { - log::debug!("chosen_clock: {}, limit_clock: {}, step: {}", clock, limit, step); + log::debug!( + "chosen_clock: {}, limit_clock: {}, step: {}", + clock, + limit, + step + ); return clock > &(limit - step); } else { log::debug!("chosen_clock: {}, limit_clock: {}", clock, limit); @@ -129,7 +164,10 @@ impl Gpu { } fn quantize_memory_clock(&self, clock: u64) -> u64 { - if let Ok(f) = self.sysfs_card.read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) { + if let Ok(f) = self + .sysfs_card + .read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) + { let options = parse_pp_dpm_fclk(&String::from_utf8_lossy(&f)); // round (and find) nearest valid clock step // roughly price is right strategy (clock step will always be lower or equal to chosen) @@ -142,7 +180,7 @@ impl Gpu { if i == 0 { return *current_val_opt as _; } else { - return options[i-1].0 as _; + return options[i - 1].0 as _; } } } @@ -160,9 +198,11 @@ impl Gpu { use std::fmt::Write; let mut payload = String::from("0"); for i in 1..max_val { - write!(payload, " {}", i).expect("Failed to write to memory payload (should be infallible!?)"); + write!(payload, " {}", i) + .expect("Failed to write to memory payload (should be infallible!?)"); } - write!(payload, " {}\n", max_val).expect("Failed to write to memory payload (should be infallible!?)"); + write!(payload, " {}\n", max_val) + .expect("Failed to write to memory payload (should be infallible!?)"); payload } } @@ -177,11 +217,13 @@ impl Gpu { self.state.clock_limits_set = true; // max clock if let Some(max) = clock_limits.max { - self.set_clock_limit(max, ClockType::Max).unwrap_or_else(|e| errors.push(e)); + self.set_clock_limit(max, ClockType::Max) + .unwrap_or_else(|e| errors.push(e)); } // min clock if let Some(min) = clock_limits.min { - self.set_clock_limit(min, ClockType::Min).unwrap_or_else(|e| errors.push(e)); + self.set_clock_limit(min, ClockType::Min) + .unwrap_or_else(|e| errors.push(e)); } self.set_confirm().unwrap_or_else(|e| errors.push(e)); @@ -195,11 +237,23 @@ impl Gpu { POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.enforce_level(&self.sysfs_card)?; // disable manual clock limits // max clock - self.set_clock_limit(self.limits.clock_max.and_then(|lim| lim.max).unwrap_or(MAX_CLOCK), ClockType::Max) - .unwrap_or_else(|e| errors.push(e)); + self.set_clock_limit( + self.limits + .clock_max + .and_then(|lim| lim.max) + .unwrap_or(MAX_CLOCK), + ClockType::Max, + ) + .unwrap_or_else(|e| errors.push(e)); // min clock - self.set_clock_limit(self.limits.clock_min.and_then(|lim| lim.min).unwrap_or(MIN_CLOCK), ClockType::Min) - .unwrap_or_else(|e| errors.push(e)); + self.set_clock_limit( + self.limits + .clock_min + .and_then(|lim| lim.min) + .unwrap_or(MIN_CLOCK), + ClockType::Min, + ) + .unwrap_or_else(|e| errors.push(e)); self.set_confirm().unwrap_or_else(|e| errors.push(e)); } else { @@ -218,28 +272,37 @@ impl Gpu { fn set_memory_speed(&self, clock: u64) -> Result<(), SettingError> { let path = GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.path(&self.sysfs_card); let payload = self.build_memory_clock_payload(clock); - log::debug!("Generated payload for gpu fclk (memory): `{}` (is maxed? {})", payload, self.is_memory_clock_maxed()); - self.sysfs_card.set(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned(), payload).map_err(|e| { - SettingError { + log::debug!( + "Generated payload for gpu fclk (memory): `{}` (is maxed? {})", + payload, + self.is_memory_clock_maxed() + ); + self.sysfs_card + .set(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned(), payload) + .map_err(|e| SettingError { msg: format!("Failed to write to `{}`: {}", path.display(), e), setting: crate::settings::SettingVariant::Gpu, - } - }) + }) } fn set_force_performance_related(&mut self) -> Result<(), Vec> { let mut errors = Vec::new(); - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_gpu(!self.is_memory_clock_maxed() || self.clock_limits.is_some()); POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT - .enforce_level(&self.sysfs_card) - .unwrap_or_else(|mut e| errors.append(&mut e)); + .set_gpu(!self.is_memory_clock_maxed() || self.clock_limits.is_some()); + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT + .enforce_level(&self.sysfs_card) + .unwrap_or_else(|mut e| errors.append(&mut e)); // enable/disable downclock of GPU memory (to 400Mhz?) self.set_memory_speed( self.memory_clock - .or_else(|| self.limits.memory_clock - .map(|lim| lim.max.unwrap_or(MAX_MEMORY_CLOCK)) - ).unwrap_or(MAX_MEMORY_CLOCK) - ).unwrap_or_else(|e| errors.push(e)); + .or_else(|| { + self.limits + .memory_clock + .map(|lim| lim.max.unwrap_or(MAX_MEMORY_CLOCK)) + }) + .unwrap_or(MAX_MEMORY_CLOCK), + ) + .unwrap_or_else(|e| errors.push(e)); self.set_clocks() .unwrap_or_else(|mut e| errors.append(&mut e)); // commit changes (if no errors have already occured) @@ -262,7 +325,8 @@ impl Gpu { // set fast PPT if let Some(fast_ppt) = &self.fast_ppt { self.state.fast_ppt_set = true; - self.sysfs_hwmon.set(FAST_PPT_ATTRIBUTE, fast_ppt) + self.sysfs_hwmon + .set(FAST_PPT_ATTRIBUTE, fast_ppt) .map_err(|e| SettingError { msg: format!( "Failed to write `{}` to `{:?}`: {}", @@ -276,7 +340,8 @@ impl Gpu { } else if self.state.fast_ppt_set { self.state.fast_ppt_set = false; let fast_ppt = self.limits.fast_ppt_default.unwrap_or(MIDDLE_PPT); - self.sysfs_hwmon.set(FAST_PPT_ATTRIBUTE, fast_ppt) + self.sysfs_hwmon + .set(FAST_PPT_ATTRIBUTE, fast_ppt) .map_err(|e| SettingError { msg: format!( "Failed to write `{}` to `{:?}`: {}", @@ -291,7 +356,8 @@ impl Gpu { // set slow PPT if let Some(slow_ppt) = &self.slow_ppt { self.state.slow_ppt_set = true; - self.sysfs_hwmon.set(SLOW_PPT_ATTRIBUTE, slow_ppt) + self.sysfs_hwmon + .set(SLOW_PPT_ATTRIBUTE, slow_ppt) .map_err(|e| SettingError { msg: format!( "Failed to write `{}` to `{:?}`: {}", @@ -305,7 +371,8 @@ impl Gpu { } else if self.state.slow_ppt_set { self.state.slow_ppt_set = false; let slow_ppt = self.limits.slow_ppt_default.unwrap_or(MIDDLE_PPT); - self.sysfs_hwmon.set(SLOW_PPT_ATTRIBUTE, slow_ppt) + self.sysfs_hwmon + .set(SLOW_PPT_ATTRIBUTE, slow_ppt) .map_err(|e| SettingError { msg: format!( "Failed to write `{}` to `{:?}`: {}", @@ -328,23 +395,72 @@ impl Gpu { fn clamp_all(&mut self) { if let Some(fast_ppt) = &mut self.fast_ppt { - *fast_ppt = (*fast_ppt).clamp(self.limits.fast_ppt.and_then(|lim| lim.min).unwrap_or(MIN_FAST_PPT), self.limits.fast_ppt.and_then(|lim| lim.max).unwrap_or(MAX_FAST_PPT)); + *fast_ppt = (*fast_ppt).clamp( + self.limits + .fast_ppt + .and_then(|lim| lim.min) + .unwrap_or(MIN_FAST_PPT), + self.limits + .fast_ppt + .and_then(|lim| lim.max) + .unwrap_or(MAX_FAST_PPT), + ); } if let Some(slow_ppt) = &mut self.slow_ppt { - *slow_ppt = (*slow_ppt).clamp(self.limits.slow_ppt.and_then(|lim| lim.min).unwrap_or(MIN_SLOW_PPT), self.limits.slow_ppt.and_then(|lim| lim.max).unwrap_or(MAX_SLOW_PPT)); + *slow_ppt = (*slow_ppt).clamp( + self.limits + .slow_ppt + .and_then(|lim| lim.min) + .unwrap_or(MIN_SLOW_PPT), + self.limits + .slow_ppt + .and_then(|lim| lim.max) + .unwrap_or(MAX_SLOW_PPT), + ); } if let Some(clock_limits) = &mut self.clock_limits { if let Some(min) = clock_limits.min { - clock_limits.min = - Some(min.clamp(self.limits.clock_min.and_then(|lim| lim.min).unwrap_or(MIN_CLOCK), self.limits.clock_min.and_then(|lim| lim.max).unwrap_or(MAX_CLOCK))); + clock_limits.min = Some( + min.clamp( + self.limits + .clock_min + .and_then(|lim| lim.min) + .unwrap_or(MIN_CLOCK), + self.limits + .clock_min + .and_then(|lim| lim.max) + .unwrap_or(MAX_CLOCK), + ), + ); } if let Some(max) = clock_limits.max { - clock_limits.max = - Some(max.clamp(self.limits.clock_max.and_then(|lim| lim.min).unwrap_or(MIN_CLOCK), self.limits.clock_max.and_then(|lim| lim.max).unwrap_or(MAX_CLOCK))); + clock_limits.max = Some( + max.clamp( + self.limits + .clock_max + .and_then(|lim| lim.min) + .unwrap_or(MIN_CLOCK), + self.limits + .clock_max + .and_then(|lim| lim.max) + .unwrap_or(MAX_CLOCK), + ), + ); } } if let Some(mem_clock) = self.memory_clock { - self.memory_clock = Some(mem_clock.clamp(self.limits.memory_clock.and_then(|lim| lim.min).unwrap_or(MIN_MEMORY_CLOCK), self.limits.memory_clock.and_then(|lim| lim.max).unwrap_or(MAX_MEMORY_CLOCK))); + self.memory_clock = Some( + mem_clock.clamp( + self.limits + .memory_clock + .and_then(|lim| lim.min) + .unwrap_or(MIN_MEMORY_CLOCK), + self.limits + .memory_clock + .and_then(|lim| lim.max) + .unwrap_or(MAX_MEMORY_CLOCK), + ), + ); } } @@ -364,7 +480,11 @@ impl Into for Gpu { tdp_boost: None, clock_limits: self.clock_limits.map(|x| x.into()), memory_clock: self.memory_clock, - root: self.sysfs_card.root().or(self.sysfs_hwmon.root()).and_then(|p| p.as_ref().to_str().map(|r| r.to_owned())) + root: self + .sysfs_card + .root() + .or(self.sysfs_hwmon.root()) + .and_then(|p| p.as_ref().to_str().map(|r| r.to_owned())), } } } @@ -375,7 +495,9 @@ impl ProviderBuilder for Gpu { 0 => Self { fast_ppt: persistent.fast_ppt, slow_ppt: persistent.slow_ppt, - clock_limits: persistent.clock_limits.map(|x| min_max_from_json(x, version)), + clock_limits: persistent + .clock_limits + .map(|x| min_max_from_json(x, version)), memory_clock: persistent.memory_clock, limits: limits, state: crate::state::steam_deck::Gpu::default(), @@ -386,7 +508,9 @@ impl ProviderBuilder for Gpu { _ => Self { fast_ppt: persistent.fast_ppt, slow_ppt: persistent.slow_ppt, - clock_limits: persistent.clock_limits.map(|x| min_max_from_json(x, version)), + clock_limits: persistent + .clock_limits + .map(|x| min_max_from_json(x, version)), memory_clock: persistent.memory_clock, limits: limits, state: crate::state::steam_deck::Gpu::default(), @@ -433,12 +557,16 @@ impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { crate::api::GpuLimits { fast_ppt_limits: Some(RangeLimit { - min: super::util::range_min_or_fallback(&self.limits.fast_ppt, MIN_FAST_PPT) / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), - max: super::util::range_max_or_fallback(&self.limits.fast_ppt, MAX_FAST_PPT) / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + min: super::util::range_min_or_fallback(&self.limits.fast_ppt, MIN_FAST_PPT) + / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + max: super::util::range_max_or_fallback(&self.limits.fast_ppt, MAX_FAST_PPT) + / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), }), slow_ppt_limits: Some(RangeLimit { - min: super::util::range_min_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), - max: super::util::range_max_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + min: super::util::range_min_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) + / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + max: super::util::range_max_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) + / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), }), ppt_step: self.limits.ppt_step.unwrap_or(1), tdp_limits: None, @@ -454,8 +582,14 @@ impl TGpu for Gpu { }), clock_step: self.limits.clock_step.unwrap_or(100), memory_control: Some(RangeLimit { - min: super::util::range_min_or_fallback(&self.limits.memory_clock, MIN_MEMORY_CLOCK), - max: super::util::range_max_or_fallback(&self.limits.memory_clock, MAX_MEMORY_CLOCK), + min: super::util::range_min_or_fallback( + &self.limits.memory_clock, + MIN_MEMORY_CLOCK, + ), + max: super::util::range_max_or_fallback( + &self.limits.memory_clock, + MAX_MEMORY_CLOCK, + ), }), memory_step: self.limits.memory_clock_step.unwrap_or(400), } @@ -472,8 +606,10 @@ impl TGpu for Gpu { fn get_ppt(&self) -> (Option, Option) { ( - self.fast_ppt.map(|x| x / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR)), - self.slow_ppt.map(|x| x / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR)), + self.fast_ppt + .map(|x| x / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR)), + self.slow_ppt + .map(|x| x / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR)), ) } @@ -501,13 +637,16 @@ impl TGpu for Gpu { } } -fn parse_pp_dpm_fclk(s: &str) -> Vec<(usize, usize)> { // (value, MHz) +fn parse_pp_dpm_fclk(s: &str) -> Vec<(usize, usize)> { + // (value, MHz) let mut result = Vec::new(); for line in s.split('\n') { if !line.is_empty() { if let Some((val, freq_mess)) = line.split_once(':') { if let Ok(val) = val.parse::() { - if let Some((freq, _unit)) = freq_mess.trim().split_once(|c: char| !c.is_digit(10)) { + if let Some((freq, _unit)) = + freq_mess.trim().split_once(|c: char| !c.is_digit(10)) + { if let Ok(freq) = freq.parse::() { result.push((val, freq)); } diff --git a/backend/src/settings/steam_deck/mod.rs b/backend/src/settings/steam_deck/mod.rs index 5adc6c6..c5a64ce 100644 --- a/backend/src/settings/steam_deck/mod.rs +++ b/backend/src/settings/steam_deck/mod.rs @@ -7,7 +7,9 @@ mod util; pub use battery::Battery; pub use cpu::Cpus; pub use gpu::Gpu; -pub(self) use power_dpm_force::{POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT, DPM_FORCE_LIMITS_ATTRIBUTE}; +pub(self) use power_dpm_force::{ + DPM_FORCE_LIMITS_ATTRIBUTE, POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT, +}; #[derive(Debug, Clone, Copy)] pub enum Model { @@ -20,7 +22,15 @@ pub use util::flash_led; fn _impl_checker() { fn impl_provider_builder, J, L>() {} - impl_provider_builder::(); - impl_provider_builder::, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::< + Battery, + crate::persist::BatteryJson, + limits_core::json_v2::GenericBatteryLimit, + >(); + impl_provider_builder::< + Cpus, + Vec, + limits_core::json_v2::GenericCpusLimit, + >(); impl_provider_builder::(); } diff --git a/backend/src/settings/steam_deck/power_dpm_force.rs b/backend/src/settings/steam_deck/power_dpm_force.rs index d804a18..039026a 100644 --- a/backend/src/settings/steam_deck/power_dpm_force.rs +++ b/backend/src/settings/steam_deck/power_dpm_force.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; -use sysfuss::{BasicEntityPath, SysEntityAttributesExt, SysAttribute}; +use sysfuss::{BasicEntityPath, SysAttribute, SysEntityAttributesExt}; use crate::settings::SettingError; @@ -63,7 +63,8 @@ impl PDFPLManager { let needs = self.needs_manual(); let mut errors = Vec::new(); let path = DPM_FORCE_LIMITS_ATTRIBUTE.path(entity); - let mode: String = entity.attribute(DPM_FORCE_LIMITS_ATTRIBUTE.to_owned()) + let mode: String = entity + .attribute(DPM_FORCE_LIMITS_ATTRIBUTE.to_owned()) .map_err(|e| { vec![SettingError { msg: format!("Failed to read `{}`: {}", path.display(), e), @@ -73,7 +74,8 @@ impl PDFPLManager { if mode != "manual" && needs { log::info!("Setting `{}` to manual", path.display()); // set manual control - entity.set(DPM_FORCE_LIMITS_ATTRIBUTE.to_owned(), "manual") + entity + .set(DPM_FORCE_LIMITS_ATTRIBUTE.to_owned(), "manual") .map_err(|e| { errors.push(SettingError { msg: format!("Failed to write `manual` to `{}`: {}", path.display(), e), @@ -84,7 +86,8 @@ impl PDFPLManager { } else if mode != "auto" && !needs { log::info!("Setting `{}` to auto", path.display()); // unset manual control - entity.set(DPM_FORCE_LIMITS_ATTRIBUTE.to_owned(), "auto") + entity + .set(DPM_FORCE_LIMITS_ATTRIBUTE.to_owned(), "auto") .map_err(|e| { errors.push(SettingError { msg: format!("Failed to write `auto` to `{}`: {}", path.display(), e), @@ -93,10 +96,13 @@ impl PDFPLManager { }) .unwrap_or(()); } - if let Ok(mode_now) = - entity.attribute::(DPM_FORCE_LIMITS_ATTRIBUTE.to_owned()) - { - log::debug!("Mode for `{}` is now `{}` ({:#b})", path.display(), mode_now, self.get()); + if let Ok(mode_now) = entity.attribute::(DPM_FORCE_LIMITS_ATTRIBUTE.to_owned()) { + log::debug!( + "Mode for `{}` is now `{}` ({:#b})", + path.display(), + mode_now, + self.get() + ); } else { log::debug!("Error getting new mode for debugging purposes"); } diff --git a/backend/src/settings/steam_deck/util.rs b/backend/src/settings/steam_deck/util.rs index 2d37f51..88d940c 100644 --- a/backend/src/settings/steam_deck/util.rs +++ b/backend/src/settings/steam_deck/util.rs @@ -4,16 +4,23 @@ pub const JUPITER_HWMON_NAME: &'static str = "jupiter"; pub const STEAMDECK_HWMON_NAME: &'static str = "steamdeck_hwmon"; pub const GPU_HWMON_NAME: &'static str = "amdgpu"; -pub fn range_min_or_fallback(range: &Option>, fallback: I) -> I { +pub fn range_min_or_fallback( + range: &Option>, + fallback: I, +) -> I { range.and_then(|lim| lim.min).unwrap_or(fallback) } -pub fn range_max_or_fallback(range: &Option>, fallback: I) -> I { +pub fn range_max_or_fallback( + range: &Option>, + fallback: I, +) -> I { range.and_then(|lim| lim.max).unwrap_or(fallback) } pub fn card_also_has(card: &dyn sysfuss::SysEntity, extensions: &'static [&'static str]) -> bool { - extensions.iter() + extensions + .iter() .all(|ext| card.as_ref().join(ext).exists()) } @@ -31,7 +38,11 @@ pub fn flash_led() { let mut ec = smokepatio::ec::unnamed_power::UnnamedPowerEC::new(); for &code in THINGS { let on = code != 0; - let colour = if on { smokepatio::ec::unnamed_power::StaticColour::Red } else { smokepatio::ec::unnamed_power::StaticColour::Off }; + let colour = if on { + smokepatio::ec::unnamed_power::StaticColour::Red + } else { + smokepatio::ec::unnamed_power::StaticColour::Off + }; if let Err(e) = ec.set(colour) { log::error!("Thing err: {}", e); } diff --git a/backend/src/settings/traits.rs b/backend/src/settings/traits.rs index 5f993b2..f68f5cf 100644 --- a/backend/src/settings/traits.rs +++ b/backend/src/settings/traits.rs @@ -125,7 +125,10 @@ pub trait TGeneral: OnSet + OnResume + OnPowerEvent + Debug + Send { fn get_variant_info(&self) -> crate::api::VariantInfo; - fn add_variant(&self, variant: crate::persist::SettingsJson) -> Result, SettingError>; + fn add_variant( + &self, + variant: crate::persist::SettingsJson, + ) -> Result, SettingError>; fn provider(&self) -> crate::persist::DriverJson; } diff --git a/backend/src/settings/unknown/battery.rs b/backend/src/settings/unknown/battery.rs index 6c1a2e3..cdd5c96 100644 --- a/backend/src/settings/unknown/battery.rs +++ b/backend/src/settings/unknown/battery.rs @@ -3,8 +3,8 @@ use std::convert::Into; use limits_core::json_v2::GenericBatteryLimit; use crate::persist::BatteryJson; -use crate::settings::{TBattery, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError}; +use crate::settings::{ProviderBuilder, TBattery}; #[derive(Debug, Clone)] pub struct Battery; @@ -29,7 +29,11 @@ impl Into for Battery { } impl ProviderBuilder for Battery { - fn from_json_and_limits(_persistent: BatteryJson, _version: u64, _limits: GenericBatteryLimit) -> Self { + fn from_json_and_limits( + _persistent: BatteryJson, + _version: u64, + _limits: GenericBatteryLimit, + ) -> Self { Battery::system_default() } diff --git a/backend/src/settings/unknown/cpu.rs b/backend/src/settings/unknown/cpu.rs index 25130ca..2aed5c8 100644 --- a/backend/src/settings/unknown/cpu.rs +++ b/backend/src/settings/unknown/cpu.rs @@ -5,7 +5,7 @@ use limits_core::json_v2::GenericCpusLimit; use crate::persist::CpuJson; use crate::settings::MinMax; use crate::settings::{OnResume, OnSet, SettingError}; -use crate::settings::{TCpu, TCpus, ProviderBuilder}; +use crate::settings::{ProviderBuilder, TCpu, TCpus}; const CPU_PRESENT_PATH: &str = "/sys/devices/system/cpu/present"; const CPU_SMT_PATH: &str = "/sys/devices/system/cpu/smt/control"; @@ -146,7 +146,11 @@ impl Cpus { } impl ProviderBuilder, GenericCpusLimit> for Cpus { - fn from_json_and_limits(mut persistent: Vec, version: u64, _limits: GenericCpusLimit) -> Self { + fn from_json_and_limits( + mut persistent: Vec, + version: u64, + _limits: GenericCpusLimit, + ) -> Self { let (_, can_smt) = Self::system_smt_capabilities(); let mut result = Vec::with_capacity(persistent.len()); let max_cpus = Self::cpu_count(); @@ -284,7 +288,7 @@ impl Cpu { .unwrap_or("schedutil".to_owned()), index: cpu_index, state: crate::state::steam_deck::Cpu::default(), - root: "/".into() + root: "/".into(), } } diff --git a/backend/src/settings/unknown/gpu.rs b/backend/src/settings/unknown/gpu.rs index c676cda..4adcfec 100644 --- a/backend/src/settings/unknown/gpu.rs +++ b/backend/src/settings/unknown/gpu.rs @@ -4,15 +4,15 @@ use limits_core::json_v2::GenericGpuLimit; use crate::persist::GpuJson; use crate::settings::MinMax; -use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError}; +use crate::settings::{ProviderBuilder, TGpu}; #[derive(Debug, Clone)] pub struct Gpu {} impl Gpu { pub fn system_default() -> Self { - Self { } + Self {} } } diff --git a/backend/src/settings/unknown/mod.rs b/backend/src/settings/unknown/mod.rs index 200c1ea..58b6ac1 100644 --- a/backend/src/settings/unknown/mod.rs +++ b/backend/src/settings/unknown/mod.rs @@ -9,7 +9,15 @@ pub use gpu::Gpu; fn _impl_checker() { fn impl_provider_builder, J, L>() {} - impl_provider_builder::(); - impl_provider_builder::, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::< + Battery, + crate::persist::BatteryJson, + limits_core::json_v2::GenericBatteryLimit, + >(); + impl_provider_builder::< + Cpus, + Vec, + limits_core::json_v2::GenericCpusLimit, + >(); impl_provider_builder::(); } diff --git a/backend/src/settings/util.rs b/backend/src/settings/util.rs index 6d4e8ed..cebd95b 100644 --- a/backend/src/settings/util.rs +++ b/backend/src/settings/util.rs @@ -18,10 +18,7 @@ pub fn always_satisfied<'a, X>(_: &'a X) -> bool { true } -pub const CARD_NEEDS: &[&'static str] = &[ - "dev", - "uevent" -]; +pub const CARD_NEEDS: &[&'static str] = &["dev", "uevent"]; #[cfg(test)] mod test { diff --git a/backend/src/utility.rs b/backend/src/utility.rs index f6999c7..2aa20f4 100644 --- a/backend/src/utility.rs +++ b/backend/src/utility.rs @@ -120,51 +120,66 @@ mod generate { let limits = limits_core::json_v2::Limits { cpu: limits_core::json_v2::Limit { provider: limits_core::json_v2::CpuLimitType::SteamDeck, - limits: limits_core::json_v2::GenericCpusLimit::default_for(limits_core::json_v2::CpuLimitType::SteamDeck), + limits: limits_core::json_v2::GenericCpusLimit::default_for( + limits_core::json_v2::CpuLimitType::SteamDeck, + ), }, gpu: limits_core::json_v2::Limit { provider: limits_core::json_v2::GpuLimitType::SteamDeck, - limits: limits_core::json_v2::GenericGpuLimit::default_for(limits_core::json_v2::GpuLimitType::SteamDeck), + limits: limits_core::json_v2::GenericGpuLimit::default_for( + limits_core::json_v2::GpuLimitType::SteamDeck, + ), }, battery: limits_core::json_v2::Limit { provider: limits_core::json_v2::BatteryLimitType::SteamDeck, - limits: limits_core::json_v2::GenericBatteryLimit::default_for(limits_core::json_v2::BatteryLimitType::SteamDeck), + limits: limits_core::json_v2::GenericBatteryLimit::default_for( + limits_core::json_v2::BatteryLimitType::SteamDeck, + ), }, }; - let output_file = std::fs::File::create(format!("../{}", crate::consts::LIMITS_OVERRIDE_FILE)).unwrap(); - ron::ser::to_writer_pretty(output_file, &limits, crate::utility::ron_pretty_config()).unwrap(); + let output_file = + std::fs::File::create(format!("../{}", crate::consts::LIMITS_OVERRIDE_FILE)).unwrap(); + ron::ser::to_writer_pretty(output_file, &limits, crate::utility::ron_pretty_config()) + .unwrap(); } #[test] fn generate_default_minimal_save_file() { let mut mini_variants = std::collections::HashMap::with_capacity(2); - mini_variants.insert(0, crate::persist::SettingsJson { - version: 0, - name: crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(), - variant: 0, - persistent: false, - cpus: vec![crate::persist::CpuJson::default(); 8], - gpu: crate::persist::GpuJson::default(), - battery: crate::persist::BatteryJson::default(), - provider: None, - }); - mini_variants.insert(42, crate::persist::SettingsJson { - version: 0, - name: "FortySecondary".to_owned(), - variant: 42, - persistent: false, - cpus: vec![crate::persist::CpuJson::default(); 8], - gpu: crate::persist::GpuJson::default(), - battery: crate::persist::BatteryJson::default(), - provider: None, - }); + mini_variants.insert( + 0, + crate::persist::SettingsJson { + version: 0, + name: crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(), + variant: 0, + persistent: false, + cpus: vec![crate::persist::CpuJson::default(); 8], + gpu: crate::persist::GpuJson::default(), + battery: crate::persist::BatteryJson::default(), + provider: None, + }, + ); + mini_variants.insert( + 42, + crate::persist::SettingsJson { + version: 0, + name: "FortySecondary".to_owned(), + variant: 42, + persistent: false, + cpus: vec![crate::persist::CpuJson::default(); 8], + gpu: crate::persist::GpuJson::default(), + battery: crate::persist::BatteryJson::default(), + provider: None, + }, + ); let savefile = crate::persist::FileJson { version: 0, name: crate::consts::DEFAULT_SETTINGS_NAME.to_owned(), variants: mini_variants, }; - let output_file = std::fs::File::create(format!("../{}", crate::consts::DEFAULT_SETTINGS_FILE)).unwrap(); - ron::ser::to_writer_pretty(output_file, &savefile, crate::utility::ron_pretty_config()).unwrap(); + let output_file = + std::fs::File::create(format!("../{}", crate::consts::DEFAULT_SETTINGS_FILE)).unwrap(); + ron::ser::to_writer_pretty(output_file, &savefile, crate::utility::ron_pretty_config()) + .unwrap(); } } - diff --git a/src/index.tsx b/src/index.tsx index 1c8bef7..e3b8e95 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -16,6 +16,8 @@ import { Dropdown, SingleDropdownOption, Navigation, + Focusable, + Spinner, //NotchLabel //gamepadDialogClasses, //joinClassNames, @@ -89,6 +91,7 @@ var startHook: any = null; var endHook: any = null; var userHook: any = null; var usdplReady = false; +var isVariantLoading = false; var tryNotifyProfileChange = function() {}; @@ -343,6 +346,12 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { data: elem, label: {elem.name}, };}); + console.log("variant options", variantOptions); + console.log("current variant", get_value(CURRENT_VARIANT_GEN)); + console.log("variant selected", variantOptions.find((val: SingleDropdownOption, _index, _arr) => { + backend.log(backend.LogLevel.Debug, "POWERTOOLS: looking for variant data.id " + (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).id.toString()); + return (val.data as backend.VariantInfo).id == (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).id; + })); return ( @@ -383,83 +392,102 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { - { - backend.log(backend.LogLevel.Debug, "POWERTOOLS: looking for data " + (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).toString()); + backend.log(backend.LogLevel.Debug, "POWERTOOLS: looking for variant data.id " + (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).id.toString()); return (val.data as backend.VariantInfo).id == (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).id; })} - strDefaultLabel={(get_value(VARIANTS_GEN) as backend.VariantInfo[])[0].name} + strDefaultLabel={(get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo | undefined)?.name?? (get_value(VARIANTS_GEN) as backend.VariantInfo[])[0].name} onChange={(elem: SingleDropdownOption) => { - let data = elem.data as backend.VariantInfo; - backend.log(backend.LogLevel.Debug, "Profile variant dropdown selected " + elem.data.toString()); - backend.loadGeneralSettingsVariant(data.id, data.name); - set_value(CURRENT_VARIANT_GEN, elem.data as backend.VariantInfo); - reloadGUI("ProfileVariantGovernor"); + if (elem.data.id != (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).id) { + isVariantLoading = true; + let data = elem.data as backend.VariantInfo; + backend.log(backend.LogLevel.Debug, "Profile variant dropdown selected " + elem.data.id.toString()); + //set_value(CURRENT_VARIANT_GEN, elem.data as backend.VariantInfo); + backend.resolve( + backend.loadGeneralSettingsVariant(data.id, data.name), + (ok: boolean) => { + backend.log(backend.LogLevel.Debug, "Loaded settings variant ok? " + ok); + reload(); + isVariantLoading = false; + backend.resolve(backend.waitForComplete(), (_) => { + backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to newly-selected settings variant"); + tryNotifyProfileChange(); + //reloadGUI("ProfileVariantSelected"); + }); + } + ); + } }} - /> + />)} + {(isVariantLoading && )} - - { - backend.log(backend.LogLevel.Debug, "Creating new PowerTools settings variant"); - backend.resolve( - backend.loadGeneralSettingsVariant("please give me a new ID k thx bye" /* anything that cannot be parsed as a u64 will be set to u64::MAX, which will cause the back-end to auto-generate an ID */, undefined), - (ok: boolean) => { - backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok); - reload(); - backend.resolve(backend.waitForComplete(), (_) => { - backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new settings variant"); - tryNotifyProfileChange(); - }); + + + { + backend.log(backend.LogLevel.Debug, "Creating new PowerTools settings variant"); + backend.resolve( + backend.loadGeneralSettingsVariant("please give me a new ID k thx bye" /* anything that cannot be parsed as a u64 will be set to u64::MAX, which will cause the back-end to auto-generate an ID */, undefined), + (ok: boolean) => { + backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok); + reload(); + backend.resolve(backend.waitForComplete(), (_) => { + backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new settings variant"); + tryNotifyProfileChange(); + }); + } + ); + }} + > + + + { + const steamId = get_value(INTERNAL_STEAM_ID); + const steamName = get_value(INTERNAL_STEAM_USERNAME); + if (steamId && steamName) { + backend.storeUpload(steamId, steamName); + } else { + backend.log(backend.LogLevel.Warn, "Cannot upload with null steamID (is null: " + !steamId + ") and/or username (is null: " + !steamName + ")"); } - ); - }} - > - - - { - const steamId = get_value(INTERNAL_STEAM_ID); - const steamName = get_value(INTERNAL_STEAM_USERNAME); - if (steamId && steamName) { - backend.storeUpload(steamId, steamName); - } else { - backend.log(backend.LogLevel.Warn, "Cannot upload with null steamID (is null: " + !steamId + ") and/or username (is null: " + !steamName + ")"); - } - }} - > - - - { - Navigation.Navigate(STORE_RESULTS_URI); - Navigation.CloseSideMenus(); - }} - > - - + }} + > + + + { + Navigation.Navigate(STORE_RESULTS_URI); + Navigation.CloseSideMenus(); + }} + > + + + From 32a36f86275759632f64c8415ef80e1de8ea5f2e Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Thu, 1 Feb 2024 19:39:29 -0500 Subject: [PATCH 34/56] Add general quirk support and specific quirk for SD mem clock bug --- backend/limits_core/src/json_v2/base.rs | 13 +++-- .../limits_core/src/json_v2/battery_limit.rs | 11 ++-- backend/limits_core/src/json_v2/cpu_limit.rs | 13 +++-- backend/limits_core/src/json_v2/gpu_limit.rs | 26 ++++++---- backend/limits_core/src/json_v2/limits.rs | 6 +++ backend/limits_core/src/json_v2/mod.rs | 2 +- backend/src/api/web.rs | 33 ++++++++++-- backend/src/settings/detect/limits_worker.rs | 1 + backend/src/settings/steam_deck/gpu.rs | 52 +++++++++++++++---- src/index.tsx | 4 +- 10 files changed, 120 insertions(+), 41 deletions(-) diff --git a/backend/limits_core/src/json_v2/base.rs b/backend/limits_core/src/json_v2/base.rs index 7733759..e066c71 100644 --- a/backend/limits_core/src/json_v2/base.rs +++ b/backend/limits_core/src/json_v2/base.rs @@ -8,6 +8,8 @@ pub struct Base { pub configs: Vec, /// Server messages pub messages: Vec, + /// Base URL for the config store + pub store: String, /// URL from which to grab the next update pub refresh: Option, } @@ -110,7 +112,7 @@ impl Default for Base { ..Default::default() }; 4], global_governors: true, - experiments: false, + ..Default::default() } }, gpu: super::GpuLimit { @@ -154,7 +156,7 @@ impl Default for Base { ..Default::default() }; 12], // 6 cores with SMTx2 global_governors: true, - experiments: false, + ..Default::default() } }, gpu: super::GpuLimit { @@ -198,7 +200,7 @@ impl Default for Base { ..Default::default() }; 16], // 8 cores with SMTx2 global_governors: true, - experiments: false, + ..Default::default() } }, gpu: super::GpuLimit { @@ -242,7 +244,7 @@ impl Default for Base { ..Default::default() }; 16], // 8 cores with SMTx2 global_governors: true, - experiments: false, + ..Default::default() } }, gpu: super::GpuLimit { @@ -286,7 +288,7 @@ impl Default for Base { ..Default::default() }; 16], // 8 cores with SMTx2 global_governors: true, - experiments: false, + ..Default::default() } }, gpu: super::GpuLimit { @@ -341,6 +343,7 @@ impl Default for Base { url: Some("https://git.ngni.us/NG-SD-Plugins/PowerTools/wiki".to_owned()), } ], + store: "https://powertools.ngni.us".to_owned(), refresh: Some("http://limits.ngni.us:45000/powertools/v2".to_owned()) } } diff --git a/backend/limits_core/src/json_v2/battery_limit.rs b/backend/limits_core/src/json_v2/battery_limit.rs index 0c3de9d..3506d4b 100644 --- a/backend/limits_core/src/json_v2/battery_limit.rs +++ b/backend/limits_core/src/json_v2/battery_limit.rs @@ -19,7 +19,7 @@ pub struct GenericBatteryLimit { pub charge_modes: Vec, pub charge_limit: Option>, // battery charge % pub extra_readouts: bool, - pub experiments: bool, + pub extras: super::LimitExtras, } impl GenericBatteryLimit { @@ -47,7 +47,7 @@ impl GenericBatteryLimit { max: Some(90.0), }), extra_readouts: false, - experiments: false, + extras: Default::default(), } } @@ -67,7 +67,10 @@ impl GenericBatteryLimit { max: Some(99.0), }), extra_readouts: true, - experiments: true, + extras: super::LimitExtras { + experiments: true, + quirks: vec!["".to_owned()].into_iter().collect(), + }, } } @@ -91,6 +94,6 @@ impl GenericBatteryLimit { } } self.extra_readouts = limit_override.extra_readouts; - self.experiments = limit_override.experiments; + self.extras = limit_override.extras; } } diff --git a/backend/limits_core/src/json_v2/cpu_limit.rs b/backend/limits_core/src/json_v2/cpu_limit.rs index 58af870..24b7fde 100644 --- a/backend/limits_core/src/json_v2/cpu_limit.rs +++ b/backend/limits_core/src/json_v2/cpu_limit.rs @@ -19,7 +19,7 @@ pub enum CpuLimitType { pub struct GenericCpusLimit { pub cpus: Vec, pub global_governors: bool, - pub experiments: bool, + pub extras: super::LimitExtras, } impl GenericCpusLimit { @@ -29,14 +29,17 @@ impl GenericCpusLimit { Self { cpus: [(); 8].iter().enumerate().map(|(i, _)| GenericCpuLimit::default_for(&t, i)).collect(), global_governors: true, - experiments: false, + extras: Default::default(), } }, CpuLimitType::DevMode => { Self { cpus: [(); 11].iter().enumerate().map(|(i, _)| GenericCpuLimit::default_for(&t, i)).collect(), global_governors: true, - experiments: true, + extras: super::LimitExtras { + experiments: true, + quirks: vec!["".to_owned()].into_iter().collect(), + }, } }, t => { @@ -48,7 +51,7 @@ impl GenericCpusLimit { Self { cpus, global_governors: true, - experiments: false, + extras: Default::default(), } } } @@ -76,7 +79,7 @@ impl GenericCpusLimit { .for_each(|(cpu, limit_override)| cpu.apply_override(limit_override)); } self.global_governors = limit_override.global_governors; - self.experiments = limit_override.experiments; + self.extras = limit_override.extras; } } diff --git a/backend/limits_core/src/json_v2/gpu_limit.rs b/backend/limits_core/src/json_v2/gpu_limit.rs index f7d1dbe..f5a326a 100644 --- a/backend/limits_core/src/json_v2/gpu_limit.rs +++ b/backend/limits_core/src/json_v2/gpu_limit.rs @@ -32,7 +32,7 @@ pub struct GenericGpuLimit { pub memory_clock: Option>, pub memory_clock_step: Option, pub skip_resume_reclock: bool, - pub experiments: bool, + pub extras: super::LimitExtras, } impl GenericGpuLimit { @@ -72,16 +72,21 @@ impl GenericGpuLimit { max: Some(1600), }), clock_step: Some(100), - // Disabled for now since LCD version is a bit broken on sysfs right now - /*memory_clock: Some(RangeLimit { + // LCD version is a bit broken on sysfs, but it's ok + memory_clock: Some(RangeLimit { min: Some(400), max: Some(800), }), - memory_clock_step: Some(400),*/ - memory_clock: None, - memory_clock_step: None, + memory_clock_step: Some(400), skip_resume_reclock: false, - experiments: false, + extras: super::LimitExtras { + experiments: false, + quirks: vec![ + "pp_dpm_fclk-reversed".to_owned(), + "pp_dpm_fclk-not-updated-on-LCD".to_owned(), + //"pp_dpm_fclk-static".to_owned(), + ].into_iter().collect(), + } } } @@ -124,7 +129,10 @@ impl GenericGpuLimit { }), memory_clock_step: Some(100), skip_resume_reclock: false, - experiments: true, + extras: super::LimitExtras { + experiments: true, + quirks: vec!["dev".to_owned()].into_iter().collect(), + }, } } @@ -190,6 +198,6 @@ impl GenericGpuLimit { self.clock_step = Some(val); } self.skip_resume_reclock = limit_override.skip_resume_reclock; - self.experiments = limit_override.experiments; + self.extras = limit_override.extras; } } diff --git a/backend/limits_core/src/json_v2/limits.rs b/backend/limits_core/src/json_v2/limits.rs index b983603..516d31c 100644 --- a/backend/limits_core/src/json_v2/limits.rs +++ b/backend/limits_core/src/json_v2/limits.rs @@ -26,3 +26,9 @@ pub struct Limit { pub type CpuLimit = Limit; pub type GpuLimit = Limit; pub type BatteryLimit = Limit; + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct LimitExtras { + pub experiments: bool, + pub quirks: std::collections::HashSet, +} diff --git a/backend/limits_core/src/json_v2/mod.rs b/backend/limits_core/src/json_v2/mod.rs index c5bfeb6..d863d7f 100644 --- a/backend/limits_core/src/json_v2/mod.rs +++ b/backend/limits_core/src/json_v2/mod.rs @@ -16,6 +16,6 @@ pub use cpu_limit::{CpuLimitType, GenericCpusLimit, GenericCpuLimit}; pub use devel_message::DeveloperMessage; pub use gpu_limit::{GpuLimitType, GenericGpuLimit}; pub use config::Config; -pub use limits::{Limits, Limit, CpuLimit, GpuLimit, BatteryLimit}; +pub use limits::{Limits, Limit, CpuLimit, GpuLimit, BatteryLimit, LimitExtras}; pub use range::RangeLimit; pub use target::Target; diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index 385ec92..1001e49 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -1,17 +1,40 @@ use std::sync::mpsc::{self, Sender}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use usdpl_back::core::serdes::Primitive; use usdpl_back::AsyncCallable; use super::handler::{ApiMessage, GeneralMessage}; -const BASE_URL: &'static str = "http://powertools.ngni.us"; +const BASE_URL_FALLBACK: &'static str = "https://powertools.ngni.us"; +static BASE_URL: RwLock> = RwLock::new(None); + +pub fn set_base_url(base_url: String) { + *BASE_URL.write().expect("Failed to acquire write lock for store base url") = Some(base_url); +} + +fn get_base_url() -> String { + BASE_URL.read().expect("Failed to acquire read lock for store base url") + .clone() + .unwrap_or_else(|| BASE_URL_FALLBACK.to_owned()) +} + +fn url_search_by_app_id(steam_app_id: u32) -> String { + format!("{}/api/setting/by_app_id/{}", get_base_url(), steam_app_id) +} + +fn url_download_config_by_id(id: u128) -> String { + format!("{}/api/setting/by_id/{}", get_base_url(), id) +} + +fn url_upload_config() -> String { + format!("{}/api/setting", get_base_url()) +} /// Get search results web method pub fn search_by_app_id() -> impl AsyncCallable { let getter = move || { move |steam_app_id: u32| { - let req_url = format!("{}/api/setting/by_app_id/{}", BASE_URL, steam_app_id); + let req_url = url_search_by_app_id(steam_app_id); match ureq::get(&req_url).call() { Ok(response) => { let json_res: std::io::Result> = @@ -110,7 +133,7 @@ fn web_config_to_settings_json( } fn download_config(id: u128) -> std::io::Result { - let req_url = format!("{}/api/setting/by_id/{}", BASE_URL, id); + let req_url = url_download_config_by_id(id); let response = ureq::get(&req_url).call().map_err(|e| { log::warn!("GET to {} failed: {}", req_url, e); std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e) @@ -207,7 +230,7 @@ fn settings_to_web_config( } fn upload_config(config: community_settings_core::v1::Metadata) -> std::io::Result<()> { - let req_url = format!("{}/api/setting", BASE_URL); + let req_url = url_upload_config(); ureq::post(&req_url) .send_json(&config) .map_err(|e| { diff --git a/backend/src/settings/detect/limits_worker.rs b/backend/src/settings/detect/limits_worker.rs index 3665b11..4bdad39 100644 --- a/backend/src/settings/detect/limits_worker.rs +++ b/backend/src/settings/detect/limits_worker.rs @@ -32,6 +32,7 @@ pub fn spawn() -> JoinHandle<()> { save_base(&base, &limits_path); base }; + crate::api::web::set_base_url(base.store); if let Some(refresh) = &base.refresh { // try to retrieve newer version match ureq::get(refresh).call() { diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index 8ca3faa..beb0a47 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -192,18 +192,50 @@ impl Gpu { fn build_memory_clock_payload(&self, clock: u64) -> String { let max_val = self.quantize_memory_clock(clock); - match max_val { - 0 => "0\n".to_owned(), - max_val => { - use std::fmt::Write; - let mut payload = String::from("0"); - for i in 1..max_val { - write!(payload, " {}", i) + let is_oled = matches!(self.variant, super::Model::OLED); + let is_lcd = matches!(self.variant, super::Model::LCD); + let is_lock_feature_enabled = self.limits.extras.quirks.contains("pp_dpm_fclk-static"); + + if (is_oled && self.limits.extras.quirks.contains("pp_dpm_fclk-reversed-on-OLED")) + || (is_lcd && self.limits.extras.quirks.contains("pp_dpm_fclk-reversed-on-LCD")) + || self.limits.extras.quirks.contains("pp_dpm_fclk-reversed") { + let options_count = self + .sysfs_card + .read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) + .map(|b| parse_pp_dpm_fclk(&String::from_utf8_lossy(&b)).len()) + .unwrap_or_else(|_| if is_oled { 4 } else { 2 }); + let modifier = (options_count - 1) as u64; + if is_lock_feature_enabled { + format!("{}\n", modifier - max_val) + } else { + if max_val == 0 as u64 { + format!("{}\n", modifier) + } else { + use std::fmt::Write; + let mut payload = format!("{}", modifier - max_val); + for i in (0..max_val).rev(/* rev() isn't necessary but it creates a nicer (ascending) order */) { + write!(payload, " {}", modifier - i) + .expect("Failed to write to memory payload (should be infallible!?)"); + } + write!(payload, "\n") .expect("Failed to write to memory payload (should be infallible!?)"); + payload + } + } + } else { + match max_val { + 0 => "0\n".to_owned(), + max_val => { + use std::fmt::Write; + let mut payload = String::from("0"); + for i in 1..max_val { + write!(payload, " {}", i) + .expect("Failed to write to memory payload (should be infallible!?)"); + } + write!(payload, " {}\n", max_val) + .expect("Failed to write to memory payload (should be infallible!?)"); + payload } - write!(payload, " {}\n", max_val) - .expect("Failed to write to memory payload (should be infallible!?)"); - payload } } } diff --git a/src/index.tsx b/src/index.tsx index e3b8e95..8fb9f4d 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,7 +23,7 @@ import { //joinClassNames, } from "decky-frontend-lib"; import { VFC, useState } from "react"; -import { GiDrill, GiTimeBomb, GiTimeTrap, GiDynamite } from "react-icons/gi"; +import { GiDrill, GiFireExtinguisher, GiFireBomb, GiMineExplosion } from "react-icons/gi"; import { HiRefresh, HiTrash, HiPlus, HiUpload } from "react-icons/hi"; import { TbWorldPlus } from "react-icons/tb"; @@ -532,7 +532,7 @@ export default definePlugin((serverApi: ServerAPI) => { let ico = ; let now = new Date(); if (now.getDate() == 1 && now.getMonth() == 3) { - ico = ; + ico = ; } //registerCallbacks(false); serverApi.routerHook.addRoute(STORE_RESULTS_URI, StoreResultsPage); From 23f5f607cc3c948648e44c926939f3e41651d440 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Thu, 1 Feb 2024 21:02:59 -0500 Subject: [PATCH 35/56] Add minimal CLI debug utilities --- backend/Cargo.lock | 113 ++++++++++++++++++++++++++++++++++++ backend/Cargo.toml | 2 + backend/src/cli/args.rs | 42 ++++++++++++++ backend/src/cli/clean.rs | 21 +++++++ backend/src/cli/dump_sys.rs | 66 +++++++++++++++++++++ backend/src/cli/mod.rs | 12 ++++ backend/src/main.rs | 19 +++++- 7 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 backend/src/cli/args.rs create mode 100644 backend/src/cli/clean.rs create mode 100644 backend/src/cli/dump_sys.rs create mode 100644 backend/src/cli/mod.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 64ff1dd..a580744 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -92,6 +92,54 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "async-recursion" version = "1.0.5" @@ -280,6 +328,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + [[package]] name = "cmake" version = "0.1.50" @@ -289,6 +377,12 @@ dependencies = [ "cc", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "community_settings_core" version = "0.1.0" @@ -624,6 +718,12 @@ dependencies = [ "http", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.3" @@ -1072,6 +1172,7 @@ name = "powertools" version = "1.5.0-ng1" dependencies = [ "async-trait", + "clap", "community_settings_core", "libc", "libryzenadj", @@ -1364,6 +1465,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" @@ -1691,6 +1798,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "version_check" version = "0.9.4" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 8a8e2b0..302a2f9 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -37,6 +37,8 @@ libryzenadj = { version = "0.13" } # ureq's tls feature does not like musl targets ureq = { version = "2", features = ["json", "gzip", "brotli", "charset"], default-features = false, optional = true } +clap = { version = "4.4", features = [ "derive" ] } + [features] default = ["online", "decky"] decky = ["usdpl-back/decky"] diff --git a/backend/src/cli/args.rs b/backend/src/cli/args.rs new file mode 100644 index 0000000..9edebad --- /dev/null +++ b/backend/src/cli/args.rs @@ -0,0 +1,42 @@ +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + /// TCP port for front-end client connection + #[arg(long)] + pub port: Option, + + /// Override log file location + #[arg(long)] + pub log: Option, + + /// Force verbose logging + #[arg(short, long)] + pub verbose: bool, + + /// Specail operation to perform + #[command(subcommand)] + pub op: Option, +} + +impl Args { + pub fn load() -> Self { + Self::parse() + } + + pub fn is_default(&self) -> bool { + self.port.is_none() + && self.log.is_none() + && !self.verbose + && self.op.is_none() + } +} + +#[derive(Subcommand, Debug)] +pub enum Operation { + /// Dump useful system information for adding new device support + DumpSys, + /// Remove all files created by PowerTools, not including $HOME/homebrew/plugins/PowerTools/ + Clean, +} diff --git a/backend/src/cli/clean.rs b/backend/src/cli/clean.rs new file mode 100644 index 0000000..7afe215 --- /dev/null +++ b/backend/src/cli/clean.rs @@ -0,0 +1,21 @@ +pub fn clean_up() -> Result<(), ()> { + let dirs = vec![ + crate::utility::settings_dir_old(), + crate::utility::settings_dir(), + ]; + + if let Err(e) = clean_up_io(dirs.iter()) { + log::error!("Error removing directories: {}", e); + Err(()) + } else { + Ok(()) + } +} + +fn clean_up_io(directories: impl Iterator>) -> std::io::Result<()> { + let results = directories.map(|dir| std::fs::remove_dir_all(dir)); + for res in results { + res?; + } + Ok(()) +} diff --git a/backend/src/cli/dump_sys.rs b/backend/src/cli/dump_sys.rs new file mode 100644 index 0000000..03e4c0c --- /dev/null +++ b/backend/src/cli/dump_sys.rs @@ -0,0 +1,66 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::thread::{self, JoinHandle}; +use std::sync::mpsc::{channel, Sender}; +use std::io::Write; + +pub fn dump_sys_info() -> Result<(), ()> { + let (tx, rx) = channel(); + let mut join_handles = Vec::new(); + let useful_files = vec![ + PathBuf::from("/proc/ioports"), + PathBuf::from("/proc/cpuinfo"), + PathBuf::from("/etc/os-release"), + ]; + for file in useful_files { + join_handles.push(read_file(file, tx.clone())); + } + + let useful_commands = vec![ + "dmidecode", + ]; + for cmd in useful_commands.into_iter() { + join_handles.push(execute_command(cmd, tx.clone())); + } + + for join_handle in join_handles.into_iter() { + if let Err(e) = join_handle.join() { + log::error!("Thread failed to complete: {:?}", e); + } + } + + let mut dump_file = std::fs::File::create("powertools_sys_dump.txt").expect("Failed to create dump file"); + for response in rx.into_iter() { + dump_file.write( + &format!("{} v{} ###### {} ######\n{}\n", + crate::consts::PACKAGE_NAME, + crate::consts::PACKAGE_VERSION, + response.0, + response.1.unwrap_or("[None]".to_owned()) + ).into_bytes() + ).expect("Failed to write to dump file"); + } + Ok(()) +} + +fn read_file(file: impl AsRef + Send + 'static, tx: Sender<(String, Option)>) -> JoinHandle<()> { + thread::spawn(move || { + let file = file.as_ref(); + tx.send( + (file.display().to_string(), + std::fs::read_to_string(file).ok()) + ).expect("Failed to send file contents"); + }) +} + +fn execute_command(command: &'static str, tx: Sender<(String, Option)>) -> JoinHandle<()> { + thread::spawn(move || { + tx.send( + (command.to_owned(), Command::new(command) + .output() + .map(|out| String::from_utf8_lossy(&out.stdout).into_owned()) + .ok() + )).expect("Failed to send command output"); + } + ) +} diff --git a/backend/src/cli/mod.rs b/backend/src/cli/mod.rs new file mode 100644 index 0000000..720332d --- /dev/null +++ b/backend/src/cli/mod.rs @@ -0,0 +1,12 @@ +mod args; +mod clean; +mod dump_sys; + +pub use args::{Args, Operation}; + +pub fn do_op(op: &Operation) -> Result<(), ()> { + match op { + Operation::DumpSys => dump_sys::dump_sys_info(), + Operation::Clean => clean::clean_up(), + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 0a245ca..f9116c7 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,4 +1,5 @@ mod api; +mod cli; mod persist; mod settings; mod state; @@ -19,13 +20,18 @@ use usdpl_back::core::serdes::Primitive; use usdpl_back::Instance; fn main() -> Result<(), ()> { + let args = cli::Args::load(); #[cfg(debug_assertions)] let log_filepath = usdpl_back::api::dirs::home() .unwrap_or_else(|| "/tmp/".into()) .join(PACKAGE_NAME.to_owned() + ".log"); #[cfg(not(debug_assertions))] let log_filepath = std::path::Path::new("/tmp").join(format!("{}.log", PACKAGE_NAME)); + let log_filepath = args.log.clone().unwrap_or(log_filepath); println!("Logging to: {:?}", log_filepath); + if !args.is_default() { + println!("CLI arguments, as parsed: {:?}", &args); + } #[cfg(debug_assertions)] let old_log_filepath = usdpl_back::api::dirs::home() .unwrap_or_else(|| "/tmp/".into()) @@ -44,7 +50,7 @@ fn main() -> Result<(), ()> { }, #[cfg(not(debug_assertions))] { - LevelFilter::Info + if args.verbose { LevelFilter::Debug } else { LevelFilter::Info } }, Default::default(), std::fs::File::create(&log_filepath).expect("Failed to create log file"), @@ -52,6 +58,15 @@ fn main() -> Result<(), ()> { ) .unwrap(); log::debug!("Logging to: {:?}.", log_filepath); + log::info!("CLI arguments, as parsed: {:?}", &args); + + // sepcial operation start-up + if let Some(op) = &args.op { + return cli::do_op(op); + // do not continue with regular startup + } + + // regular start-up log::info!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION); println!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION); log::info!( @@ -125,7 +140,7 @@ fn main() -> Result<(), ()> { let (message_getter, message_dismisser) = api::message::MessageHandler::new().to_callables(); - let instance = Instance::new(PORT) + let instance = Instance::new(args.port.unwrap_or(PORT)) .register("V_INFO", |_: Vec| { #[cfg(debug_assertions)] { From d714b98dee00faed444739209bb6b8b3aa7a0aee Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Fri, 2 Feb 2024 20:26:43 -0500 Subject: [PATCH 36/56] cargo fmt --- backend/Cargo.toml | 4 +- backend/src/api/general.rs | 9 ++-- backend/src/api/web.rs | 8 +++- backend/src/cli/args.rs | 5 +-- backend/src/cli/clean.rs | 4 +- backend/src/cli/dump_sys.rs | 58 +++++++++++++++----------- backend/src/main.rs | 6 ++- backend/src/persist/file.rs | 31 ++++++++++---- backend/src/settings/general.rs | 33 ++++++++++++--- backend/src/settings/steam_deck/gpu.rs | 22 +++++++--- 10 files changed, 123 insertions(+), 57 deletions(-) diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 302a2f9..d81c417 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -53,13 +53,13 @@ debug = false strip = true lto = true codegen-units = 1 +opt-level = 3 [profile.docker] inherits = "release" debug = false strip = true lto = "thin" -codegen-units = 16 -opt-level = 2 +codegen-units = 8 debug-assertions = false overflow-checks = false diff --git a/backend/src/api/general.rs b/backend/src/api/general.rs index feda6af..806347a 100644 --- a/backend/src/api/general.rs +++ b/backend/src/api/general.rs @@ -111,14 +111,17 @@ pub fn load_variant( ) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety let setter = move |variant: u64, variant_name: Option| { - log::debug!("load_variant(variant: {}, variant_name: {:?})", variant, variant_name); + log::debug!( + "load_variant(variant: {}, variant_name: {:?})", + variant, + variant_name + ); sender .lock() .unwrap() .send(ApiMessage::LoadVariant( variant, - variant_name - .unwrap_or_else(|| "".to_owned()), + variant_name.unwrap_or_else(|| "".to_owned()), )) .expect("load_variant send failed") }; diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index 1001e49..965be44 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -9,11 +9,15 @@ const BASE_URL_FALLBACK: &'static str = "https://powertools.ngni.us"; static BASE_URL: RwLock> = RwLock::new(None); pub fn set_base_url(base_url: String) { - *BASE_URL.write().expect("Failed to acquire write lock for store base url") = Some(base_url); + *BASE_URL + .write() + .expect("Failed to acquire write lock for store base url") = Some(base_url); } fn get_base_url() -> String { - BASE_URL.read().expect("Failed to acquire read lock for store base url") + BASE_URL + .read() + .expect("Failed to acquire read lock for store base url") .clone() .unwrap_or_else(|| BASE_URL_FALLBACK.to_owned()) } diff --git a/backend/src/cli/args.rs b/backend/src/cli/args.rs index 9edebad..3c10a59 100644 --- a/backend/src/cli/args.rs +++ b/backend/src/cli/args.rs @@ -26,10 +26,7 @@ impl Args { } pub fn is_default(&self) -> bool { - self.port.is_none() - && self.log.is_none() - && !self.verbose - && self.op.is_none() + self.port.is_none() && self.log.is_none() && !self.verbose && self.op.is_none() } } diff --git a/backend/src/cli/clean.rs b/backend/src/cli/clean.rs index 7afe215..590e61a 100644 --- a/backend/src/cli/clean.rs +++ b/backend/src/cli/clean.rs @@ -12,7 +12,9 @@ pub fn clean_up() -> Result<(), ()> { } } -fn clean_up_io(directories: impl Iterator>) -> std::io::Result<()> { +fn clean_up_io( + directories: impl Iterator>, +) -> std::io::Result<()> { let results = directories.map(|dir| std::fs::remove_dir_all(dir)); for res in results { res?; diff --git a/backend/src/cli/dump_sys.rs b/backend/src/cli/dump_sys.rs index 03e4c0c..c626581 100644 --- a/backend/src/cli/dump_sys.rs +++ b/backend/src/cli/dump_sys.rs @@ -1,8 +1,8 @@ +use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Command; -use std::thread::{self, JoinHandle}; use std::sync::mpsc::{channel, Sender}; -use std::io::Write; +use std::thread::{self, JoinHandle}; pub fn dump_sys_info() -> Result<(), ()> { let (tx, rx) = channel(); @@ -16,9 +16,7 @@ pub fn dump_sys_info() -> Result<(), ()> { join_handles.push(read_file(file, tx.clone())); } - let useful_commands = vec![ - "dmidecode", - ]; + let useful_commands = vec!["dmidecode"]; for cmd in useful_commands.into_iter() { join_handles.push(execute_command(cmd, tx.clone())); } @@ -29,38 +27,48 @@ pub fn dump_sys_info() -> Result<(), ()> { } } - let mut dump_file = std::fs::File::create("powertools_sys_dump.txt").expect("Failed to create dump file"); + let mut dump_file = + std::fs::File::create("powertools_sys_dump.txt").expect("Failed to create dump file"); for response in rx.into_iter() { - dump_file.write( - &format!("{} v{} ###### {} ######\n{}\n", - crate::consts::PACKAGE_NAME, - crate::consts::PACKAGE_VERSION, - response.0, - response.1.unwrap_or("[None]".to_owned()) - ).into_bytes() - ).expect("Failed to write to dump file"); + dump_file + .write( + &format!( + "{} v{} ###### {} ######\n{}\n", + crate::consts::PACKAGE_NAME, + crate::consts::PACKAGE_VERSION, + response.0, + response.1.unwrap_or("[None]".to_owned()) + ) + .into_bytes(), + ) + .expect("Failed to write to dump file"); } Ok(()) } -fn read_file(file: impl AsRef + Send + 'static, tx: Sender<(String, Option)>) -> JoinHandle<()> { +fn read_file( + file: impl AsRef + Send + 'static, + tx: Sender<(String, Option)>, +) -> JoinHandle<()> { thread::spawn(move || { let file = file.as_ref(); - tx.send( - (file.display().to_string(), - std::fs::read_to_string(file).ok()) - ).expect("Failed to send file contents"); + tx.send(( + file.display().to_string(), + std::fs::read_to_string(file).ok(), + )) + .expect("Failed to send file contents"); }) } fn execute_command(command: &'static str, tx: Sender<(String, Option)>) -> JoinHandle<()> { thread::spawn(move || { - tx.send( - (command.to_owned(), Command::new(command) + tx.send(( + command.to_owned(), + Command::new(command) .output() .map(|out| String::from_utf8_lossy(&out.stdout).into_owned()) - .ok() - )).expect("Failed to send command output"); - } - ) + .ok(), + )) + .expect("Failed to send command output"); + }) } diff --git a/backend/src/main.rs b/backend/src/main.rs index f9116c7..b15fefb 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -50,7 +50,11 @@ fn main() -> Result<(), ()> { }, #[cfg(not(debug_assertions))] { - if args.verbose { LevelFilter::Debug } else { LevelFilter::Info } + if args.verbose { + LevelFilter::Debug + } else { + LevelFilter::Info + } }, Default::default(), std::fs::File::create(&log_filepath).expect("Failed to create log file"), diff --git a/backend/src/persist/file.rs b/backend/src/persist/file.rs index 0999517..9720b1e 100644 --- a/backend/src/persist/file.rs +++ b/backend/src/persist/file.rs @@ -71,7 +71,13 @@ impl FileJson { if setting.name.is_empty() { setting.name = format!("Variant {}", setting.variant); } - log::debug!("Inserting setting variant `{}` ({}) for app `{}` ({})", setting.name, setting.variant, file.name, app_id); + log::debug!( + "Inserting setting variant `{}` ({}) for app `{}` ({})", + setting.name, + setting.variant, + file.name, + app_id + ); file.variants.insert(setting.variant, setting.clone()); (file, setting) } else { @@ -83,15 +89,24 @@ impl FileJson { if setting.name.is_empty() { setting.name = format!("Variant {}", setting.variant); } - log::debug!("Creating new setting variant `{}` ({}) for app `{}` ({})", setting.name, setting.variant, app_name, app_id); + log::debug!( + "Creating new setting variant `{}` ({}) for app `{}` ({})", + setting.name, + setting.variant, + app_name, + app_id + ); let mut setting_variants = HashMap::with_capacity(1); setting_variants.insert(setting.variant, setting.clone()); - (Self { - version: 0, - app_id: app_id, - name: app_name, - variants: setting_variants, - }, setting) + ( + Self { + version: 0, + app_id: app_id, + name: app_name, + variants: setting_variants, + }, + setting, + ) }; file.save(path)?; diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index b873b0a..2a75eae 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -135,7 +135,8 @@ impl TGeneral for General { setting: SettingVariant::General, }) .map(|file| { - file.0.variants + file.0 + .variants .into_iter() .map(|(id, conf)| crate::api::VariantInfo { id: id.to_string(), @@ -147,7 +148,11 @@ impl TGeneral for General { } fn get_variant_info(&self) -> crate::api::VariantInfo { - log::debug!("Current variant `{}` ({})", self.variant_name, self.variant_id); + log::debug!( + "Current variant `{}` ({})", + self.variant_name, + self.variant_id + ); crate::api::VariantInfo { id: self.variant_id.to_string(), name: self.variant_name.clone(), @@ -260,7 +265,10 @@ impl Settings { let mut valid_ids: Vec<&u64> = settings_file.variants.keys().collect(); valid_ids.sort(); if let Some(id) = valid_ids.get(0) { - Ok(settings_file.variants.get(id).expect("variant id key magically disappeared")) + Ok(settings_file + .variants + .get(id) + .expect("variant id key magically disappeared")) } else { Err(SettingError { msg: format!( @@ -293,7 +301,11 @@ impl Settings { let json_path = crate::utility::settings_dir().join(&filename); if json_path.exists() { if variant == u64::MAX { - log::debug!("Creating new variant `{}` in existing settings file {}", variant_name, json_path.display()); + log::debug!( + "Creating new variant `{}` in existing settings file {}", + variant_name, + json_path.display() + ); self.create_and_load_variant(&json_path, app_id, variant_name)?; } else { let file_json = FileJson::open(&json_path).map_err(|e| SettingError { @@ -336,7 +348,11 @@ impl Settings { } *self.general.persistent() = false; if variant == u64::MAX { - log::debug!("Creating new variant `{}` in new settings file {}", variant_name, json_path.display()); + log::debug!( + "Creating new variant `{}` in new settings file {}", + variant_name, + json_path.display() + ); self.create_and_load_variant(&json_path, app_id, variant_name)?; } } @@ -345,7 +361,12 @@ impl Settings { Ok(*self.general.persistent()) } - fn create_and_load_variant(&mut self, json_path: &PathBuf, app_id: u64, variant_name: String) -> Result<(), SettingError> { + fn create_and_load_variant( + &mut self, + json_path: &PathBuf, + app_id: u64, + variant_name: String, + ) -> Result<(), SettingError> { *self.general.persistent() = true; self.general.variant_id(u64::MAX); self.general.variant_name(variant_name.clone()); diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index beb0a47..4912713 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -196,9 +196,20 @@ impl Gpu { let is_lcd = matches!(self.variant, super::Model::LCD); let is_lock_feature_enabled = self.limits.extras.quirks.contains("pp_dpm_fclk-static"); - if (is_oled && self.limits.extras.quirks.contains("pp_dpm_fclk-reversed-on-OLED")) - || (is_lcd && self.limits.extras.quirks.contains("pp_dpm_fclk-reversed-on-LCD")) - || self.limits.extras.quirks.contains("pp_dpm_fclk-reversed") { + if (is_oled + && self + .limits + .extras + .quirks + .contains("pp_dpm_fclk-reversed-on-OLED")) + || (is_lcd + && self + .limits + .extras + .quirks + .contains("pp_dpm_fclk-reversed-on-LCD")) + || self.limits.extras.quirks.contains("pp_dpm_fclk-reversed") + { let options_count = self .sysfs_card .read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) @@ -208,12 +219,13 @@ impl Gpu { if is_lock_feature_enabled { format!("{}\n", modifier - max_val) } else { - if max_val == 0 as u64 { + if max_val == 0 as u64 { format!("{}\n", modifier) } else { use std::fmt::Write; let mut payload = format!("{}", modifier - max_val); - for i in (0..max_val).rev(/* rev() isn't necessary but it creates a nicer (ascending) order */) { + for i in (0..max_val).rev(/* rev() isn't necessary but it creates a nicer (ascending) order */) + { write!(payload, " {}", modifier - i) .expect("Failed to write to memory payload (should be infallible!?)"); } From 2ec89ee1ebb20eea5137eb4e365b4e694c87acea Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Fri, 2 Feb 2024 21:04:10 -0500 Subject: [PATCH 37/56] Cache store results --- backend/Cargo.lock | 2 + backend/Cargo.toml | 10 ++- backend/src/api/web.rs | 150 ++++++++++++++++++++++++++++++++++------- backend/src/consts.rs | 2 + backend/src/utility.rs | 1 + 5 files changed, 141 insertions(+), 24 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index a580744..6d6f640 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -304,6 +304,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.48.5", ] @@ -1172,6 +1173,7 @@ name = "powertools" version = "1.5.0-ng1" dependencies = [ "async-trait", + "chrono", "clap", "community_settings_core", "libc", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d81c417..06cdd2e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -28,12 +28,20 @@ simplelog = "0.12" # limits & driver functionality limits_core = { version = "3", path = "./limits_core" } -community_settings_core = { version = "0.1", path = "./community_settings_core" } regex = "1" + +# steam deck libs smokepatio = { version = "0.1", features = [ "std" ], path = "../../smokepatio" } libc = "0.2" + +# online settings +community_settings_core = { version = "0.1", path = "./community_settings_core" } +chrono = { version = "0.4", features = [ "serde" ] } + +# hardware enablement #libryzenadj = { version = "0.14", path = "../../libryzenadj-rs-14" } libryzenadj = { version = "0.13" } + # ureq's tls feature does not like musl targets ureq = { version = "2", features = ["json", "gzip", "brotli", "charset"], default-features = false, optional = true } diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index 965be44..c5f2d2f 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -3,11 +3,26 @@ use std::sync::{Arc, Mutex, RwLock}; use usdpl_back::core::serdes::Primitive; use usdpl_back::AsyncCallable; +use chrono::{offset::Utc, DateTime}; +use serde::{Deserialize, Serialize}; + use super::handler::{ApiMessage, GeneralMessage}; const BASE_URL_FALLBACK: &'static str = "https://powertools.ngni.us"; static BASE_URL: RwLock> = RwLock::new(None); +const MAX_CACHE_DURATION: std::time::Duration = + std::time::Duration::from_secs(60 * 60 * 24 * 7 /* 7 days */); + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CachedData { + data: T, + updated: DateTime, +} + +type StoreCache = + std::collections::HashMap>>; + pub fn set_base_url(base_url: String) { *BASE_URL .write() @@ -34,35 +49,124 @@ fn url_upload_config() -> String { format!("{}/api/setting", get_base_url()) } +fn cache_path() -> std::path::PathBuf { + crate::utility::settings_dir().join(crate::consts::WEB_SETTINGS_CACHE) +} + +fn load_cache() -> StoreCache { + let path = cache_path(); + let file = match std::fs::File::open(&path) { + Ok(f) => f, + Err(e) => { + log::warn!("Failed to open store cache {}: {}", path.display(), e); + return StoreCache::default(); + } + }; + let mut file = std::io::BufReader::new(file); + match ron::de::from_reader(&mut file) { + Ok(cache) => cache, + Err(e) => { + log::error!("Failed to parse store cache {}: {}", path.display(), e); + return StoreCache::default(); + } + } +} + +fn save_cache(cache: &StoreCache) { + let path = cache_path(); + let file = match std::fs::File::create(&path) { + Ok(f) => f, + Err(e) => { + log::warn!("Failed to create store cache {}: {}", path.display(), e); + return; + } + }; + let mut file = std::io::BufWriter::new(file); + if let Err(e) = + ron::ser::to_writer_pretty(&mut file, cache, crate::utility::ron_pretty_config()) + { + log::error!("Failed to parse store cache {}: {}", path.display(), e); + } +} + +fn get_maybe_cached(steam_app_id: u32) -> Vec { + let mut cache = load_cache(); + if let Some(cached_result) = cache.get(&steam_app_id) { + if cached_result.updated < (Utc::now() - MAX_CACHE_DURATION) { + // cache needs update + if let Ok(result) = search_by_app_id_online(steam_app_id) { + cache.insert( + steam_app_id, + CachedData { + data: result.clone(), + updated: Utc::now(), + }, + ); + save_cache(&cache); + result + } else { + // if all else fails, out of date results are better than no results + cached_result.data.to_owned() + } + } else { + // cache is ok, use it + cached_result.data.to_owned() + } + } else { + if let Ok(result) = search_by_app_id_online(steam_app_id) { + cache.insert( + steam_app_id, + CachedData { + data: result.clone(), + updated: Utc::now(), + }, + ); + save_cache(&cache); + result + } else { + Vec::with_capacity(0) + } + } +} + +fn search_by_app_id_online( + steam_app_id: u32, +) -> std::io::Result> { + let req_url = url_search_by_app_id(steam_app_id); + match ureq::get(&req_url).call() { + Ok(response) => { + let json_res: std::io::Result> = + response.into_json(); + match json_res { + Ok(search_results) => Ok(search_results), + Err(e) => { + log::error!("Cannot parse response from `{}`: {}", req_url, e); + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + } + } + } + Err(e) => { + log::warn!("Cannot get search results from `{}`: {}", req_url, e); + Err(std::io::Error::new( + std::io::ErrorKind::ConnectionAborted, + e, + )) + } + } +} + /// Get search results web method pub fn search_by_app_id() -> impl AsyncCallable { let getter = move || { move |steam_app_id: u32| { - let req_url = url_search_by_app_id(steam_app_id); - match ureq::get(&req_url).call() { - Ok(response) => { - let json_res: std::io::Result> = - response.into_json(); - match json_res { - Ok(search_results) => { - // search results may be quite large, so let's do the JSON string conversion in the background (blocking) thread - match serde_json::to_string(&search_results) { - Err(e) => log::error!( - "Cannot convert search results from `{}` to JSON: {}", - req_url, - e - ), - Ok(s) => return s, - } - } - Err(e) => { - log::error!("Cannot parse response from `{}`: {}", req_url, e) - } - } + let search_results = get_maybe_cached(steam_app_id); + match serde_json::to_string(&search_results) { + Err(e) => { + log::error!("Cannot convert search results to JSON: {}", e); + "[]".to_owned() } - Err(e) => log::warn!("Cannot get search results from `{}`: {}", req_url, e), + Ok(s) => s, } - "[]".to_owned() } }; super::async_utils::AsyncIsh { diff --git a/backend/src/consts.rs b/backend/src/consts.rs index 502f6ce..01707e0 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -10,4 +10,6 @@ pub const DEFAULT_SETTINGS_VARIANT_NAME: &str = "Primary"; pub const LIMITS_FILE: &str = "limits_cache.ron"; pub const LIMITS_OVERRIDE_FILE: &str = "limits_override.ron"; +pub const WEB_SETTINGS_CACHE: &str = "store_cache.ron"; + pub const MESSAGE_SEEN_ID_FILE: &str = "seen_message.bin"; diff --git a/backend/src/utility.rs b/backend/src/utility.rs index 2aa20f4..d9ec7b1 100644 --- a/backend/src/utility.rs +++ b/backend/src/utility.rs @@ -174,6 +174,7 @@ mod generate { ); let savefile = crate::persist::FileJson { version: 0, + app_id: 0, name: crate::consts::DEFAULT_SETTINGS_NAME.to_owned(), variants: mini_variants, }; From 9dedebb234932a81e6dd1980a7052416eff295a7 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Fri, 2 Feb 2024 21:40:35 -0500 Subject: [PATCH 38/56] Use basic DFL components for store UI --- src/store/page.tsx | 86 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/src/store/page.tsx b/src/store/page.tsx index 74926e2..a3269e1 100644 --- a/src/store/page.tsx +++ b/src/store/page.tsx @@ -1,4 +1,7 @@ -import { Component, Fragment } from "react"; +import { Component } from "react"; +import { HiDownload } from "react-icons/hi"; + +import { PanelSectionRow, PanelSection, Focusable, staticClasses, DialogButton } from "decky-frontend-lib"; import * as backend from "../backend"; import { tr } from "usdpl-front"; @@ -22,27 +25,84 @@ export class StoreResultsPage extends Component { if (storeItems) { if (storeItems.length == 0) { backend.log(backend.LogLevel.Warn, "No store results; got array with length 0 from cache"); - return (
+ return ( { tr("No results") /* TODO translate */ } -
); +
); } else { // TODO - return storeItems.map((meta: backend.StoreMetadata) => { -
-
{ meta.name }
-
{ tr("Created by") /* TODO translate */} { meta.steam_username }
-
{ meta.tags.map((tag: string) => {tag}) }
- Hey NG you should finish this page -
- }); + return ( + { + storeItems.map((meta: backend.StoreMetadata) => { + + + { + backend.log(backend.LogLevel.Info, "Downloading settings " + meta.name + " (" + meta.id + ")"); + backend.storeDownloadById(meta.id); + }} + > + { /* TODO make this responsive when clicked */} + + +
+
+ { meta.name } +
+
+ { tr("Created by") /* TODO translate */} { meta.steam_username } +
+
+ { meta.tags.map((tag: string) => ( + {tag} + ) + ) } +
+
+
+
+ }) + } +
); } } else { backend.log(backend.LogLevel.Warn, "Store failed to load; got null from cache"); // store did not pre-load when the game started - return ( + return ( { tr("Store failed to load") /* TODO translate */ } - ); + ); } } } From c8b8cd1571389ffa5ea6ee864ed42ad0c73dcb3b Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 3 Feb 2024 09:46:21 -0500 Subject: [PATCH 39/56] Fix old fields used in specials --- backend/community_settings_srv/src/api/get_game.rs | 2 +- backend/community_settings_srv/src/api/get_setting.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/community_settings_srv/src/api/get_game.rs b/backend/community_settings_srv/src/api/get_game.rs index c7d1966..ec19424 100644 --- a/backend/community_settings_srv/src/api/get_game.rs +++ b/backend/community_settings_srv/src/api/get_game.rs @@ -38,7 +38,7 @@ fn special_settings() -> Vec { tdp: None, tdp_boost: None, clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), - slow_memory: false, + memory_clock: Some(404), }, battery: community_settings_core::v1::Battery { charge_rate: Some(42), diff --git a/backend/community_settings_srv/src/api/get_setting.rs b/backend/community_settings_srv/src/api/get_setting.rs index 47f9cc4..3121192 100644 --- a/backend/community_settings_srv/src/api/get_setting.rs +++ b/backend/community_settings_srv/src/api/get_setting.rs @@ -35,7 +35,7 @@ fn special_settings() -> community_settings_core::v1::Metadata { tdp: None, tdp_boost: None, clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }), - slow_memory: false, + memory_clock: Some(404), }, battery: community_settings_core::v1::Battery { charge_rate: Some(42), From 48ec9f518feaadf5ac9e84e71d29b8ba9bdfc84d Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 5 Feb 2024 17:45:35 -0500 Subject: [PATCH 40/56] Add on_load and on_unload traits, reduce impact of #123 --- backend/limits_srv/pt_limits_v2.json | 1261 +++++++++++++++++--- backend/src/api/handler.rs | 28 +- backend/src/settings/dev_mode/battery.rs | 14 + backend/src/settings/dev_mode/cpu.rs | 14 + backend/src/settings/dev_mode/gpu.rs | 14 + backend/src/settings/general.rs | 68 +- backend/src/settings/generic/battery.rs | 12 + backend/src/settings/generic/cpu.rs | 16 + backend/src/settings/generic/gpu.rs | 12 + backend/src/settings/generic_amd/cpu.rs | 12 + backend/src/settings/generic_amd/gpu.rs | 12 + backend/src/settings/mod.rs | 4 +- backend/src/settings/steam_deck/battery.rs | 12 + backend/src/settings/steam_deck/cpu.rs | 71 +- backend/src/settings/steam_deck/gpu.rs | 12 + backend/src/settings/traits.rs | 16 +- backend/src/settings/unknown/battery.rs | 12 + backend/src/settings/unknown/cpu.rs | 12 + backend/src/settings/unknown/gpu.rs | 12 + 19 files changed, 1416 insertions(+), 198 deletions(-) diff --git a/backend/limits_srv/pt_limits_v2.json b/backend/limits_srv/pt_limits_v2.json index 0196b5d..a0fc84e 100644 --- a/backend/limits_srv/pt_limits_v2.json +++ b/backend/limits_srv/pt_limits_v2.json @@ -1,155 +1,336 @@ { "configs": [ { - "name": "Steam Deck Custom", + "name": "Devs mode best mode", "conditions": { "dmi": null, - "cpuinfo": "model name\t: AMD Custom APU (0405)|(0932)\n", + "cpuinfo": null, "os": null, "command": null, - "file_exists": "./limits_override.json" + "file_exists": "/etc/powertools_dev_mode" }, "limits": { "cpu": { - "provider": "GabeBoyAdvance", + "provider": "DevMode", "limits": { "cpus": [ { "clock_min": { - "min": 1400, - "max": 3500 + "min": 100, + "max": 5000 }, "clock_max": { - "min": 400, - "max": 3500 + "min": 100, + "max": 4800 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true }, { "clock_min": { - "min": 1400, - "max": 3500 + "min": 100, + "max": 5000 }, "clock_max": { - "min": 400, - "max": 3500 + "min": 100, + "max": 4800 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true }, { "clock_min": { - "min": 1400, - "max": 3500 + "min": 100, + "max": 5000 }, "clock_max": { - "min": 400, - "max": 3500 + "min": 100, + "max": 4800 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true }, { "clock_min": { - "min": 1400, - "max": 3500 + "min": 100, + "max": 5000 }, "clock_max": { - "min": 400, - "max": 3500 + "min": 100, + "max": 4800 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true }, { "clock_min": { - "min": 1400, - "max": 3500 + "min": 100, + "max": 5000 }, "clock_max": { - "min": 400, - "max": 3500 + "min": 100, + "max": 4800 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true }, { "clock_min": { - "min": 1400, - "max": 3500 + "min": 100, + "max": 5000 }, "clock_max": { - "min": 400, - "max": 3500 + "min": 100, + "max": 4800 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true }, { "clock_min": { - "min": 1400, - "max": 3500 + "min": 100, + "max": 5000 }, "clock_max": { - "min": 400, - "max": 3500 + "min": 100, + "max": 4800 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true }, { "clock_min": { - "min": 1400, - "max": 3500 + "min": 100, + "max": 5000 }, "clock_max": { - "min": 400, - "max": 3500 + "min": 100, + "max": 4800 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true + }, + { + "clock_min": { + "min": 100, + "max": 5000 + }, + "clock_max": { + "min": 100, + "max": 4800 + }, + "clock_step": 100, + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true + }, + { + "clock_min": { + "min": 100, + "max": 5000 + }, + "clock_max": { + "min": 100, + "max": 4800 + }, + "clock_step": 100, + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true + }, + { + "clock_min": { + "min": 100, + "max": 5000 + }, + "clock_max": { + "min": 100, + "max": 4800 + }, + "clock_step": 100, + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, + "skip_resume_reclock": false, + "experiments": true } ], - "global_governors": true + "global_governors": true, + "extras": { + "experiments": true, + "quirks": [ + "" + ] + } } }, "gpu": { - "provider": "GabeBoyAdvance", + "provider": "DevMode", "limits": { "fast_ppt": { - "min": 1000000, - "max": 30000000 + "min": 3000000, + "max": 11000000 }, - "fast_ppt_default": 15000000, + "fast_ppt_default": 10000000, "slow_ppt": { - "min": 1000000, - "max": 29000000 + "min": 7000000, + "max": 11000000 }, - "slow_ppt_default": 15000000, + "slow_ppt_default": 10000000, "ppt_divisor": 1000000, "ppt_step": 1, - "tdp": null, - "tdp_boost": null, - "tdp_step": null, + "tdp": { + "min": 1000000, + "max": 100000000 + }, + "tdp_boost": { + "min": 1000000, + "max": 110000000 + }, + "tdp_divisor": 1000000, + "tdp_step": 1, "clock_min": { - "min": 400, - "max": 1600 + "min": 100, + "max": 1000 }, "clock_max": { - "min": 400, - "max": 1600 + "min": 100, + "max": 1100 }, "clock_step": 100, - "skip_resume_reclock": false + "memory_clock": { + "min": 100, + "max": 1100 + }, + "memory_clock_step": 100, + "skip_resume_reclock": false, + "extras": { + "experiments": true, + "quirks": [ + "dev" + ] + } } }, "battery": { - "provider": "GabeBoyAdvance", + "provider": "DevMode", "limits": { "charge_rate": { - "min": 250, - "max": 2500 + "min": 0, + "max": 1000 }, "charge_modes": [ "normal", @@ -157,10 +338,16 @@ "idle" ], "charge_limit": { - "min": 10.0, - "max": 90.0 + "min": 1.0, + "max": 99.0 }, - "extra_readouts": false + "extra_readouts": true, + "extras": { + "experiments": true, + "quirks": [ + "" + ] + } } } } @@ -169,7 +356,7 @@ "name": "Steam Deck", "conditions": { "dmi": null, - "cpuinfo": "model name\t: AMD Custom APU (0405)|(0932)\n", + "cpuinfo": "model name\t: AMD Custom APU 0405\n", "os": null, "command": null, "file_exists": null @@ -189,7 +376,12 @@ "max": 3500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -201,7 +393,12 @@ "max": 3500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -213,7 +410,12 @@ "max": 3500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -225,7 +427,12 @@ "max": 3500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -237,7 +444,12 @@ "max": 3500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -249,7 +461,12 @@ "max": 3500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -261,7 +478,12 @@ "max": 3500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -273,10 +495,19 @@ "max": 3500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false } ], - "global_governors": true + "global_governors": true, + "extras": { + "experiments": false, + "quirks": [] + } } }, "gpu": { @@ -296,6 +527,7 @@ "ppt_step": 1, "tdp": null, "tdp_boost": null, + "tdp_divisor": null, "tdp_step": null, "clock_min": { "min": 400, @@ -306,7 +538,19 @@ "max": 1600 }, "clock_step": 100, - "skip_resume_reclock": false + "memory_clock": { + "min": 400, + "max": 800 + }, + "memory_clock_step": 400, + "skip_resume_reclock": false, + "extras": { + "experiments": false, + "quirks": [ + "pp_dpm_fclk-reversed", + "pp_dpm_fclk-not-updated-on-LCD" + ] + } } }, "battery": { @@ -325,7 +569,237 @@ "min": 10.0, "max": 90.0 }, - "extra_readouts": false + "extra_readouts": false, + "extras": { + "experiments": false, + "quirks": [] + } + } + } + } + }, + { + "name": "Steam Deck OLED", + "conditions": { + "dmi": null, + "cpuinfo": "model name\t: AMD Custom APU 0932\n", + "os": null, + "command": null, + "file_exists": null + }, + "limits": { + "cpu": { + "provider": "GabeBoy", + "limits": { + "cpus": [ + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false + } + ], + "global_governors": true, + "extras": { + "experiments": false, + "quirks": [] + } + } + }, + "gpu": { + "provider": "GabeBoy", + "limits": { + "fast_ppt": { + "min": 1000000, + "max": 30000000 + }, + "fast_ppt_default": 15000000, + "slow_ppt": { + "min": 1000000, + "max": 29000000 + }, + "slow_ppt_default": 15000000, + "ppt_divisor": 1000000, + "ppt_step": 1, + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "clock_min": { + "min": 400, + "max": 1600 + }, + "clock_max": { + "min": 400, + "max": 1600 + }, + "clock_step": 100, + "memory_clock": { + "min": 400, + "max": 800 + }, + "memory_clock_step": 200, + "skip_resume_reclock": false, + "extras": { + "experiments": false, + "quirks": [ + "pp_dpm_fclk-reversed", + "pp_dpm_fclk-not-updated-on-LCD" + ] + } + } + }, + "battery": { + "provider": "GabeBoy", + "limits": { + "charge_rate": { + "min": 250, + "max": 2500 + }, + "charge_modes": [ + "normal", + "discharge", + "idle" + ], + "charge_limit": { + "min": 10.0, + "max": 90.0 + }, + "extra_readouts": false, + "extras": { + "experiments": false, + "quirks": [] + } } } } @@ -354,7 +828,12 @@ "max": 3700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -366,7 +845,12 @@ "max": 3700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -378,7 +862,12 @@ "max": 3700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -390,10 +879,19 @@ "max": 3700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false } ], - "global_governors": true + "global_governors": true, + "extras": { + "experiments": false, + "quirks": [] + } } }, "gpu": { @@ -413,6 +911,7 @@ "ppt_step": 1000, "tdp": null, "tdp_boost": null, + "tdp_divisor": null, "tdp_step": null, "clock_min": { "min": 400, @@ -423,7 +922,13 @@ "max": 1100 }, "clock_step": 100, - "skip_resume_reclock": false + "memory_clock": null, + "memory_clock_step": null, + "skip_resume_reclock": false, + "extras": { + "experiments": false, + "quirks": [] + } } }, "battery": { @@ -432,7 +937,11 @@ "charge_rate": null, "charge_modes": [], "charge_limit": null, - "extra_readouts": false + "extra_readouts": false, + "extras": { + "experiments": false, + "quirks": [] + } } } } @@ -461,7 +970,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -473,7 +987,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -485,7 +1004,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -497,7 +1021,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -509,7 +1038,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -521,7 +1055,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -533,7 +1072,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -545,7 +1089,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -557,7 +1106,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -569,7 +1123,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -581,7 +1140,12 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -593,10 +1157,19 @@ "max": 4000 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false } ], - "global_governors": true + "global_governors": true, + "extras": { + "experiments": false, + "quirks": [] + } } }, "gpu": { @@ -616,6 +1189,7 @@ "ppt_step": 1000, "tdp": null, "tdp_boost": null, + "tdp_divisor": null, "tdp_step": null, "clock_min": { "min": 400, @@ -626,7 +1200,13 @@ "max": 1600 }, "clock_step": 100, - "skip_resume_reclock": false + "memory_clock": null, + "memory_clock_step": null, + "skip_resume_reclock": false, + "extras": { + "experiments": false, + "quirks": [] + } } }, "battery": { @@ -635,7 +1215,11 @@ "charge_rate": null, "charge_modes": [], "charge_limit": null, - "extra_readouts": false + "extra_readouts": false, + "extras": { + "experiments": false, + "quirks": [] + } } } } @@ -664,7 +1248,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -676,7 +1265,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -688,7 +1282,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -700,7 +1299,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -712,7 +1316,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -724,7 +1333,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -736,7 +1350,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -748,7 +1367,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -760,7 +1384,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -772,7 +1401,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -784,7 +1418,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -796,7 +1435,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -808,7 +1452,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -820,7 +1469,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -832,7 +1486,12 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -844,10 +1503,19 @@ "max": 4500 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false } ], - "global_governors": true + "global_governors": true, + "extras": { + "experiments": false, + "quirks": [] + } } }, "gpu": { @@ -867,6 +1535,7 @@ "ppt_step": 1000, "tdp": null, "tdp_boost": null, + "tdp_divisor": null, "tdp_step": null, "clock_min": { "min": 400, @@ -877,7 +1546,13 @@ "max": 2200 }, "clock_step": 100, - "skip_resume_reclock": false + "memory_clock": null, + "memory_clock_step": null, + "skip_resume_reclock": false, + "extras": { + "experiments": false, + "quirks": [] + } } }, "battery": { @@ -886,7 +1561,11 @@ "charge_rate": null, "charge_modes": [], "charge_limit": null, - "extra_readouts": false + "extra_readouts": false, + "extras": { + "experiments": false, + "quirks": [] + } } } } @@ -915,7 +1594,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -927,7 +1611,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -939,7 +1628,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -951,7 +1645,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -963,7 +1662,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -975,7 +1679,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -987,7 +1696,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -999,7 +1713,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1011,7 +1730,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1023,7 +1747,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1035,7 +1764,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1047,7 +1781,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1059,7 +1798,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1071,7 +1815,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1083,7 +1832,12 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1095,10 +1849,19 @@ "max": 4700 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false } ], - "global_governors": true + "global_governors": true, + "extras": { + "experiments": false, + "quirks": [] + } } }, "gpu": { @@ -1118,6 +1881,7 @@ "ppt_step": 1000, "tdp": null, "tdp_boost": null, + "tdp_divisor": null, "tdp_step": null, "clock_min": { "min": 400, @@ -1128,7 +1892,13 @@ "max": 2200 }, "clock_step": 100, - "skip_resume_reclock": false + "memory_clock": null, + "memory_clock_step": null, + "skip_resume_reclock": false, + "extras": { + "experiments": false, + "quirks": [] + } } }, "battery": { @@ -1137,7 +1907,11 @@ "charge_rate": null, "charge_modes": [], "charge_limit": null, - "extra_readouts": false + "extra_readouts": false, + "extras": { + "experiments": false, + "quirks": [] + } } } } @@ -1166,7 +1940,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1178,7 +1957,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1190,7 +1974,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1202,7 +1991,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1214,7 +2008,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1226,7 +2025,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1238,7 +2042,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1250,7 +2059,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1262,7 +2076,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1274,7 +2093,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1286,7 +2110,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1298,7 +2127,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1310,7 +2144,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1322,7 +2161,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1334,7 +2178,12 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": { @@ -1346,10 +2195,19 @@ "max": 5100 }, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false } ], - "global_governors": true + "global_governors": true, + "extras": { + "experiments": false, + "quirks": [] + } } }, "gpu": { @@ -1369,11 +2227,18 @@ "ppt_step": 1000, "tdp": null, "tdp_boost": null, + "tdp_divisor": null, "tdp_step": null, "clock_min": null, "clock_max": null, "clock_step": null, - "skip_resume_reclock": false + "memory_clock": null, + "memory_clock_step": null, + "skip_resume_reclock": false, + "extras": { + "experiments": false, + "quirks": [] + } } }, "battery": { @@ -1382,7 +2247,11 @@ "charge_rate": null, "charge_modes": [], "charge_limit": null, - "extra_readouts": false + "extra_readouts": false, + "extras": { + "experiments": false, + "quirks": [] + } } } } @@ -1405,52 +2274,96 @@ "clock_min": null, "clock_max": null, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": null, "clock_max": null, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": null, "clock_max": null, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": null, "clock_max": null, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": null, "clock_max": null, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": null, "clock_max": null, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": null, "clock_max": null, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false }, { "clock_min": null, "clock_max": null, "clock_step": 100, - "skip_resume_reclock": false + "tdp": null, + "tdp_boost": null, + "tdp_divisor": null, + "tdp_step": null, + "skip_resume_reclock": false, + "experiments": false } ], - "global_governors": true + "global_governors": true, + "extras": { + "experiments": false, + "quirks": [] + } } }, "gpu": { @@ -1464,11 +2377,18 @@ "ppt_step": null, "tdp": null, "tdp_boost": null, + "tdp_divisor": null, "tdp_step": null, "clock_min": null, "clock_max": null, "clock_step": null, - "skip_resume_reclock": false + "memory_clock": null, + "memory_clock_step": null, + "skip_resume_reclock": false, + "extras": { + "experiments": false, + "quirks": [] + } } }, "battery": { @@ -1477,7 +2397,11 @@ "charge_rate": null, "charge_modes": [], "charge_limit": null, - "extra_readouts": false + "extra_readouts": false, + "extras": { + "experiments": false, + "quirks": [] + } } } } @@ -1491,5 +2415,6 @@ "url": "https://git.ngni.us/NG-SD-Plugins/PowerTools/wiki" } ], + "store": "https://powertools.ngni.us", "refresh": "http://limits.ngni.us:45000/powertools/v2" } \ No newline at end of file diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index 194b179..5058eab 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -3,7 +3,8 @@ use std::sync::mpsc::{self, Receiver, Sender}; use crate::persist::SettingsJson; use crate::settings::{ - MinMax, OnPowerEvent, OnResume, OnSet, PowerMode, Settings, TBattery, TCpus, TGeneral, TGpu, + MinMax, OnLoad, OnPowerEvent, OnResume, OnSet, OnUnload, PowerMode, Settings, TBattery, TCpus, + TGeneral, TGpu, }; type Callback = Box; @@ -401,13 +402,22 @@ impl ApiMessageHandler { } ApiMessage::LoadSettings(id, name, variant_id, variant_name) => { let path = format!("{}.ron", id); + if let Err(e) = settings.on_unload() { + print_errors("LoadSettings on_unload()", e); + } match settings.load_file(path.into(), id, name, variant_id, variant_name, false) { Ok(success) => log::info!("Loaded settings file? {}", success), Err(e) => log::warn!("Load file err: {}", e), } + if let Err(e) = settings.on_load() { + print_errors("LoadSettings on_load()", e); + } true } ApiMessage::LoadVariant(variant_id, variant_name) => { + if let Err(e) = settings.on_unload() { + print_errors("LoadVariant on_unload()", e); + } let path = settings.general.get_path(); let app_id = settings.general.get_app_id(); match settings.load_file( @@ -421,9 +431,15 @@ impl ApiMessageHandler { Ok(success) => log::info!("Loaded variant settings file? {}", success), Err(e) => log::warn!("Load file err: {}", e), } + if let Err(e) = settings.on_load() { + print_errors("LoadVariant on_load()", e); + } true } ApiMessage::LoadMainSettings => { + if let Err(e) = settings.on_unload() { + print_errors("LoadMainSettings on_unload()", e); + } match settings.load_file( crate::consts::DEFAULT_SETTINGS_FILE.into(), 0, @@ -435,14 +451,23 @@ impl ApiMessageHandler { Ok(success) => log::info!("Loaded main settings file? {}", success), Err(e) => log::warn!("Load file err: {}", e), } + if let Err(e) = settings.on_load() { + print_errors("LoadMainSettings on_load()", e); + } true } ApiMessage::LoadSystemSettings => { + if let Err(e) = settings.on_unload() { + print_errors("LoadSystemSettings on_unload()", e); + } settings.load_system_default( settings.general.get_name().to_owned(), settings.general.get_variant_id(), settings.general.get_variant_info().name, ); + if let Err(e) = settings.on_load() { + print_errors("LoadSystemSettings on_load()", e); + } true } ApiMessage::GetLimits(cb) => { @@ -464,7 +489,6 @@ impl ApiMessageHandler { false } ApiMessage::UploadCurrentVariant(steam_id, steam_username) => { - //TODO let steam_app_id = settings.general.get_app_id(); super::web::upload_settings( steam_app_id, diff --git a/backend/src/settings/dev_mode/battery.rs b/backend/src/settings/dev_mode/battery.rs index a57e736..a0fe208 100644 --- a/backend/src/settings/dev_mode/battery.rs +++ b/backend/src/settings/dev_mode/battery.rs @@ -76,6 +76,20 @@ impl OnResume for Battery { impl crate::settings::OnPowerEvent for Battery {} +impl crate::settings::OnLoad for Battery { + fn on_load(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Battery::on_load(self)"); + Ok(()) + } +} + +impl crate::settings::OnUnload for Battery { + fn on_unload(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Battery::on_unload(self)"); + Ok(()) + } +} + impl TBattery for Battery { fn limits(&self) -> crate::api::BatteryLimits { log::debug!("dev_mode_Battery::limits(self) -> {{...}}"); diff --git a/backend/src/settings/dev_mode/cpu.rs b/backend/src/settings/dev_mode/cpu.rs index a505b69..059aaa8 100644 --- a/backend/src/settings/dev_mode/cpu.rs +++ b/backend/src/settings/dev_mode/cpu.rs @@ -39,6 +39,20 @@ impl OnResume for Cpus { impl crate::settings::OnPowerEvent for Cpus {} +impl crate::settings::OnLoad for Cpus { + fn on_load(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Cpus::on_load(self)"); + Ok(()) + } +} + +impl crate::settings::OnUnload for Cpus { + fn on_unload(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Cpus::on_unload(self)"); + Ok(()) + } +} + impl ProviderBuilder, GenericCpusLimit> for Cpus { fn from_json_and_limits( persistent: Vec, diff --git a/backend/src/settings/dev_mode/gpu.rs b/backend/src/settings/dev_mode/gpu.rs index 919d905..c6605a2 100644 --- a/backend/src/settings/dev_mode/gpu.rs +++ b/backend/src/settings/dev_mode/gpu.rs @@ -80,6 +80,20 @@ impl OnResume for Gpu { impl crate::settings::OnPowerEvent for Gpu {} +impl crate::settings::OnLoad for Gpu { + fn on_load(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Gpu::on_load(self)"); + Ok(()) + } +} + +impl crate::settings::OnUnload for Gpu { + fn on_unload(&mut self) -> Result<(), Vec> { + log::debug!("dev_mode_Gpu::on_unload(self)"); + Ok(()) + } +} + impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { log::debug!("dev_mode_Gpu::limits(self) -> {{...}}"); diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index 2a75eae..1cc1a33 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; //use std::sync::{Arc, Mutex}; //use super::{Battery, Cpus, Gpu}; -use super::{OnResume, OnSet, SettingError}; +use super::{OnLoad, OnPowerEvent, OnResume, OnSet, OnUnload, SettingError}; use super::{TBattery, TCpus, TGeneral, TGpu}; use crate::persist::{FileJson, SettingsJson}; //use crate::utility::unwrap_lock; @@ -51,7 +51,19 @@ impl OnResume for General { } } -impl crate::settings::OnPowerEvent for General {} +impl OnPowerEvent for General {} + +impl OnLoad for General { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl OnUnload for General { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} impl TGeneral for General { fn limits(&self) -> crate::api::GeneralLimits { @@ -461,7 +473,7 @@ impl OnResume for Settings { } } -impl crate::settings::OnPowerEvent for Settings { +impl OnPowerEvent for Settings { fn on_power_event(&mut self, new_mode: super::PowerMode) -> Result<(), Vec> { let mut errors = Vec::new(); @@ -486,6 +498,56 @@ impl crate::settings::OnPowerEvent for Settings { } } +impl OnLoad for Settings { + fn on_load(&mut self) -> Result<(), Vec> { + let mut errors = Vec::new(); + + self.general + .on_load() + .unwrap_or_else(|mut e| errors.append(&mut e)); + self.battery + .on_load() + .unwrap_or_else(|mut e| errors.append(&mut e)); + self.cpus + .on_load() + .unwrap_or_else(|mut e| errors.append(&mut e)); + self.gpu + .on_load() + .unwrap_or_else(|mut e| errors.append(&mut e)); + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +impl OnUnload for Settings { + fn on_unload(&mut self) -> Result<(), Vec> { + let mut errors = Vec::new(); + + self.general + .on_unload() + .unwrap_or_else(|mut e| errors.append(&mut e)); + self.battery + .on_unload() + .unwrap_or_else(|mut e| errors.append(&mut e)); + self.cpus + .on_unload() + .unwrap_or_else(|mut e| errors.append(&mut e)); + self.gpu + .on_unload() + .unwrap_or_else(|mut e| errors.append(&mut e)); + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + /*impl Into for Settings { #[inline] fn into(self) -> SettingsJson { diff --git a/backend/src/settings/generic/battery.rs b/backend/src/settings/generic/battery.rs index cfe201a..a046412 100644 --- a/backend/src/settings/generic/battery.rs +++ b/backend/src/settings/generic/battery.rs @@ -132,6 +132,18 @@ impl OnResume for Battery { impl crate::settings::OnPowerEvent for Battery {} +impl crate::settings::OnLoad for Battery { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Battery { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + impl TBattery for Battery { fn limits(&self) -> crate::api::BatteryLimits { crate::api::BatteryLimits { diff --git a/backend/src/settings/generic/cpu.rs b/backend/src/settings/generic/cpu.rs index 0083028..6142f62 100644 --- a/backend/src/settings/generic/cpu.rs +++ b/backend/src/settings/generic/cpu.rs @@ -176,6 +176,22 @@ impl + AsRef + TCpu + crate::settings::OnPowerEvent> } } +impl + AsRef + TCpu + OnResume + OnSet + crate::settings::OnPowerEvent> + crate::settings::OnLoad for Cpus +{ + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl + AsRef + TCpu + OnResume + OnSet + crate::settings::OnPowerEvent> + crate::settings::OnUnload for Cpus +{ + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + impl + AsRef + TCpu + OnResume + OnSet + crate::settings::OnPowerEvent> TCpus for Cpus { diff --git a/backend/src/settings/generic/gpu.rs b/backend/src/settings/generic/gpu.rs index 1cc61db..a96bae8 100644 --- a/backend/src/settings/generic/gpu.rs +++ b/backend/src/settings/generic/gpu.rs @@ -138,6 +138,18 @@ impl OnResume for Gpu { impl crate::settings::OnPowerEvent for Gpu {} +impl crate::settings::OnLoad for Gpu { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Gpu { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { crate::api::GpuLimits { diff --git a/backend/src/settings/generic_amd/cpu.rs b/backend/src/settings/generic_amd/cpu.rs index 2f618b2..0809ee6 100644 --- a/backend/src/settings/generic_amd/cpu.rs +++ b/backend/src/settings/generic_amd/cpu.rs @@ -43,6 +43,18 @@ impl OnSet for Cpus { impl crate::settings::OnPowerEvent for Cpus {} +impl crate::settings::OnLoad for Cpus { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Cpus { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + impl TCpus for Cpus { fn limits(&self) -> crate::api::CpusLimits { self.generic.limits() diff --git a/backend/src/settings/generic_amd/gpu.rs b/backend/src/settings/generic_amd/gpu.rs index 4b5066e..265956c 100644 --- a/backend/src/settings/generic_amd/gpu.rs +++ b/backend/src/settings/generic_amd/gpu.rs @@ -312,6 +312,18 @@ impl OnSet for Gpu { impl crate::settings::OnPowerEvent for Gpu {} +impl crate::settings::OnLoad for Gpu { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Gpu { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + fn bad_gpu_limits() -> crate::api::GpuLimits { crate::api::GpuLimits { fast_ppt_limits: None, diff --git a/backend/src/settings/mod.rs b/backend/src/settings/mod.rs index 91e4662..4e74636 100644 --- a/backend/src/settings/mod.rs +++ b/backend/src/settings/mod.rs @@ -22,8 +22,8 @@ pub use min_max::{min_max_from_json, MinMax}; pub use error::SettingError; pub use traits::{ - OnPowerEvent, OnResume, OnSet, PowerMode, ProviderBuilder, TBattery, TCpu, TCpus, TGeneral, - TGpu, + OnLoad, OnPowerEvent, OnResume, OnSet, OnUnload, PowerMode, ProviderBuilder, TBattery, TCpu, + TCpus, TGeneral, TGpu, }; #[cfg(test)] diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index f47d9bd..d48e2a5 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -743,6 +743,18 @@ impl OnPowerEvent for Battery { } } +impl crate::settings::OnLoad for Battery { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Battery { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + impl TBattery for Battery { fn limits(&self) -> crate::api::BatteryLimits { crate::api::BatteryLimits { diff --git a/backend/src/settings/steam_deck/cpu.rs b/backend/src/settings/steam_deck/cpu.rs index a1c78b6..de849fb 100644 --- a/backend/src/settings/steam_deck/cpu.rs +++ b/backend/src/settings/steam_deck/cpu.rs @@ -181,6 +181,35 @@ impl ProviderBuilder, GenericCpusLimit> for Cpus { impl crate::settings::OnPowerEvent for Cpus {} +impl crate::settings::OnLoad for Cpus { + fn on_load(&mut self) -> Result<(), Vec> { + let mut errors = Vec::new(); + for cpu in &mut self.cpus { + cpu.on_load().unwrap_or_else(|mut e| errors.append(&mut e)); + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +impl crate::settings::OnUnload for Cpus { + fn on_unload(&mut self) -> Result<(), Vec> { + let mut errors = Vec::new(); + for cpu in &mut self.cpus { + cpu.on_unload() + .unwrap_or_else(|mut e| errors.append(&mut e)); + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + impl TCpus for Cpus { fn limits(&self) -> crate::api::CpusLimits { crate::api::CpusLimits { @@ -404,9 +433,6 @@ impl Cpu { ) .unwrap_or_else(|e| errors.push(e)); } - // TODO remove this when it's no longer needed - self.clock_unset_workaround() - .unwrap_or_else(|mut e| errors.append(&mut e)); if errors.is_empty() { Ok(()) } else { @@ -418,14 +444,9 @@ impl Cpu { } // https://github.com/NGnius/PowerTools/issues/107 - fn clock_unset_workaround(&self) -> Result<(), Vec> { - if !self.state.is_resuming { - let mut errors = Vec::new(); - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_cpu(true, self.index); - // always set clock speeds, since it doesn't reset correctly (kernel/hardware bug) - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.enforce_level(&self.sysfs)?; - // disable manual clock limits - log::debug!("Setting CPU {} to default clockspeed", self.index); + fn clock_unset(&self) -> Result<(), Vec> { + let mut errors = Vec::new(); + if self.state.clock_limits_set && POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.needs_manual() { // max clock self.set_clock_limit( self.index, @@ -445,15 +466,15 @@ impl Cpu { self.reset_clock_limits().unwrap_or_else(|e| errors.push(e)); self.set_confirm().unwrap_or_else(|e| errors.push(e)); - - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_cpu(false, self.index); - if errors.is_empty() { - Ok(()) - } else { - Err(errors) - } - } else { + } + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_cpu(false, self.index); + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT + .enforce_level(&self.sysfs) + .unwrap_or_else(|mut e| errors.append(&mut e)); + if errors.is_empty() { Ok(()) + } else { + Err(errors) } } @@ -639,6 +660,18 @@ impl OnResume for Cpu { } } +impl crate::settings::OnLoad for Cpu { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Cpu { + fn on_unload(&mut self) -> Result<(), Vec> { + self.clock_unset() + } +} + impl TCpu for Cpu { fn online(&mut self) -> &mut bool { &mut self.online diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index 4912713..562923b 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -597,6 +597,18 @@ impl OnResume for Gpu { impl crate::settings::OnPowerEvent for Gpu {} +impl crate::settings::OnLoad for Gpu { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Gpu { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { crate::api::GpuLimits { diff --git a/backend/src/settings/traits.rs b/backend/src/settings/traits.rs index f68f5cf..2c8524f 100644 --- a/backend/src/settings/traits.rs +++ b/backend/src/settings/traits.rs @@ -10,6 +10,14 @@ pub trait OnResume { fn on_resume(&self) -> Result<(), Vec>; } +pub trait OnLoad { + fn on_load(&mut self) -> Result<(), Vec>; +} + +pub trait OnUnload { + fn on_unload(&mut self) -> Result<(), Vec>; +} + #[repr(u8)] #[derive(Clone, Copy, Debug)] pub enum PowerMode { @@ -46,7 +54,7 @@ pub trait ProviderBuilder { fn from_limits(limits: L) -> Self; } -pub trait TGpu: OnSet + OnResume + OnPowerEvent + Debug + Send { +pub trait TGpu: OnSet + OnResume + OnPowerEvent + OnLoad + OnUnload + Debug + Send { fn limits(&self) -> crate::api::GpuLimits; fn json(&self) -> crate::persist::GpuJson; @@ -68,7 +76,7 @@ pub trait TGpu: OnSet + OnResume + OnPowerEvent + Debug + Send { } } -pub trait TCpus: OnSet + OnResume + OnPowerEvent + Debug + Send { +pub trait TCpus: OnSet + OnResume + OnPowerEvent + OnLoad + OnUnload + Debug + Send { fn limits(&self) -> crate::api::CpusLimits; fn json(&self) -> Vec; @@ -96,7 +104,7 @@ pub trait TCpu: Debug + Send { fn get_clock_limits(&self) -> Option<&MinMax>; } -pub trait TGeneral: OnSet + OnResume + OnPowerEvent + Debug + Send { +pub trait TGeneral: OnSet + OnResume + OnPowerEvent + OnLoad + OnUnload + Debug + Send { fn limits(&self) -> crate::api::GeneralLimits; fn get_persistent(&self) -> bool; @@ -133,7 +141,7 @@ pub trait TGeneral: OnSet + OnResume + OnPowerEvent + Debug + Send { fn provider(&self) -> crate::persist::DriverJson; } -pub trait TBattery: OnSet + OnResume + OnPowerEvent + Debug + Send { +pub trait TBattery: OnSet + OnResume + OnPowerEvent + OnLoad + OnUnload + Debug + Send { fn limits(&self) -> crate::api::BatteryLimits; fn json(&self) -> crate::persist::BatteryJson; diff --git a/backend/src/settings/unknown/battery.rs b/backend/src/settings/unknown/battery.rs index cdd5c96..ed2cdda 100644 --- a/backend/src/settings/unknown/battery.rs +++ b/backend/src/settings/unknown/battery.rs @@ -56,6 +56,18 @@ impl OnResume for Battery { impl crate::settings::OnPowerEvent for Battery {} +impl crate::settings::OnLoad for Battery { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Battery { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + impl TBattery for Battery { fn limits(&self) -> crate::api::BatteryLimits { crate::api::BatteryLimits { diff --git a/backend/src/settings/unknown/cpu.rs b/backend/src/settings/unknown/cpu.rs index 2aed5c8..7c1ebd9 100644 --- a/backend/src/settings/unknown/cpu.rs +++ b/backend/src/settings/unknown/cpu.rs @@ -71,6 +71,18 @@ impl OnResume for Cpus { impl crate::settings::OnPowerEvent for Cpus {} +impl crate::settings::OnLoad for Cpus { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Cpus { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + impl Cpus { pub fn cpu_count() -> Option { let mut data: String = usdpl_back::api::files::read_single(CPU_PRESENT_PATH) diff --git a/backend/src/settings/unknown/gpu.rs b/backend/src/settings/unknown/gpu.rs index 4adcfec..d7c4826 100644 --- a/backend/src/settings/unknown/gpu.rs +++ b/backend/src/settings/unknown/gpu.rs @@ -55,6 +55,18 @@ impl OnResume for Gpu { impl crate::settings::OnPowerEvent for Gpu {} +impl crate::settings::OnLoad for Gpu { + fn on_load(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +impl crate::settings::OnUnload for Gpu { + fn on_unload(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { crate::api::GpuLimits { From e27c899d1abafd54f482403f9a8d5d86ee60351e Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 5 Feb 2024 18:16:48 -0500 Subject: [PATCH 41/56] Set version to 2.0.0-alpha1 --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- package.json | 2 +- pt_oc.json | 134 --------------------------------------------- 4 files changed, 3 insertions(+), 137 deletions(-) delete mode 100644 pt_oc.json diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 6d6f640..bd0c8b9 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "powertools" -version = "1.5.0-ng1" +version = "2.0.0-alpha1" dependencies = [ "async-trait", "chrono", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 06cdd2e..dffea73 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "powertools" -version = "1.5.0-ng1" +version = "2.0.0-alpha1" edition = "2021" authors = ["NGnius (Graham) "] description = "Backend (superuser) functionality for PowerTools" diff --git a/package.json b/package.json index 7f17bc1..bddc88d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "PowerTools", - "version": "1.5.0-ng1", + "version": "2.0.0-alpha1", "description": "Power tweaks for power users", "scripts": { "build": "shx rm -rf dist && rollup -c", diff --git a/pt_oc.json b/pt_oc.json deleted file mode 100644 index 4ebd7c1..0000000 --- a/pt_oc.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "battery": { - "charge_rate": { - "min": 250, - "max": 2500 - }, - "extra_readouts": false - }, - "cpus": { - "cpus": [ - { - "clock_min": { - "min": 1400, - "max": 3500 - }, - "clock_max": { - "min": 400, - "max": 3500 - }, - "clock_step": 100, - "skip_resume_reclock": false - }, - { - "clock_min": { - "min": 1400, - "max": 3500 - }, - "clock_max": { - "min": 400, - "max": 3500 - }, - "clock_step": 100, - "skip_resume_reclock": false - }, - { - "clock_min": { - "min": 1400, - "max": 3500 - }, - "clock_max": { - "min": 400, - "max": 3500 - }, - "clock_step": 100, - "skip_resume_reclock": false - }, - { - "clock_min": { - "min": 1400, - "max": 3500 - }, - "clock_max": { - "min": 400, - "max": 3500 - }, - "clock_step": 100, - "skip_resume_reclock": false - }, - { - "clock_min": { - "min": 1400, - "max": 3500 - }, - "clock_max": { - "min": 400, - "max": 3500 - }, - "clock_step": 100, - "skip_resume_reclock": false - }, - { - "clock_min": { - "min": 1400, - "max": 3500 - }, - "clock_max": { - "min": 400, - "max": 3500 - }, - "clock_step": 100, - "skip_resume_reclock": false - }, - { - "clock_min": { - "min": 1400, - "max": 3500 - }, - "clock_max": { - "min": 400, - "max": 3500 - }, - "clock_step": 100, - "skip_resume_reclock": false - }, - { - "clock_min": { - "min": 1400, - "max": 3500 - }, - "clock_max": { - "min": 400, - "max": 3500 - }, - "clock_step": 100, - "skip_resume_reclock": false - } - ], - "global_governors": true - }, - "gpu": { - "fast_ppt": { - "min": 1000000, - "max": 30000000 - }, - "fast_ppt_default": 15000000, - "slow_ppt": { - "min": 1000000, - "max": 29000000 - }, - "slow_ppt_default": 15000000, - "ppt_divisor": 1000000, - "ppt_step": 1, - "clock_min": { - "min": 400, - "max": 1600 - }, - "clock_max": { - "min": 400, - "max": 1600 - }, - "clock_step": 100, - "skip_resume_reclock": false - } -} \ No newline at end of file From 1a4bfb9669187ac7420f0c5b4d9b542b3dd5aa0b Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Tue, 6 Feb 2024 22:48:29 -0500 Subject: [PATCH 42/56] Add ability to name variant during creation; v2.0.0-alpha2 --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- default_settings.ron | 109 +++++++++++++++------------- limits_override.ron | 58 +++++++++++++++ package.json | 2 +- src/components/text_field_modal.tsx | 65 +++++++++++++++++ src/index.tsx | 54 +++++++------- 7 files changed, 212 insertions(+), 80 deletions(-) create mode 100644 src/components/text_field_modal.tsx diff --git a/backend/Cargo.lock b/backend/Cargo.lock index bd0c8b9..61c7515 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "powertools" -version = "2.0.0-alpha1" +version = "2.0.0-alpha2" dependencies = [ "async-trait", "chrono", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index dffea73..0c13046 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "powertools" -version = "2.0.0-alpha1" +version = "2.0.0-alpha2" edition = "2021" authors = ["NGnius (Graham) "] description = "Backend (superuser) functionality for PowerTools" diff --git a/default_settings.ron b/default_settings.ron index fbc356a..b0a7583 100644 --- a/default_settings.ron +++ b/default_settings.ron @@ -1,58 +1,8 @@ FileJson( version: 0, name: "Main", + app_id: 0, variants: { - 0: SettingsJson( - version: 0, - name: "Primary", - variant: 0, - persistent: false, - cpus: [CpuJson( - online: true, - clock_limits: None, - governor: "schedutil", - ), CpuJson( - online: true, - clock_limits: None, - governor: "schedutil", - ), CpuJson( - online: true, - clock_limits: None, - governor: "schedutil", - ), CpuJson( - online: true, - clock_limits: None, - governor: "schedutil", - ), CpuJson( - online: true, - clock_limits: None, - governor: "schedutil", - ), CpuJson( - online: true, - clock_limits: None, - governor: "schedutil", - ), CpuJson( - online: true, - clock_limits: None, - governor: "schedutil", - ), CpuJson( - online: true, - clock_limits: None, - governor: "schedutil", - )], - gpu: GpuJson( - fast_ppt: None, - slow_ppt: None, - clock_limits: None, - slow_memory: false, - ), - battery: BatteryJson( - charge_rate: None, - charge_mode: None, - events: [], -), - provider: None, - ), 42: SettingsJson( version: 0, name: "FortySecondary", @@ -94,8 +44,63 @@ FileJson( gpu: GpuJson( fast_ppt: None, slow_ppt: None, + tdp: None, + tdp_boost: None, clock_limits: None, - slow_memory: false, + memory_clock: None, + ), + battery: BatteryJson( + charge_rate: None, + charge_mode: None, + events: [], +), + provider: None, + ), + 0: SettingsJson( + version: 0, + name: "Primary", + variant: 0, + persistent: false, + cpus: [CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + )], + gpu: GpuJson( + fast_ppt: None, + slow_ppt: None, + tdp: None, + tdp_boost: None, + clock_limits: None, + memory_clock: None, ), battery: BatteryJson( charge_rate: None, diff --git a/limits_override.ron b/limits_override.ron index 2d74572..e963283 100644 --- a/limits_override.ron +++ b/limits_override.ron @@ -12,7 +12,12 @@ Limits( max: Some(3500), )), clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -23,7 +28,12 @@ Limits( max: Some(3500), )), clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -34,7 +44,12 @@ Limits( max: Some(3500), )), clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -45,7 +60,12 @@ Limits( max: Some(3500), )), clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -56,7 +76,12 @@ Limits( max: Some(3500), )), clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -67,7 +92,12 @@ Limits( max: Some(3500), )), clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -78,7 +108,12 @@ Limits( max: Some(3500), )), clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -89,9 +124,18 @@ Limits( max: Some(3500), )), clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, skip_resume_reclock: false, + experiments: false, )], global_governors: true, + extras: LimitExtras( + experiments: false, + quirks: [], +), ), ), gpu: Limit( @@ -111,6 +155,7 @@ Limits( ppt_step: Some(1), tdp: None, tdp_boost: None, + tdp_divisor: None, tdp_step: None, clock_min: Some(RangeLimit( min: Some(400), @@ -121,7 +166,16 @@ Limits( max: Some(1600), )), clock_step: Some(100), + memory_clock: Some(RangeLimit( + min: Some(400), + max: Some(800), + )), + memory_clock_step: Some(400), skip_resume_reclock: false, + extras: LimitExtras( + experiments: false, + quirks: ["pp_dpm_fclk-not-updated-on-LCD", "pp_dpm_fclk-reversed"], + ), ), ), battery: Limit( @@ -137,6 +191,10 @@ Limits( max: Some(90.0), )), extra_readouts: false, + extras: LimitExtras( + experiments: false, + quirks: [], +), ), ), ) \ No newline at end of file diff --git a/package.json b/package.json index bddc88d..04f659b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "PowerTools", - "version": "2.0.0-alpha1", + "version": "2.0.0-alpha2", "description": "Power tweaks for power users", "scripts": { "build": "shx rm -rf dist && rollup -c", diff --git a/src/components/text_field_modal.tsx b/src/components/text_field_modal.tsx new file mode 100644 index 0000000..c5ff5d6 --- /dev/null +++ b/src/components/text_field_modal.tsx @@ -0,0 +1,65 @@ +// Based on https://github.com/isiah-lloyd/radiyo-steam-deck/blob/main/src/TextFieldModal.tsx + +import { ModalRoot, ModalRootProps, Router, TextField, Focusable, DialogButton } from 'decky-frontend-lib'; +import { useEffect, useRef, useState } from 'react'; +import { HiCheck, HiX } from "react-icons/hi"; +type props = ModalRootProps & { + label: string, + placeholder: string, + onClosed: (inputText: string) => void; +} +export const TextFieldModal = ({ closeModal, onClosed, label, placeholder }: props) => { + const [inputText, setInputText] = useState(''); + const handleText = (e: React.ChangeEvent) => { + setInputText(e.target.value); + }; + const textField = useRef(); + useEffect(() => { + Router.CloseSideMenus(); + //This will open up the virtual keyboard + textField.current?.element?.click(); + }, []); + const submit = () => onClosed(inputText); + return ( + +
+ + + { submit() }} + > + + + { if (closeModal) { closeModal() } }} + > + + + + +
+ ); +}; diff --git a/src/index.tsx b/src/index.tsx index 8fb9f4d..fb54817 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,25 +2,20 @@ import { ButtonItem, definePlugin, DialogButton, - //Menu, - //MenuItem, PanelSection, PanelSectionRow, ServerAPI, - //showContextMenu, staticClasses, - //SliderField, ToggleField, - //Dropdown, Field, Dropdown, SingleDropdownOption, Navigation, Focusable, Spinner, - //NotchLabel - //gamepadDialogClasses, - //joinClassNames, + showModal, + QuickAccessTab, + ShowModalResult, } from "decky-frontend-lib"; import { VFC, useState } from "react"; import { GiDrill, GiFireExtinguisher, GiFireBomb, GiMineExplosion } from "react-icons/gi"; @@ -83,6 +78,8 @@ import { Battery } from "./components/battery"; import { Cpus } from "./components/cpus"; import { DevMessages } from "./components/message"; +import { TextFieldModal } from "./components/text_field_modal"; + import { StoreResultsPage } from "./store/page"; var periodicHook: NodeJS.Timeout | null = null; @@ -346,12 +343,29 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { data: elem, label: {elem.name}, };}); - console.log("variant options", variantOptions); - console.log("current variant", get_value(CURRENT_VARIANT_GEN)); - console.log("variant selected", variantOptions.find((val: SingleDropdownOption, _index, _arr) => { - backend.log(backend.LogLevel.Debug, "POWERTOOLS: looking for variant data.id " + (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).id.toString()); - return (val.data as backend.VariantInfo).id == (get_value(CURRENT_VARIANT_GEN) as backend.VariantInfo).id; - })); + + var modalResult: ShowModalResult | undefined = undefined; + + const onNewVariantModelClosed = (name: string) => { + if (modalResult) { + modalResult.Close(); + } + console.log("POWERTOOLS: variant name", name); + isVariantLoading = true; + backend.resolve( + backend.loadGeneralSettingsVariant("please give me a new ID k thx bye" /* anything that cannot be parsed as a u64 will be set to u64::MAX, which will cause the back-end to auto-generate an ID */, name), + (ok: boolean) => { + isVariantLoading = false; + backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok); + reload(); + backend.resolve(backend.waitForComplete(), (_) => { + backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new settings variant"); + tryNotifyProfileChange(); + }); + } + ); + Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky); + }; return ( @@ -441,17 +455,7 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { //layout="below" onClick={(_: MouseEvent) => { backend.log(backend.LogLevel.Debug, "Creating new PowerTools settings variant"); - backend.resolve( - backend.loadGeneralSettingsVariant("please give me a new ID k thx bye" /* anything that cannot be parsed as a u64 will be set to u64::MAX, which will cause the back-end to auto-generate an ID */, undefined), - (ok: boolean) => { - backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok); - reload(); - backend.resolve(backend.waitForComplete(), (_) => { - backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new settings variant"); - tryNotifyProfileChange(); - }); - } - ); + modalResult = showModal( { modalResult?.Close(); Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky)}}/>, window); }} > From 3e3e9a68f4cb1782b9bbba960f96edf60009078a Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 10 Feb 2024 12:52:21 -0500 Subject: [PATCH 43/56] Add Steam Deck max clock detection since OC can now be reported correctly version 2.0.0-alpha3 --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- backend/limits_core/src/json_v2/base.rs | 10 +- backend/limits_core/src/json_v2/cpu_limit.rs | 38 +- backend/limits_core/src/json_v2/gpu_limit.rs | 1 + backend/limits_srv/pt_limits_v2.json | 524 +++++++++++++++---- backend/src/settings/steam_deck/cpu.rs | 41 +- backend/src/settings/steam_deck/gpu.rs | 41 +- package.json | 2 +- 9 files changed, 536 insertions(+), 125 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 61c7515..a2f06f9 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "powertools" -version = "2.0.0-alpha2" +version = "2.0.0-alpha3" dependencies = [ "async-trait", "chrono", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 0c13046..ef0df3b 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "powertools" -version = "2.0.0-alpha2" +version = "2.0.0-alpha3" edition = "2021" authors = ["NGnius (Graham) "] description = "Backend (superuser) functionality for PowerTools" diff --git a/backend/limits_core/src/json_v2/base.rs b/backend/limits_core/src/json_v2/base.rs index e066c71..72aa0f3 100644 --- a/backend/limits_core/src/json_v2/base.rs +++ b/backend/limits_core/src/json_v2/base.rs @@ -77,16 +77,16 @@ impl Default for Base { }, limits: super::Limits { cpu: super::Limit { - provider: super::CpuLimitType::SteamDeck, - limits: super::GenericCpusLimit::default_for(super::CpuLimitType::SteamDeck), + provider: super::CpuLimitType::SteamDeckOLED, + limits: super::GenericCpusLimit::default_for(super::CpuLimitType::SteamDeckOLED), }, gpu: super::Limit { - provider: super::GpuLimitType::SteamDeck, + provider: super::GpuLimitType::SteamDeckOLED, limits: super::GenericGpuLimit::default_for(super::GpuLimitType::SteamDeckOLED), }, battery: super::Limit { - provider: super::BatteryLimitType::SteamDeck, - limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::SteamDeck), + provider: super::BatteryLimitType::SteamDeckOLED, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::SteamDeckOLED), }, } }, diff --git a/backend/limits_core/src/json_v2/cpu_limit.rs b/backend/limits_core/src/json_v2/cpu_limit.rs index 24b7fde..4433a66 100644 --- a/backend/limits_core/src/json_v2/cpu_limit.rs +++ b/backend/limits_core/src/json_v2/cpu_limit.rs @@ -93,13 +93,14 @@ pub struct GenericCpuLimit { pub tdp_divisor: Option, pub tdp_step: Option, pub skip_resume_reclock: bool, - pub experiments: bool, + pub extras: super::LimitExtras, } impl GenericCpuLimit { pub fn default_for(t: &CpuLimitType, _index: usize) -> Self { match t { - CpuLimitType::SteamDeck | CpuLimitType::SteamDeckOLED => Self::default_steam_deck(), + CpuLimitType::SteamDeck => Self::default_steam_deck(), + CpuLimitType::SteamDeckOLED => Self::default_steam_deck_oled(), CpuLimitType::DevMode => Self { clock_min: Some(RangeLimit { min: Some(100), max: Some(5000) }), clock_max: Some(RangeLimit { min: Some(100), max: Some(4800) }), @@ -109,7 +110,7 @@ impl GenericCpuLimit { tdp_divisor: Some(1_000_000), tdp_step: Some(1), skip_resume_reclock: false, - experiments: true, + extras: Default::default(), }, _ => Self { clock_min: None, @@ -120,7 +121,7 @@ impl GenericCpuLimit { tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: Default::default(), }, } } @@ -141,7 +142,32 @@ impl GenericCpuLimit { tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: Default::default(), + } + } + + fn default_steam_deck_oled() -> Self { + Self { + clock_min: Some(RangeLimit { + min: Some(1400), + max: Some(3500), + }), + clock_max: Some(RangeLimit { + min: Some(400), + max: Some(3500), + }), + clock_step: Some(100), + tdp: None, + tdp_boost: None, + tdp_divisor: None, + tdp_step: None, + skip_resume_reclock: false, + extras: super::LimitExtras { + experiments: false, + quirks: vec![ + "clock-autodetect".to_owned(), + ].into_iter().collect() + }, } } @@ -165,6 +191,6 @@ impl GenericCpuLimit { } self.clock_step = limit_override.clock_step; self.skip_resume_reclock = limit_override.skip_resume_reclock; - self.experiments = limit_override.experiments; + self.extras = limit_override.extras; } } diff --git a/backend/limits_core/src/json_v2/gpu_limit.rs b/backend/limits_core/src/json_v2/gpu_limit.rs index f5a326a..d29ae6e 100644 --- a/backend/limits_core/src/json_v2/gpu_limit.rs +++ b/backend/limits_core/src/json_v2/gpu_limit.rs @@ -93,6 +93,7 @@ impl GenericGpuLimit { fn default_steam_deck_oled() -> Self { let mut sd = Self::default_steam_deck(); sd.memory_clock_step = Some(200); + sd.extras.quirks.insert("clock-autodetect".to_owned()); sd } diff --git a/backend/limits_srv/pt_limits_v2.json b/backend/limits_srv/pt_limits_v2.json index a0fc84e..9250956 100644 --- a/backend/limits_srv/pt_limits_v2.json +++ b/backend/limits_srv/pt_limits_v2.json @@ -35,7 +35,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -58,7 +61,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -81,7 +87,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -104,7 +113,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -127,7 +139,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -150,7 +165,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -173,7 +191,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -196,7 +217,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -219,7 +243,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -242,7 +269,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -265,7 +295,10 @@ "tdp_divisor": 1000000, "tdp_step": 1, "skip_resume_reclock": false, - "experiments": true + "extras": { + "experiments": false, + "quirks": [] + } } ], "global_governors": true, @@ -381,7 +414,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -398,7 +434,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -415,7 +454,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -432,7 +474,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -449,7 +494,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -466,7 +514,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -483,7 +534,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -500,7 +554,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } } ], "global_governors": true, @@ -547,8 +604,8 @@ "extras": { "experiments": false, "quirks": [ - "pp_dpm_fclk-reversed", - "pp_dpm_fclk-not-updated-on-LCD" + "pp_dpm_fclk-not-updated-on-LCD", + "pp_dpm_fclk-reversed" ] } } @@ -589,7 +646,7 @@ }, "limits": { "cpu": { - "provider": "GabeBoy", + "provider": "GabeBoySP", "limits": { "cpus": [ { @@ -607,7 +664,12 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [ + "clock-autodetect" + ] + } }, { "clock_min": { @@ -624,7 +686,12 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [ + "clock-autodetect" + ] + } }, { "clock_min": { @@ -641,7 +708,12 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [ + "clock-autodetect" + ] + } }, { "clock_min": { @@ -658,7 +730,12 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [ + "clock-autodetect" + ] + } }, { "clock_min": { @@ -675,7 +752,12 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [ + "clock-autodetect" + ] + } }, { "clock_min": { @@ -692,7 +774,12 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [ + "clock-autodetect" + ] + } }, { "clock_min": { @@ -709,7 +796,12 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [ + "clock-autodetect" + ] + } }, { "clock_min": { @@ -726,7 +818,12 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [ + "clock-autodetect" + ] + } } ], "global_governors": true, @@ -737,7 +834,7 @@ } }, "gpu": { - "provider": "GabeBoy", + "provider": "GabeBoySP", "limits": { "fast_ppt": { "min": 1000000, @@ -773,14 +870,15 @@ "extras": { "experiments": false, "quirks": [ + "pp_dpm_fclk-not-updated-on-LCD", "pp_dpm_fclk-reversed", - "pp_dpm_fclk-not-updated-on-LCD" + "clock-autodetect" ] } } }, "battery": { - "provider": "GabeBoy", + "provider": "GabeBoySP", "limits": { "charge_rate": { "min": 250, @@ -833,7 +931,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -850,7 +951,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -867,7 +971,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -884,7 +991,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } } ], "global_governors": true, @@ -975,7 +1085,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -992,7 +1105,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1009,7 +1125,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1026,7 +1145,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1043,7 +1165,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1060,7 +1185,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1077,7 +1205,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1094,7 +1225,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1111,7 +1245,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1128,7 +1265,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1145,7 +1285,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1162,7 +1305,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } } ], "global_governors": true, @@ -1253,7 +1399,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1270,7 +1419,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1287,7 +1439,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1304,7 +1459,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1321,7 +1479,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1338,7 +1499,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1355,7 +1519,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1372,7 +1539,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1389,7 +1559,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1406,7 +1579,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1423,7 +1599,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1440,7 +1619,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1457,7 +1639,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1474,7 +1659,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1491,7 +1679,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1508,7 +1699,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } } ], "global_governors": true, @@ -1599,7 +1793,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1616,7 +1813,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1633,7 +1833,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1650,7 +1853,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1667,7 +1873,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1684,7 +1893,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1701,7 +1913,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1718,7 +1933,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1735,7 +1953,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1752,7 +1973,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1769,7 +1993,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1786,7 +2013,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1803,7 +2033,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1820,7 +2053,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1837,7 +2073,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1854,7 +2093,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } } ], "global_governors": true, @@ -1945,7 +2187,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1962,7 +2207,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1979,7 +2227,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -1996,7 +2247,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2013,7 +2267,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2030,7 +2287,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2047,7 +2307,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2064,7 +2327,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2081,7 +2347,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2098,7 +2367,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2115,7 +2387,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2132,7 +2407,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2149,7 +2427,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2166,7 +2447,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2183,7 +2467,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": { @@ -2200,7 +2487,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } } ], "global_governors": true, @@ -2279,7 +2569,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": null, @@ -2290,7 +2583,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": null, @@ -2301,7 +2597,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": null, @@ -2312,7 +2611,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": null, @@ -2323,7 +2625,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": null, @@ -2334,7 +2639,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": null, @@ -2345,7 +2653,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } }, { "clock_min": null, @@ -2356,7 +2667,10 @@ "tdp_divisor": null, "tdp_step": null, "skip_resume_reclock": false, - "experiments": false + "extras": { + "experiments": false, + "quirks": [] + } } ], "global_governors": true, diff --git a/backend/src/settings/steam_deck/cpu.rs b/backend/src/settings/steam_deck/cpu.rs index de849fb..0ba44c5 100644 --- a/backend/src/settings/steam_deck/cpu.rs +++ b/backend/src/settings/steam_deck/cpu.rs @@ -102,6 +102,9 @@ impl Cpus { pub fn variant(mut self, model: super::Model) -> Self { self.variant = model; + for cpu in self.cpus.iter_mut() { + cpu.variant(model) + } self } } @@ -125,7 +128,12 @@ impl ProviderBuilder, GenericCpusLimit> for Cpus { } } let new_cpu = if let Some(cpu_limit) = limits.cpus.get(i) { - Cpu::from_json_and_limits(cpu, version, i, cpu_limit.to_owned()) + let mut cpu_limit_clone = cpu_limit.to_owned(); + for item in &limits.extras.quirks { + cpu_limit_clone.extras.quirks.insert(item.to_owned()); + } + cpu_limit_clone.extras.experiments |= limits.extras.experiments; + Cpu::from_json_and_limits(cpu, version, i, cpu_limit_clone) } else { Cpu::from_json(cpu, version, i) }; @@ -262,6 +270,7 @@ pub struct Cpu { index: usize, state: crate::state::steam_deck::Cpu, sysfs: BasicEntityPath, + variant: super::Model, } //const CPU_CLOCK_LIMITS_PATH: &str = "/sys/class/drm/card0/device/pp_od_clk_voltage"; @@ -289,6 +298,7 @@ impl Cpu { index: i, state: crate::state::steam_deck::Cpu::default(), sysfs: Self::find_card_sysfs(other.root), + variant: super::Model::LCD, }, _ => Self { online: other.online, @@ -298,6 +308,7 @@ impl Cpu { index: i, state: crate::state::steam_deck::Cpu::default(), sysfs: Self::find_card_sysfs(other.root), + variant: super::Model::LCD, }, } } @@ -341,6 +352,19 @@ impl Cpu { } } + fn read_max_cpu_clock(&self) -> u64 { + if !(self.limits.extras.experiments || self.limits.extras.quirks.contains("clock-autodetect")) { + return MAX_CLOCK; + } + if let super::Model::OLED = self.variant { + if let Ok(freq_khz) = usdpl_back::api::files::read_single::<_, u64, _>(cpu_max_clock_path(self.index)) { + log::debug!("Detected CPU max clock of {}KHz", freq_khz); + return freq_khz / 1000 + } + } + MAX_CLOCK + } + fn set_clock_limit( &self, index: usize, @@ -591,6 +615,7 @@ impl Cpu { index: cpu_index, state: crate::state::steam_deck::Cpu::default(), sysfs: Self::find_card_sysfs(None::<&'static str>), + variant: super::Model::LCD, } } @@ -602,14 +627,15 @@ impl Cpu { } fn limits(&self) -> crate::api::CpuLimits { + let max_cpu_clock = self.read_max_cpu_clock(); crate::api::CpuLimits { clock_min_limits: Some(RangeLimit { min: range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), // allows min to be set by max (it's weird, blame the kernel) - max: range_max_or_fallback(&self.limits.clock_min, MAX_CLOCK), + max: range_max_or_fallback(&self.limits.clock_min, max_cpu_clock), }), clock_max_limits: Some(RangeLimit { min: range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), - max: range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), + max: range_max_or_fallback(&self.limits.clock_max, max_cpu_clock), }), clock_step: self.limits.clock_step.unwrap_or(CLOCK_STEP), governors: self.governors(), @@ -628,6 +654,10 @@ impl Cpu { }; gov_str.split(' ').map(|s| s.to_owned()).collect() } + + pub fn variant(&mut self, model: super::Model) { + self.variant = model; + } } impl Into for Cpu { @@ -714,3 +744,8 @@ fn cpu_available_governors_path(index: usize) -> String { index ) } + +#[inline] +fn cpu_max_clock_path(index: usize) -> String { + format!("/sys/devices/system/cpu/cpufreq/policy{}/cpuinfo_max_freq", index) +} diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index 562923b..f638f10 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -37,6 +37,7 @@ pub struct Gpu { const GPU_CLOCK_LIMITS_ATTRIBUTE: &str = "device/pp_od_clk_voltage"; const GPU_MEMORY_DOWNCLOCK_ATTRIBUTE: &str = "device/pp_dpm_fclk"; +const GPU_CLOCK_READOUT_ATTRIBUTE: &str = "device/pp_dpm_sclk"; const CARD_EXTENSIONS: &[&'static str] = &[ GPU_CLOCK_LIMITS_ATTRIBUTE, @@ -141,6 +142,29 @@ impl Gpu { }) } + fn read_max_gpu_clock(&self) -> u64 { + if !(self.limits.extras.experiments || self.limits.extras.quirks.contains("clock-autodetect")) { + return MAX_CLOCK; + } + if let super::Model::OLED = self.variant { + if let Ok(f) = self + .sysfs_card + .read_value(GPU_CLOCK_READOUT_ATTRIBUTE.to_owned()) + { + let options = parse_pp_dpm_sclk(&String::from_utf8_lossy(&f)); + return options.get(options.len() - 1) + .map(|x| { + let x = x.1 as u64; + log::debug!("Detected GPU max clock of {}MHz", x); + x + + }) + .unwrap_or(MAX_CLOCK); + } + } + MAX_CLOCK + } + fn is_memory_clock_maxed(&self) -> bool { if let Some(clock) = &self.memory_clock { if let Some(limit) = &self.limits.memory_clock { @@ -611,6 +635,7 @@ impl crate::settings::OnUnload for Gpu { impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { + let max_gpu_clock = self.read_max_gpu_clock(); crate::api::GpuLimits { fast_ppt_limits: Some(RangeLimit { min: super::util::range_min_or_fallback(&self.limits.fast_ppt, MIN_FAST_PPT) @@ -630,11 +655,11 @@ impl TGpu for Gpu { tdp_step: 42, clock_min_limits: Some(RangeLimit { min: super::util::range_min_or_fallback(&self.limits.clock_min, MIN_CLOCK), - max: super::util::range_max_or_fallback(&self.limits.clock_min, MAX_CLOCK), + max: super::util::range_max_or_fallback(&self.limits.clock_min, max_gpu_clock), }), clock_max_limits: Some(RangeLimit { min: super::util::range_min_or_fallback(&self.limits.clock_max, MIN_CLOCK), - max: super::util::range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), + max: super::util::range_max_or_fallback(&self.limits.clock_max, max_gpu_clock), }), clock_step: self.limits.clock_step.unwrap_or(100), memory_control: Some(RangeLimit { @@ -693,7 +718,7 @@ impl TGpu for Gpu { } } -fn parse_pp_dpm_fclk(s: &str) -> Vec<(usize, usize)> { +fn parse_sysfs_clk_selector_str(s: &str) -> Vec<(usize, usize)> { // (value, MHz) let mut result = Vec::new(); for line in s.split('\n') { @@ -715,3 +740,13 @@ fn parse_pp_dpm_fclk(s: &str) -> Vec<(usize, usize)> { } result } + +#[inline] +fn parse_pp_dpm_fclk(s: &str) -> Vec<(usize, usize)> { + parse_sysfs_clk_selector_str(s) +} + +#[inline] +fn parse_pp_dpm_sclk(s: &str) -> Vec<(usize, usize)> { + parse_sysfs_clk_selector_str(s) +} diff --git a/package.json b/package.json index 04f659b..46ce370 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "PowerTools", - "version": "2.0.0-alpha2", + "version": "2.0.0-alpha3", "description": "Power tweaks for power users", "scripts": { "build": "shx rm -rf dist && rollup -c", From 1da7bafe5e3ed2d13c9dcb17412ee46c48a414e7 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 10 Feb 2024 13:03:04 -0500 Subject: [PATCH 44/56] Wait for a while to avoid DNS issues when fetching new limits during startup --- backend/src/consts.rs | 2 ++ backend/src/settings/detect/limits_worker.rs | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/consts.rs b/backend/src/consts.rs index 01707e0..72c1c4b 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -8,6 +8,8 @@ pub const DEFAULT_SETTINGS_NAME: &str = "Main"; pub const DEFAULT_SETTINGS_VARIANT_NAME: &str = "Primary"; pub const LIMITS_FILE: &str = "limits_cache.ron"; +pub const LIMITS_REFRESH_PERIOD: std::time::Duration = std::time::Duration::from_secs(60 * 60 * 24); // 1 day +pub const LIMITS_STARTUP_WAIT: std::time::Duration = std::time::Duration::from_secs(60); // 1 minute pub const LIMITS_OVERRIDE_FILE: &str = "limits_override.ron"; pub const WEB_SETTINGS_CACHE: &str = "store_cache.ron"; diff --git a/backend/src/settings/detect/limits_worker.rs b/backend/src/settings/detect/limits_worker.rs index 4bdad39..f211cc8 100644 --- a/backend/src/settings/detect/limits_worker.rs +++ b/backend/src/settings/detect/limits_worker.rs @@ -1,6 +1,4 @@ use std::thread::{self, JoinHandle}; -#[cfg(feature = "online")] -use std::time::Duration; use limits_core::json_v2::Base; @@ -8,8 +6,9 @@ use limits_core::json_v2::Base; pub fn spawn() -> JoinHandle<()> { thread::spawn(move || { log::info!("limits_worker starting..."); - let sleep_dur = Duration::from_secs(60 * 60 * 24); // 1 day let limits_path = super::utility::limits_path(); + thread::sleep(crate::consts::LIMITS_STARTUP_WAIT); + log::info!("limits_worker completed startup wait"); loop { if (limits_path.exists() && limits_path.is_file()) || !limits_path.exists() { // try to load limits from file, fallback to built-in default @@ -56,7 +55,7 @@ pub fn spawn() -> JoinHandle<()> { } else if !limits_path.is_file() { log::error!("Path for storing limits is not a file!"); } - thread::sleep(sleep_dur); + thread::sleep(crate::consts::LIMITS_REFRESH_PERIOD); } log::warn!("limits_worker completed!"); }) From fb038665c9bd89e9878ef515e99ee35757488d70 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 10 Feb 2024 15:02:08 -0500 Subject: [PATCH 45/56] Update limits_override reference --- backend/src/settings/steam_deck/cpu.rs | 15 ++++++--- backend/src/settings/steam_deck/gpu.rs | 10 +++--- limits_override.ron | 42 ++++++++++++++++++++------ 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/backend/src/settings/steam_deck/cpu.rs b/backend/src/settings/steam_deck/cpu.rs index 0ba44c5..e76df46 100644 --- a/backend/src/settings/steam_deck/cpu.rs +++ b/backend/src/settings/steam_deck/cpu.rs @@ -353,13 +353,17 @@ impl Cpu { } fn read_max_cpu_clock(&self) -> u64 { - if !(self.limits.extras.experiments || self.limits.extras.quirks.contains("clock-autodetect")) { + if !(self.limits.extras.experiments + || self.limits.extras.quirks.contains("clock-autodetect")) + { return MAX_CLOCK; } if let super::Model::OLED = self.variant { - if let Ok(freq_khz) = usdpl_back::api::files::read_single::<_, u64, _>(cpu_max_clock_path(self.index)) { + if let Ok(freq_khz) = + usdpl_back::api::files::read_single::<_, u64, _>(cpu_max_clock_path(self.index)) + { log::debug!("Detected CPU max clock of {}KHz", freq_khz); - return freq_khz / 1000 + return freq_khz / 1000; } } MAX_CLOCK @@ -747,5 +751,8 @@ fn cpu_available_governors_path(index: usize) -> String { #[inline] fn cpu_max_clock_path(index: usize) -> String { - format!("/sys/devices/system/cpu/cpufreq/policy{}/cpuinfo_max_freq", index) + format!( + "/sys/devices/system/cpu/cpufreq/policy{}/cpuinfo_max_freq", + index + ) } diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index f638f10..46a7c00 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -143,7 +143,9 @@ impl Gpu { } fn read_max_gpu_clock(&self) -> u64 { - if !(self.limits.extras.experiments || self.limits.extras.quirks.contains("clock-autodetect")) { + if !(self.limits.extras.experiments + || self.limits.extras.quirks.contains("clock-autodetect")) + { return MAX_CLOCK; } if let super::Model::OLED = self.variant { @@ -152,12 +154,12 @@ impl Gpu { .read_value(GPU_CLOCK_READOUT_ATTRIBUTE.to_owned()) { let options = parse_pp_dpm_sclk(&String::from_utf8_lossy(&f)); - return options.get(options.len() - 1) + return options + .get(options.len() - 1) .map(|x| { let x = x.1 as u64; log::debug!("Detected GPU max clock of {}MHz", x); x - }) .unwrap_or(MAX_CLOCK); } @@ -635,7 +637,7 @@ impl crate::settings::OnUnload for Gpu { impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { - let max_gpu_clock = self.read_max_gpu_clock(); + let max_gpu_clock = self.read_max_gpu_clock(); crate::api::GpuLimits { fast_ppt_limits: Some(RangeLimit { min: super::util::range_min_or_fallback(&self.limits.fast_ppt, MIN_FAST_PPT) diff --git a/limits_override.ron b/limits_override.ron index e963283..c97c3b8 100644 --- a/limits_override.ron +++ b/limits_override.ron @@ -17,7 +17,10 @@ Limits( tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: LimitExtras( + experiments: false, + quirks: [], +), ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -33,7 +36,10 @@ Limits( tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: LimitExtras( + experiments: false, + quirks: [], +), ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -49,7 +55,10 @@ Limits( tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: LimitExtras( + experiments: false, + quirks: [], +), ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -65,7 +74,10 @@ Limits( tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: LimitExtras( + experiments: false, + quirks: [], +), ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -81,7 +93,10 @@ Limits( tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: LimitExtras( + experiments: false, + quirks: [], +), ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -97,7 +112,10 @@ Limits( tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: LimitExtras( + experiments: false, + quirks: [], +), ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -113,7 +131,10 @@ Limits( tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: LimitExtras( + experiments: false, + quirks: [], +), ), GenericCpuLimit( clock_min: Some(RangeLimit( min: Some(1400), @@ -129,7 +150,10 @@ Limits( tdp_divisor: None, tdp_step: None, skip_resume_reclock: false, - experiments: false, + extras: LimitExtras( + experiments: false, + quirks: [], +), )], global_governors: true, extras: LimitExtras( @@ -174,7 +198,7 @@ Limits( skip_resume_reclock: false, extras: LimitExtras( experiments: false, - quirks: ["pp_dpm_fclk-not-updated-on-LCD", "pp_dpm_fclk-reversed"], + quirks: ["pp_dpm_fclk-reversed", "pp_dpm_fclk-not-updated-on-LCD"], ), ), ), From 808ce76eee5fb112e3f887ff0ec0a8e16799a96a Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 17 Feb 2024 09:37:10 -0500 Subject: [PATCH 46/56] Implement ppt default (when enabled) value in UI --- backend/src/api/api_types.rs | 2 ++ backend/src/settings/dev_mode/gpu.rs | 8 ++++++-- backend/src/settings/generic/gpu.rs | 16 ++++++++++++++++ backend/src/settings/generic_amd/gpu.rs | 2 ++ backend/src/settings/steam_deck/gpu.rs | 11 +++++++---- backend/src/settings/unknown/gpu.rs | 2 ++ src/backend.ts | 2 ++ src/components/gpu.tsx | 4 ++-- 8 files changed, 39 insertions(+), 8 deletions(-) diff --git a/backend/src/api/api_types.rs b/backend/src/api/api_types.rs index 7d8e6bf..662b899 100644 --- a/backend/src/api/api_types.rs +++ b/backend/src/api/api_types.rs @@ -51,7 +51,9 @@ pub struct GeneralLimits {} #[derive(Serialize, Deserialize)] pub struct GpuLimits { pub fast_ppt_limits: Option>, + pub fast_ppt_default: u64, pub slow_ppt_limits: Option>, + pub slow_ppt_default: u64, pub ppt_step: u64, pub tdp_limits: Option>, pub tdp_boost_limits: Option>, diff --git a/backend/src/settings/dev_mode/gpu.rs b/backend/src/settings/dev_mode/gpu.rs index c6605a2..f8b8026 100644 --- a/backend/src/settings/dev_mode/gpu.rs +++ b/backend/src/settings/dev_mode/gpu.rs @@ -99,15 +99,17 @@ impl TGpu for Gpu { log::debug!("dev_mode_Gpu::limits(self) -> {{...}}"); let ppt_divisor = self.limits.ppt_divisor.unwrap_or(1_000_000); let tdp_divisor = self.limits.tdp_divisor.unwrap_or(1_000_000); - crate::api::GpuLimits { + let limit_struct = crate::api::GpuLimits { fast_ppt_limits: self.limits.fast_ppt.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(11_000_000) / ppt_divisor, max: lim.max.unwrap_or(42_000_000) / ppt_divisor, }), + fast_ppt_default: self.limits.fast_ppt_default.or_else(|| self.limits.fast_ppt.and_then(|x| x.max)).unwrap_or(2_000_000) / ppt_divisor, slow_ppt_limits: self.limits.slow_ppt.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(7_000_000) / ppt_divisor, max: lim.max.unwrap_or(69_000_000) / ppt_divisor, }), + slow_ppt_default: self.limits.slow_ppt_default.or_else(|| self.limits.slow_ppt.and_then(|x| x.max)).unwrap_or(3_000_000) / ppt_divisor, ppt_step: self.limits.ppt_step.unwrap_or(1), tdp_limits: self.limits.tdp.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(11_000_000) / tdp_divisor, @@ -132,7 +134,9 @@ impl TGpu for Gpu { max: lim.max.unwrap_or(1100), }), memory_step: self.limits.memory_clock_step.unwrap_or(400), - } + }; + log::debug!("dev_mode_Gpu::limits(self) -> {}", serde_json::to_string_pretty(&limit_struct).unwrap()); + limit_struct } fn json(&self) -> crate::persist::GpuJson { diff --git a/backend/src/settings/generic/gpu.rs b/backend/src/settings/generic/gpu.rs index a96bae8..80e19e6 100644 --- a/backend/src/settings/generic/gpu.rs +++ b/backend/src/settings/generic/gpu.rs @@ -167,6 +167,14 @@ impl TGpu for Gpu { x } }), + fast_ppt_default: { + let def = self.limits.fast_ppt_default.or_else(|| self.limits.fast_ppt.and_then(|x| x.max)).unwrap_or(15); + if let Some(ppt_divisor) = self.limits.ppt_divisor { + def / ppt_divisor + } else { + def + } + }, slow_ppt_limits: self .limits .slow_ppt @@ -181,6 +189,14 @@ impl TGpu for Gpu { x } }), + slow_ppt_default: { + let def = self.limits.slow_ppt_default.or_else(|| self.limits.slow_ppt.and_then(|x| x.max)).unwrap_or(15); + if let Some(ppt_divisor) = self.limits.ppt_divisor { + def / ppt_divisor + } else { + def + } + }, ppt_step: self.limits.ppt_step.unwrap_or(1), tdp_limits: self .limits diff --git a/backend/src/settings/generic_amd/gpu.rs b/backend/src/settings/generic_amd/gpu.rs index 265956c..983bd29 100644 --- a/backend/src/settings/generic_amd/gpu.rs +++ b/backend/src/settings/generic_amd/gpu.rs @@ -327,7 +327,9 @@ impl crate::settings::OnUnload for Gpu { fn bad_gpu_limits() -> crate::api::GpuLimits { crate::api::GpuLimits { fast_ppt_limits: None, + fast_ppt_default: 1, slow_ppt_limits: None, + slow_ppt_default: 1, ppt_step: 1, tdp_limits: None, tdp_boost_limits: None, diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index 46a7c00..c7ca486 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -638,19 +638,22 @@ impl crate::settings::OnUnload for Gpu { impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { let max_gpu_clock = self.read_max_gpu_clock(); + let ppt_divisor = self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR); crate::api::GpuLimits { fast_ppt_limits: Some(RangeLimit { min: super::util::range_min_or_fallback(&self.limits.fast_ppt, MIN_FAST_PPT) - / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + / ppt_divisor, max: super::util::range_max_or_fallback(&self.limits.fast_ppt, MAX_FAST_PPT) - / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + / ppt_divisor, }), + fast_ppt_default: self.limits.fast_ppt_default.or_else(|| self.limits.fast_ppt.and_then(|x| x.max)).unwrap_or(MAX_FAST_PPT) / ppt_divisor, slow_ppt_limits: Some(RangeLimit { min: super::util::range_min_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) - / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + / ppt_divisor, max: super::util::range_max_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) - / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + / ppt_divisor, }), + slow_ppt_default: self.limits.slow_ppt_default.or_else(|| self.limits.slow_ppt.and_then(|x| x.max)).unwrap_or(MAX_SLOW_PPT) / ppt_divisor, ppt_step: self.limits.ppt_step.unwrap_or(1), tdp_limits: None, tdp_boost_limits: None, diff --git a/backend/src/settings/unknown/gpu.rs b/backend/src/settings/unknown/gpu.rs index d7c4826..90c1279 100644 --- a/backend/src/settings/unknown/gpu.rs +++ b/backend/src/settings/unknown/gpu.rs @@ -71,7 +71,9 @@ impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { crate::api::GpuLimits { fast_ppt_limits: None, + fast_ppt_default: 1_000_000, slow_ppt_limits: None, + slow_ppt_default: 1_000_000, ppt_step: 1_000_000, tdp_limits: None, tdp_boost_limits: None, diff --git a/src/backend.ts b/src/backend.ts index bff625a..9d55f77 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -85,7 +85,9 @@ export type GeneralLimits = {}; export type GpuLimits = { fast_ppt_limits: RangeLimit | null; + fast_ppt_default: number; slow_ppt_limits: RangeLimit | null; + slow_ppt_default: number; ppt_step: number; clock_min_limits: RangeLimit | null; clock_max_limits: RangeLimit | null; diff --git a/src/components/gpu.tsx b/src/components/gpu.tsx index fb8dfc6..fce7670 100644 --- a/src/components/gpu.tsx +++ b/src/components/gpu.tsx @@ -41,11 +41,11 @@ export class Gpu extends Component { onChange={(value: boolean) => { if (value) { if ((get_value(LIMITS_INFO) as backend.SettingsLimits).gpu.slow_ppt_limits != null) { - set_value(SLOW_PPT_GPU, (get_value(LIMITS_INFO) as backend.SettingsLimits).gpu.slow_ppt_limits!.max); + set_value(SLOW_PPT_GPU, (get_value(LIMITS_INFO) as backend.SettingsLimits).gpu.slow_ppt_default); } if ((get_value(LIMITS_INFO) as backend.SettingsLimits).gpu.fast_ppt_limits != null) { - set_value(FAST_PPT_GPU, (get_value(LIMITS_INFO) as backend.SettingsLimits).gpu.fast_ppt_limits!.max); + set_value(FAST_PPT_GPU, (get_value(LIMITS_INFO) as backend.SettingsLimits).gpu.fast_ppt_default); } reloadGUI("GPUPPTToggle"); } else { From af8e8f52584d26a9dc20c62feb8ff822841b8a2e Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 17 Feb 2024 09:44:42 -0500 Subject: [PATCH 47/56] Only set Steam Deck memory speed in dpm force perf manual mode --- backend/src/settings/dev_mode/gpu.rs | 19 +++++++++-- backend/src/settings/generic/gpu.rs | 12 +++++-- backend/src/settings/steam_deck/gpu.rs | 47 +++++++++++++++++--------- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/backend/src/settings/dev_mode/gpu.rs b/backend/src/settings/dev_mode/gpu.rs index f8b8026..42963a7 100644 --- a/backend/src/settings/dev_mode/gpu.rs +++ b/backend/src/settings/dev_mode/gpu.rs @@ -104,12 +104,22 @@ impl TGpu for Gpu { min: lim.min.unwrap_or(11_000_000) / ppt_divisor, max: lim.max.unwrap_or(42_000_000) / ppt_divisor, }), - fast_ppt_default: self.limits.fast_ppt_default.or_else(|| self.limits.fast_ppt.and_then(|x| x.max)).unwrap_or(2_000_000) / ppt_divisor, + fast_ppt_default: self + .limits + .fast_ppt_default + .or_else(|| self.limits.fast_ppt.and_then(|x| x.max)) + .unwrap_or(2_000_000) + / ppt_divisor, slow_ppt_limits: self.limits.slow_ppt.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(7_000_000) / ppt_divisor, max: lim.max.unwrap_or(69_000_000) / ppt_divisor, }), - slow_ppt_default: self.limits.slow_ppt_default.or_else(|| self.limits.slow_ppt.and_then(|x| x.max)).unwrap_or(3_000_000) / ppt_divisor, + slow_ppt_default: self + .limits + .slow_ppt_default + .or_else(|| self.limits.slow_ppt.and_then(|x| x.max)) + .unwrap_or(3_000_000) + / ppt_divisor, ppt_step: self.limits.ppt_step.unwrap_or(1), tdp_limits: self.limits.tdp.map(|lim| crate::api::RangeLimit { min: lim.min.unwrap_or(11_000_000) / tdp_divisor, @@ -135,7 +145,10 @@ impl TGpu for Gpu { }), memory_step: self.limits.memory_clock_step.unwrap_or(400), }; - log::debug!("dev_mode_Gpu::limits(self) -> {}", serde_json::to_string_pretty(&limit_struct).unwrap()); + log::debug!( + "dev_mode_Gpu::limits(self) -> {}", + serde_json::to_string_pretty(&limit_struct).unwrap() + ); limit_struct } diff --git a/backend/src/settings/generic/gpu.rs b/backend/src/settings/generic/gpu.rs index 80e19e6..58b70bc 100644 --- a/backend/src/settings/generic/gpu.rs +++ b/backend/src/settings/generic/gpu.rs @@ -168,7 +168,11 @@ impl TGpu for Gpu { } }), fast_ppt_default: { - let def = self.limits.fast_ppt_default.or_else(|| self.limits.fast_ppt.and_then(|x| x.max)).unwrap_or(15); + let def = self + .limits + .fast_ppt_default + .or_else(|| self.limits.fast_ppt.and_then(|x| x.max)) + .unwrap_or(15); if let Some(ppt_divisor) = self.limits.ppt_divisor { def / ppt_divisor } else { @@ -190,7 +194,11 @@ impl TGpu for Gpu { } }), slow_ppt_default: { - let def = self.limits.slow_ppt_default.or_else(|| self.limits.slow_ppt.and_then(|x| x.max)).unwrap_or(15); + let def = self + .limits + .slow_ppt_default + .or_else(|| self.limits.slow_ppt.and_then(|x| x.max)) + .unwrap_or(15); if let Some(ppt_divisor) = self.limits.ppt_divisor { def / ppt_divisor } else { diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index c7ca486..2899e1b 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -340,19 +340,23 @@ impl Gpu { } fn set_memory_speed(&self, clock: u64) -> Result<(), SettingError> { - let path = GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.path(&self.sysfs_card); - let payload = self.build_memory_clock_payload(clock); - log::debug!( - "Generated payload for gpu fclk (memory): `{}` (is maxed? {})", - payload, - self.is_memory_clock_maxed() - ); - self.sysfs_card - .set(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned(), payload) - .map_err(|e| SettingError { - msg: format!("Failed to write to `{}`: {}", path.display(), e), - setting: crate::settings::SettingVariant::Gpu, - }) + if POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.needs_manual() { + let path = GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.path(&self.sysfs_card); + let payload = self.build_memory_clock_payload(clock); + log::debug!( + "Generated payload for gpu fclk (memory): `{}` (is maxed? {})", + payload, + self.is_memory_clock_maxed() + ); + self.sysfs_card + .set(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned(), payload) + .map_err(|e| SettingError { + msg: format!("Failed to write to `{}`: {}", path.display(), e), + setting: crate::settings::SettingVariant::Gpu, + }) + } else { + Ok(()) + } } fn set_force_performance_related(&mut self) -> Result<(), Vec> { @@ -362,7 +366,7 @@ impl Gpu { POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT .enforce_level(&self.sysfs_card) .unwrap_or_else(|mut e| errors.append(&mut e)); - // enable/disable downclock of GPU memory (to 400Mhz?) + // enable/disable downclock of GPU memory self.set_memory_speed( self.memory_clock .or_else(|| { @@ -631,6 +635,7 @@ impl crate::settings::OnLoad for Gpu { impl crate::settings::OnUnload for Gpu { fn on_unload(&mut self) -> Result<(), Vec> { + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.set_gpu(false); Ok(()) } } @@ -646,14 +651,24 @@ impl TGpu for Gpu { max: super::util::range_max_or_fallback(&self.limits.fast_ppt, MAX_FAST_PPT) / ppt_divisor, }), - fast_ppt_default: self.limits.fast_ppt_default.or_else(|| self.limits.fast_ppt.and_then(|x| x.max)).unwrap_or(MAX_FAST_PPT) / ppt_divisor, + fast_ppt_default: self + .limits + .fast_ppt_default + .or_else(|| self.limits.fast_ppt.and_then(|x| x.max)) + .unwrap_or(MAX_FAST_PPT) + / ppt_divisor, slow_ppt_limits: Some(RangeLimit { min: super::util::range_min_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) / ppt_divisor, max: super::util::range_max_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) / ppt_divisor, }), - slow_ppt_default: self.limits.slow_ppt_default.or_else(|| self.limits.slow_ppt.and_then(|x| x.max)).unwrap_or(MAX_SLOW_PPT) / ppt_divisor, + slow_ppt_default: self + .limits + .slow_ppt_default + .or_else(|| self.limits.slow_ppt.and_then(|x| x.max)) + .unwrap_or(MAX_SLOW_PPT) + / ppt_divisor, ppt_step: self.limits.ppt_step.unwrap_or(1), tdp_limits: None, tdp_boost_limits: None, From a5e4ce29a6e500173f52e9ae4da9dfd93a926db2 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 24 Feb 2024 22:00:09 -0500 Subject: [PATCH 48/56] Fix store display --- backend/Cargo.lock | 63 ++++++++++++++++++- backend/Cargo.toml | 4 +- backend/src/api/web.rs | 10 ++- package.json | 2 +- src/index.tsx | 5 +- src/store/page.tsx | 137 ++++++++++++++++++++++++++++++----------- 6 files changed, 177 insertions(+), 44 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index a2f06f9..dc737ff 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "powertools" -version = "2.0.0-alpha3" +version = "2.0.0-alpha4" dependencies = [ "async-trait", "chrono", @@ -1295,6 +1295,20 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "ron" version = "0.8.1" @@ -1332,6 +1346,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -1341,6 +1367,16 @@ dependencies = [ "base64 0.21.5", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.16" @@ -1353,6 +1389,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "serde" version = "1.0.193" @@ -1738,6 +1784,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "ureq" version = "2.9.1" @@ -1750,9 +1802,12 @@ dependencies = [ "flate2", "log", "once_cell", + "rustls", + "rustls-webpki", "serde", "serde_json", "url", + "webpki-roots", ] [[package]] @@ -1912,6 +1967,12 @@ version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + [[package]] name = "which" version = "4.4.2" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index ef0df3b..fcbe28b 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "powertools" -version = "2.0.0-alpha3" +version = "2.0.0-alpha4" edition = "2021" authors = ["NGnius (Graham) "] description = "Backend (superuser) functionality for PowerTools" @@ -43,7 +43,7 @@ chrono = { version = "0.4", features = [ "serde" ] } libryzenadj = { version = "0.13" } # ureq's tls feature does not like musl targets -ureq = { version = "2", features = ["json", "gzip", "brotli", "charset"], default-features = false, optional = true } +ureq = { version = "2", features = ["json", "gzip", "brotli", "charset", "tls"], default-features = false, optional = true } clap = { version = "4.4", features = [ "derive" ] } diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index c5f2d2f..b395e85 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -91,7 +91,7 @@ fn save_cache(cache: &StoreCache) { fn get_maybe_cached(steam_app_id: u32) -> Vec { let mut cache = load_cache(); - if let Some(cached_result) = cache.get(&steam_app_id) { + let data = if let Some(cached_result) = cache.get(&steam_app_id) { if cached_result.updated < (Utc::now() - MAX_CACHE_DURATION) { // cache needs update if let Ok(result) = search_by_app_id_online(steam_app_id) { @@ -126,6 +126,14 @@ fn get_maybe_cached(steam_app_id: u32) -> Vec = ({}) => { backend.resolve( backend.loadGeneralSettingsVariant("please give me a new ID k thx bye" /* anything that cannot be parsed as a u64 will be set to u64::MAX, which will cause the back-end to auto-generate an ID */, name), (ok: boolean) => { - isVariantLoading = false; backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok); reload(); backend.resolve(backend.waitForComplete(), (_) => { @@ -539,7 +538,7 @@ export default definePlugin((serverApi: ServerAPI) => { ico = ; } //registerCallbacks(false); - serverApi.routerHook.addRoute(STORE_RESULTS_URI, StoreResultsPage); + serverApi.routerHook.addRoute(STORE_RESULTS_URI, () => ); return { title:
PowerTools
, content: , @@ -548,7 +547,7 @@ export default definePlugin((serverApi: ServerAPI) => { tryNotifyProfileChange = function() {}; backend.log(backend.LogLevel.Debug, "PowerTools shutting down"); clearHooks(); - //serverApi.routerHook.removeRoute("/decky-plugin-test"); + serverApi.routerHook.removeRoute(STORE_RESULTS_URI); }, }; }); diff --git a/src/store/page.tsx b/src/store/page.tsx index a3269e1..da8f36d 100644 --- a/src/store/page.tsx +++ b/src/store/page.tsx @@ -1,19 +1,20 @@ import { Component } from "react"; import { HiDownload } from "react-icons/hi"; -import { PanelSectionRow, PanelSection, Focusable, staticClasses, DialogButton } from "decky-frontend-lib"; +import { PanelSectionRow, Focusable, staticClasses, DialogButton } from "decky-frontend-lib"; import * as backend from "../backend"; import { tr } from "usdpl-front"; -import { get_value} from "usdpl-front"; +import { get_value, set_value } from "usdpl-front"; import { STORE_RESULTS, + VARIANTS_GEN, } from "../consts"; -export class StoreResultsPage extends Component { - constructor() { - super({}); +export class StoreResultsPage extends Component<{onNewVariant: () => void}> { + constructor(props: {onNewVariant: () => void}) { + super(props); this.state = { reloadThingy: "/shrug", }; @@ -25,49 +26,60 @@ export class StoreResultsPage extends Component { if (storeItems) { if (storeItems.length == 0) { backend.log(backend.LogLevel.Warn, "No store results; got array with length 0 from cache"); - return ( + return ( { tr("No results") /* TODO translate */ } - ); + ); } else { // TODO - return ( + return ( { - storeItems.map((meta: backend.StoreMetadata) => { - + storeItems.map((meta: backend.StoreMetadata) => ( - { - backend.log(backend.LogLevel.Info, "Downloading settings " + meta.name + " (" + meta.id + ")"); - backend.storeDownloadById(meta.id); - }} - > - { /* TODO make this responsive when clicked */} - -
{ meta.name } @@ -76,33 +88,86 @@ export class StoreResultsPage extends Component { display: "flex", flexDirection: "row", minWidth: "0", - fontSize: "12px", + fontSize: "20px", + padding: "0.25em", }} className={staticClasses.Text}> { tr("Created by") /* TODO translate */} { meta.steam_username }
-
+
{ meta.tags.map((tag: string) => ( {tag} ) ) }
+ { + backend.log(backend.LogLevel.Info, "Downloading settings " + meta.name + " (" + meta.id + ")"); + backend.resolve(backend.storeDownloadById(meta.id), + (variants: backend.VariantInfo[]) => { + set_value(VARIANTS_GEN, variants) + this.props.onNewVariant(); + } + ); + }} + > + { /* TODO make this responsive when clicked */} + + - - }) + )) } - ); + ); } } else { backend.log(backend.LogLevel.Warn, "Store failed to load; got null from cache"); // store did not pre-load when the game started - return ( + return ( { tr("Store failed to load") /* TODO translate */ } - ); + ); } } } From 479cbf22aa7a0a25017a4634378fc766819a6a8b Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 24 Feb 2024 22:52:13 -0500 Subject: [PATCH 49/56] Fix compilation without online feature --- backend/src/api/web.rs | 45 +++++++++++++++++++++++++++++++++++++++++- backend/src/consts.rs | 3 +++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index b395e85..acd467c 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -1,5 +1,7 @@ use std::sync::mpsc::{self, Sender}; -use std::sync::{Arc, Mutex, RwLock}; +#[cfg(feature = "online")] +use std::sync::RwLock; +use std::sync::{Arc, Mutex}; use usdpl_back::core::serdes::Primitive; use usdpl_back::AsyncCallable; @@ -8,7 +10,9 @@ use serde::{Deserialize, Serialize}; use super::handler::{ApiMessage, GeneralMessage}; +#[cfg(feature = "online")] const BASE_URL_FALLBACK: &'static str = "https://powertools.ngni.us"; +#[cfg(feature = "online")] static BASE_URL: RwLock> = RwLock::new(None); const MAX_CACHE_DURATION: std::time::Duration = @@ -23,12 +27,14 @@ struct CachedData { type StoreCache = std::collections::HashMap>>; +#[cfg(feature = "online")] pub fn set_base_url(base_url: String) { *BASE_URL .write() .expect("Failed to acquire write lock for store base url") = Some(base_url); } +#[cfg(feature = "online")] fn get_base_url() -> String { BASE_URL .read() @@ -37,22 +43,27 @@ fn get_base_url() -> String { .unwrap_or_else(|| BASE_URL_FALLBACK.to_owned()) } +#[cfg(feature = "online")] fn url_search_by_app_id(steam_app_id: u32) -> String { format!("{}/api/setting/by_app_id/{}", get_base_url(), steam_app_id) } +#[cfg(feature = "online")] fn url_download_config_by_id(id: u128) -> String { format!("{}/api/setting/by_id/{}", get_base_url(), id) } +#[cfg(feature = "online")] fn url_upload_config() -> String { format!("{}/api/setting", get_base_url()) } +#[cfg(feature = "online")] fn cache_path() -> std::path::PathBuf { crate::utility::settings_dir().join(crate::consts::WEB_SETTINGS_CACHE) } +#[cfg(feature = "online")] fn load_cache() -> StoreCache { let path = cache_path(); let file = match std::fs::File::open(&path) { @@ -72,6 +83,12 @@ fn load_cache() -> StoreCache { } } +#[cfg(not(feature = "online"))] +fn load_cache() -> StoreCache { + StoreCache::default() +} + +#[cfg(feature = "online")] fn save_cache(cache: &StoreCache) { let path = cache_path(); let file = match std::fs::File::create(&path) { @@ -89,6 +106,9 @@ fn save_cache(cache: &StoreCache) { } } +#[cfg(not(feature = "online"))] +fn save_cache(_cache: &StoreCache) {} + fn get_maybe_cached(steam_app_id: u32) -> Vec { let mut cache = load_cache(); let data = if let Some(cached_result) = cache.get(&steam_app_id) { @@ -137,6 +157,7 @@ fn get_maybe_cached(steam_app_id: u32) -> Vec std::io::Result> { @@ -163,6 +184,13 @@ fn search_by_app_id_online( } } +#[cfg(not(feature = "online"))] +fn search_by_app_id_online( + _steam_app_id: u32, +) -> std::io::Result> { + Ok(Vec::with_capacity(0)) +} + /// Get search results web method pub fn search_by_app_id() -> impl AsyncCallable { let getter = move || { @@ -248,6 +276,7 @@ fn web_config_to_settings_json( } } +#[cfg(feature = "online")] fn download_config(id: u128) -> std::io::Result { let req_url = url_download_config_by_id(id); let response = ureq::get(&req_url).call().map_err(|e| { @@ -257,6 +286,14 @@ fn download_config(id: u128) -> std::io::Result std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Online functionality not included in this build", + )) +} + pub fn upload_settings( id: u64, user_id: String, @@ -345,6 +382,7 @@ fn settings_to_web_config( } } +#[cfg(feature = "online")] fn upload_config(config: community_settings_core::v1::Metadata) -> std::io::Result<()> { let req_url = url_upload_config(); ureq::post(&req_url) @@ -356,6 +394,11 @@ fn upload_config(config: community_settings_core::v1::Metadata) -> std::io::Resu .map(|_| ()) } +#[cfg(not(feature = "online"))] +fn upload_config(_config: community_settings_core::v1::Metadata) -> std::io::Result<()> { + Ok(()) +} + /// Download config web method pub fn download_new_config(sender: Sender) -> impl AsyncCallable { let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety diff --git a/backend/src/consts.rs b/backend/src/consts.rs index 72c1c4b..cd1e2f3 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -8,10 +8,13 @@ pub const DEFAULT_SETTINGS_NAME: &str = "Main"; pub const DEFAULT_SETTINGS_VARIANT_NAME: &str = "Primary"; pub const LIMITS_FILE: &str = "limits_cache.ron"; +#[cfg(feature = "online")] pub const LIMITS_REFRESH_PERIOD: std::time::Duration = std::time::Duration::from_secs(60 * 60 * 24); // 1 day +#[cfg(feature = "online")] pub const LIMITS_STARTUP_WAIT: std::time::Duration = std::time::Duration::from_secs(60); // 1 minute pub const LIMITS_OVERRIDE_FILE: &str = "limits_override.ron"; +#[cfg(feature = "online")] pub const WEB_SETTINGS_CACHE: &str = "store_cache.ron"; pub const MESSAGE_SEEN_ID_FILE: &str = "seen_message.bin"; From a2e9de941b1f857a9e9bd65415acce8b40c08af3 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 17 Mar 2024 16:16:04 -0400 Subject: [PATCH 50/56] Fix memory speed setting not updating properly between profiles --- src/backend.ts | 2 +- src/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend.ts b/src/backend.ts index 9d55f77..84be8a9 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -236,7 +236,7 @@ export async function setGpuSlowMemory(clock: number): Promise { return (await call_backend("GPU_set_slow_memory", [clock]))[0]; } -export async function getGpuSlowMemory(): Promise { +export async function getGpuSlowMemory(): Promise { return (await call_backend("GPU_get_slow_memory", []))[0]; } diff --git a/src/index.tsx b/src/index.tsx index ae24558..257db52 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -170,7 +170,7 @@ const reload = function() { set_value(CLOCK_MIN_GPU, limits[0]); set_value(CLOCK_MAX_GPU, limits[1]); }); - backend.resolve(backend.getGpuSlowMemory(), (status: number) => { set_value(SLOW_MEMORY_GPU, status) }); + backend.resolve_nullable(backend.getGpuSlowMemory(), (status: number | null) => { set_value(SLOW_MEMORY_GPU, status) }); backend.resolve(backend.getGeneralPersistent(), (value: boolean) => { set_value(PERSISTENT_GEN, value) }); backend.resolve(backend.getGeneralSettingsName(), (name: string) => { set_value(NAME_GEN, name) }); From 182c30b4ee112f08c80a5d2051d315fe1d6654b4 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 17 Mar 2024 17:14:44 -0400 Subject: [PATCH 51/56] Fix loading of main menu community profiles --- .../src/api/save_setting.rs | 2 +- backend/src/api/web.rs | 22 +++++++------------ src/backend.ts | 8 +++---- src/consts.ts | 2 ++ src/index.tsx | 11 ++++++++-- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/backend/community_settings_srv/src/api/save_setting.rs b/backend/community_settings_srv/src/api/save_setting.rs index 39d610e..173364f 100644 --- a/backend/community_settings_srv/src/api/save_setting.rs +++ b/backend/community_settings_srv/src/api/save_setting.rs @@ -11,7 +11,7 @@ pub async fn save_setting_handler( content_type: web::Header, cli: web::Data<&'static Cli>, ) -> std::io::Result { - println!("Content-Type: {}", content_type.to_string()); + //println!("Content-Type: {}", content_type.to_string()); let bytes = match data.to_bytes_limited(PAYLOAD_LIMIT).await { Ok(Ok(x)) => x, Ok(Err(e)) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("wut: {}", e))), diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index acd467c..9301064 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -5,10 +5,8 @@ use std::sync::{Arc, Mutex}; use usdpl_back::core::serdes::Primitive; use usdpl_back::AsyncCallable; -use chrono::{offset::Utc, DateTime}; -use serde::{Deserialize, Serialize}; - use super::handler::{ApiMessage, GeneralMessage}; +use crate::utility::CachedData; #[cfg(feature = "online")] const BASE_URL_FALLBACK: &'static str = "https://powertools.ngni.us"; @@ -18,12 +16,6 @@ static BASE_URL: RwLock> = RwLock::new(None); const MAX_CACHE_DURATION: std::time::Duration = std::time::Duration::from_secs(60 * 60 * 24 * 7 /* 7 days */); -#[derive(Serialize, Deserialize, Clone, Debug)] -struct CachedData { - data: T, - updated: DateTime, -} - type StoreCache = std::collections::HashMap>>; @@ -112,14 +104,14 @@ fn save_cache(_cache: &StoreCache) {} fn get_maybe_cached(steam_app_id: u32) -> Vec { let mut cache = load_cache(); let data = if let Some(cached_result) = cache.get(&steam_app_id) { - if cached_result.updated < (Utc::now() - MAX_CACHE_DURATION) { + if cached_result.needs_update(MAX_CACHE_DURATION) { // cache needs update if let Ok(result) = search_by_app_id_online(steam_app_id) { cache.insert( steam_app_id, CachedData { data: result.clone(), - updated: Utc::now(), + updated: chrono::offset::Utc::now(), }, ); save_cache(&cache); @@ -138,7 +130,7 @@ fn get_maybe_cached(steam_app_id: u32) -> Vec impl AsyncCallable { }; super::async_utils::AsyncIsh { trans_setter: |params| { - if let Some(Primitive::F64(app_id)) = params.get(0) { - Ok(*app_id as u32) + if let Some(Primitive::String(s)) = params.get(0) { + s.parse::().map_err(|e| format!("search_by_app_id invalid parameter 0: {}", e)) } else { Err("search_by_app_id missing/invalid parameter 0".to_owned()) } @@ -329,6 +321,8 @@ fn settings_to_web_config( username: String, settings: crate::persist::SettingsJson, ) -> community_settings_core::v1::Metadata { + #[cfg(any(not(debug_assertions), not(feature = "dev_stuff")))] + let app_id = if app_id == 0 { 1 } else { app_id }; community_settings_core::v1::Metadata { name: settings.name, steam_app_id: app_id, diff --git a/src/backend.ts b/src/backend.ts index 84be8a9..fa3d276 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -376,8 +376,8 @@ export type StoreMetadata = { //config: any, } -export async function searchStoreByAppId(id: number): Promise { - console.log("WEB_search_by_app"); +export async function searchStoreByAppId(id: string): Promise { + //console.log("WEB_search_by_app"); return (await call_backend("WEB_search_by_app", [id]))[0]; } @@ -395,11 +395,11 @@ export async function storeUpload(steam_id: string, steam_username: string): Pro } export async function getAllSettingVariants(): Promise { - console.log("GENERAL_get_all_variants"); + //console.log("GENERAL_get_all_variants"); return (await call_backend("GENERAL_get_all_variants", [])); } export async function getCurrentSettingVariant(): Promise { - console.log("GENERAL_get_current_variant"); + //console.log("GENERAL_get_current_variant"); return (await call_backend("GENERAL_get_current_variant", []))[0]; } diff --git a/src/consts.ts b/src/consts.ts index 8fc068a..b938fe4 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -44,4 +44,6 @@ export const PERIODICAL_BACKEND_PERIOD = 5000; // milliseconds export const AUTOMATIC_REAPPLY_WAIT = 2000; // milliseconds export const STORE_RESULTS_URI = "/plugins/PowerTools/settings_store"; +export const STORE_MAIN_APP_ID = "1"; +export const STORE_MAIN_APP_ID_DEV = "0"; diff --git a/src/index.tsx b/src/index.tsx index 257db52..f392f20 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -67,6 +67,7 @@ import { STORE_RESULTS, STORE_RESULTS_URI, + STORE_MAIN_APP_ID, PERIODICAL_BACKEND_PERIOD, AUTOMATIC_REAPPLY_WAIT, @@ -130,7 +131,7 @@ const reload = function() { }); if (!get_value(STORE_RESULTS)) { - backend.resolve(backend.searchStoreByAppId(0), (results) => set_value(STORE_RESULTS, results)); + backend.resolve(backend.searchStoreByAppId(STORE_MAIN_APP_ID), (results) => set_value(STORE_RESULTS, results)); } backend.resolve(backend.getBatteryCurrent(), (rate: number) => { set_value(CURRENT_BATT, rate) }); @@ -216,13 +217,19 @@ const registerCallbacks = function(autoclear: boolean) { }); } ); + backend.resolve( + backend.searchStoreByAppId(STORE_MAIN_APP_ID), + (results: backend.StoreMetadata[]) => { + set_value(STORE_RESULTS, results); + } + ); } }); //@ts-ignore startHook = SteamClient.Apps.RegisterForGameActionStart((actionType, id) => { //@ts-ignore let gameInfo: any = appStore.GetAppOverviewByGameID(id); - let appId = gameInfo.appid.toString(); + let appId: string = gameInfo.appid.toString(); backend.log(backend.LogLevel.Info, "RegisterForGameActionStart callback(" + actionType + ", " + id + ")"); backend.resolve( From d7a108004bbc5c930bed1c69e4f62204a1ac4acc Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 17 Mar 2024 17:15:23 -0400 Subject: [PATCH 52/56] Make limits cache update only occur exactly once per period --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- backend/src/consts.rs | 2 + backend/src/settings/detect/auto_detect.rs | 23 +----- backend/src/settings/detect/limits_worker.rs | 73 ++++++++++++-------- backend/src/utility.rs | 15 ++++ package.json | 2 +- 7 files changed, 67 insertions(+), 52 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index dc737ff..01176da 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "powertools" -version = "2.0.0-alpha4" +version = "2.0.0-alpha5" dependencies = [ "async-trait", "chrono", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index fcbe28b..e22502c 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "powertools" -version = "2.0.0-alpha4" +version = "2.0.0-alpha5" edition = "2021" authors = ["NGnius (Graham) "] description = "Backend (superuser) functionality for PowerTools" diff --git a/backend/src/consts.rs b/backend/src/consts.rs index cd1e2f3..246b1bb 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -12,6 +12,8 @@ pub const LIMITS_FILE: &str = "limits_cache.ron"; pub const LIMITS_REFRESH_PERIOD: std::time::Duration = std::time::Duration::from_secs(60 * 60 * 24); // 1 day #[cfg(feature = "online")] pub const LIMITS_STARTUP_WAIT: std::time::Duration = std::time::Duration::from_secs(60); // 1 minute +#[cfg(feature = "online")] +pub const LIMITS_CHECK_PERIOD: std::time::Duration = std::time::Duration::from_secs(5 * 60); // 5 minutes pub const LIMITS_OVERRIDE_FILE: &str = "limits_override.ron"; #[cfg(feature = "online")] diff --git a/backend/src/settings/detect/auto_detect.rs b/backend/src/settings/detect/auto_detect.rs index 5ee8e4b..7ebba0e 100644 --- a/backend/src/settings/detect/auto_detect.rs +++ b/backend/src/settings/detect/auto_detect.rs @@ -8,28 +8,7 @@ use crate::persist::{DriverJson, SettingsJson}; use crate::settings::{Driver, General, ProviderBuilder, TBattery, TCpus, TGeneral, TGpu}; fn get_limits() -> limits_core::json_v2::Base { - let limits_path = super::utility::limits_path(); - match File::open(&limits_path) { - Ok(f) => match ron::de::from_reader(f) { - Ok(lim) => lim, - Err(e) => { - log::warn!( - "Failed to parse limits file `{}`, cannot use for auto_detect: {}", - limits_path.display(), - e - ); - limits_core::json_v2::Base::default() - } - }, - Err(e) => { - log::warn!( - "Failed to open limits file `{}`: {}", - limits_path.display(), - e - ); - super::limits_worker::get_limits_cached() - } - } + super::limits_worker::get_limits_cached() } fn get_limits_overrides() -> Option { diff --git a/backend/src/settings/detect/limits_worker.rs b/backend/src/settings/detect/limits_worker.rs index f211cc8..9c671fa 100644 --- a/backend/src/settings/detect/limits_worker.rs +++ b/backend/src/settings/detect/limits_worker.rs @@ -2,6 +2,11 @@ use std::thread::{self, JoinHandle}; use limits_core::json_v2::Base; +#[inline] +fn expired_updated_time() -> chrono::DateTime { + chrono::offset::Utc::now() - (crate::consts::LIMITS_REFRESH_PERIOD * 2) +} + #[cfg(feature = "online")] pub fn spawn() -> JoinHandle<()> { thread::spawn(move || { @@ -12,41 +17,53 @@ pub fn spawn() -> JoinHandle<()> { loop { if (limits_path.exists() && limits_path.is_file()) || !limits_path.exists() { // try to load limits from file, fallback to built-in default - let base = if limits_path.exists() { + let mut base = if limits_path.exists() { match std::fs::File::open(&limits_path) { Ok(f) => match ron::de::from_reader(f) { Ok(b) => b, Err(e) => { log::error!("Cannot parse {}: {}", limits_path.display(), e); - Base::default() + crate::utility::CachedData { + data: Base::default(), + updated: expired_updated_time(), + } } }, Err(e) => { log::error!("Cannot open {}: {}", limits_path.display(), e); - Base::default() + crate::utility::CachedData { + data: Base::default(), + updated: expired_updated_time(), + } } } } else { - let base = Base::default(); - save_base(&base, &limits_path); + let mut base = crate::utility::CachedData { + data: Base::default(), + updated: expired_updated_time(), + }; + save_base(&mut base, &limits_path); base }; - crate::api::web::set_base_url(base.store); - if let Some(refresh) = &base.refresh { - // try to retrieve newer version - match ureq::get(refresh).call() { - Ok(response) => { - let json_res: std::io::Result = response.into_json(); - match json_res { - Ok(new_base) => { - save_base(&new_base, &limits_path); - } - Err(e) => { - log::error!("Cannot parse response from `{}`: {}", refresh, e) + crate::api::web::set_base_url(base.data.store.clone()); + if let Some(refresh) = &base.data.refresh { + if base.needs_update(crate::consts::LIMITS_REFRESH_PERIOD) { + // try to retrieve newer version + match ureq::get(refresh).call() { + Ok(response) => { + let json_res: std::io::Result = response.into_json(); + match json_res { + Ok(new_base) => { + base.data = new_base; + save_base(&mut base, &limits_path); + } + Err(e) => { + log::error!("Cannot parse response from `{}`: {}", refresh, e) + } } } + Err(e) => log::warn!("Cannot download limits from `{}`: {}", refresh, e), } - Err(e) => log::warn!("Cannot download limits from `{}`: {}", refresh, e), } } else { log::info!("limits_worker refresh is empty, terminating..."); @@ -55,7 +72,7 @@ pub fn spawn() -> JoinHandle<()> { } else if !limits_path.is_file() { log::error!("Path for storing limits is not a file!"); } - thread::sleep(crate::consts::LIMITS_REFRESH_PERIOD); + thread::sleep(crate::consts::LIMITS_CHECK_PERIOD); } log::warn!("limits_worker completed!"); }) @@ -68,33 +85,35 @@ pub fn spawn() -> JoinHandle<()> { }) } -pub fn get_limits_cached() -> Base { +pub fn get_limits_cached() -> Base { let limits_path = super::utility::limits_path(); - if limits_path.is_file() { + let cached: crate::utility::CachedData = if limits_path.is_file() { match std::fs::File::open(&limits_path) { Ok(f) => match ron::de::from_reader(f) { Ok(b) => b, Err(e) => { log::error!("Cannot parse {}: {}", limits_path.display(), e); - Base::default() + return Base::default(); } }, Err(e) => { log::error!("Cannot open {}: {}", limits_path.display(), e); - Base::default() + return Base::default(); } } } else { - Base::default() - } + return Base::default(); + }; + cached.data } #[cfg(feature = "online")] -fn save_base(new_base: &Base, path: impl AsRef) { +fn save_base(new_base: &mut crate::utility::CachedData, path: impl AsRef) { let limits_path = path.as_ref(); + new_base.updated = chrono::offset::Utc::now(); match std::fs::File::create(&limits_path) { Ok(f) => { - match ron::ser::to_writer_pretty(f, &new_base, crate::utility::ron_pretty_config()) { + match ron::ser::to_writer_pretty(f, new_base, crate::utility::ron_pretty_config()) { Ok(_) => log::info!("Successfully saved new limits to {}", limits_path.display()), Err(e) => log::error!( "Failed to save limits json to file `{}`: {}", diff --git a/backend/src/utility.rs b/backend/src/utility.rs index d9ec7b1..3814211 100644 --- a/backend/src/utility.rs +++ b/backend/src/utility.rs @@ -3,6 +3,9 @@ use std::io::{Read, Write}; use std::os::unix::fs::PermissionsExt; +use serde::{Deserialize, Serialize}; +use chrono::{offset::Utc, DateTime}; + /*pub fn unwrap_lock<'a, T: Sized>( result: LockResult>, lock_name: &str, @@ -16,6 +19,18 @@ use std::os::unix::fs::PermissionsExt; } }*/ +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CachedData { + pub data: T, + pub updated: DateTime, +} + +impl CachedData { + pub fn needs_update(&self, max_age: std::time::Duration) -> bool { + self.updated < (Utc::now() - max_age) + } +} + pub fn ron_pretty_config() -> ron::ser::PrettyConfig { ron::ser::PrettyConfig::default() .struct_names(true) diff --git a/package.json b/package.json index 793d0ca..e7dfd03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "PowerTools", - "version": "2.0.0-alpha4", + "version": "2.0.0-alpha5", "description": "Power tweaks for power users", "scripts": { "build": "shx rm -rf dist && rollup -c", From d7db00ac78a27a6f67430a9b81e92e73bff27ae4 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 17 Mar 2024 17:35:07 -0400 Subject: [PATCH 53/56] version 2.0.0-beta1 --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- backend/build.sh | 5 +++-- package.json | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 01176da..d194ab7 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "powertools" -version = "2.0.0-alpha5" +version = "2.0.0-beta1" dependencies = [ "async-trait", "chrono", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index e22502c..22c2fbd 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "powertools" -version = "2.0.0-alpha5" +version = "2.0.0-beta1" edition = "2021" authors = ["NGnius (Graham) "] description = "Backend (superuser) functionality for PowerTools" diff --git a/backend/build.sh b/backend/build.sh index 7e408f6..689ae88 100755 --- a/backend/build.sh +++ b/backend/build.sh @@ -3,10 +3,11 @@ #cargo build --release --target x86_64-unknown-linux-musl #cargo build --target x86_64-unknown-linux-musl #cross build -cargo build +cargo build --release +#cargo build mkdir -p ../bin #cp --preserve=mode ./target/x86_64-unknown-linux-musl/release/powertools ../bin/backend #cp --preserve=mode ./target/x86_64-unknown-linux-musl/debug/powertools ../bin/backend +cp --preserve=mode ./target/release/powertools ../bin/backend #cp --preserve=mode ./target/debug/powertools ../bin/backend -cp --preserve=mode ./target/debug/powertools ../bin/backend diff --git a/package.json b/package.json index e7dfd03..540a2fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "PowerTools", - "version": "2.0.0-alpha5", + "version": "2.0.0-beta1", "description": "Power tweaks for power users", "scripts": { "build": "shx rm -rf dist && rollup -c", From 0373e8d47a6914c8bf49adf6d9e294093163fde2 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 30 Mar 2024 21:29:11 -0400 Subject: [PATCH 54/56] Add missing translations --- src/index.tsx | 4 ++-- translations/es-ES.mo | Bin 2531 -> 2531 bytes translations/fr-CA.mo | Bin 2992 -> 3115 bytes translations/fr-CA.po | 10 ++++++++++ translations/fr-FR.mo | Bin 2992 -> 3115 bytes translations/pt.pot | 10 ++++++++++ translations/ru-RU.mo | Bin 3602 -> 3602 bytes translations/uk-UA.mo | Bin 3636 -> 3636 bytes translations/zh-CN.mo | Bin 2282 -> 2282 bytes translations/zh-HK.mo | Bin 2314 -> 2314 bytes 10 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index f392f20..a6e88f3 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -410,7 +410,7 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { {(!isVariantLoading && = ({}) => { //layout="below" onClick={(_: MouseEvent) => { backend.log(backend.LogLevel.Debug, "Creating new PowerTools settings variant"); - modalResult = showModal( { modalResult?.Close(); Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky)}}/>, window); + modalResult = showModal( { modalResult?.Close(); Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky)}}/>, window); }} > diff --git a/translations/es-ES.mo b/translations/es-ES.mo index d6ba246b99a544c2c8d686c3752ea2d25f5a9c48..19aaa3fa144fe1bd79fa3c33ef4c1d3d0cd639db 100644 GIT binary patch delta 16 YcmaDX{8)IyBR1yH5Vy@w+5RyB06xYC6#xJL delta 16 YcmaDX{8)IyBR1yJlC;fF+5RyB06}I4bpQYW diff --git a/translations/fr-CA.mo b/translations/fr-CA.mo index f25dd7832c35a2ff9df0d0ab6e5f18b7cd98ff0b..b4ecd6ee626421df780fb668b08d33b54069f61e 100644 GIT binary patch delta 1155 zcmYk*Pe_zO7{~EP-F9RSz8RS7&@C^jl4OxN+d+E?6 z%0P)CI@h6~V2PqrMnoOLpi6;x=~7A{1VM+O@9*6?IPAQidFOp+-e=~Sect*c9j-RT z?iyu~x|{m2%Ivt>HC!k?F|)1Mi+eGL2^>MaGLAcN0#jJRI=t@gZ(}$24^ZQla0`A! z<_&F`Mk5_xF^)g64L8uoByVfNEbhUhsOMu?i$&DK0IeJi~4H20dKH9{i4)KgG6b z=bE{MdZC?08gt0E>=gFnh%-b5_!M*a3NyHl8t)}5XCQ+L=osq#Asochs4c(d`fs`W z`$_Vzt$5@*%BTRAP$~F;WWzpT3w}WsVQY8@$7&ZPRDmBBXag(S(ucF4? zanIk^vHt^fe4~S(C5T2J_D~a~kqp=_s&cBLol+24sy^ODRnhzUC+N^=7xqzAloAD{ zQe7eXRzK}_7B zE2-JgQ>@6+N=>Xpma|YuB)y66_qq4<&i&kT-nr-e&hOki*Y>0}{nVVy1Z6k1gF3Y& zqJp&sE|hOeBUa#d+=_p2IkqPe+(tKUz!I*+16ahvS^ET*XrDu$yNS#2HZpG-_qbV0 z$1E1`DQ?0S*oE`xgg>wo|7QPpGD{=vA@uoSG@xT>z$ejw&!B;&=sZ)n3h&qZ(}+h| zM>X>`vPQf`1A2!}@F8n2paFixW^81T0d$}{9L(B#(SQzNHy%Ofxr9DHiAC0rJKXr- zEFLHQkF$1-&H?%pw&RYJ|F`8*Iamn6g&< z;${~vX7)4MRGq>-IEFoV7oFfGdgk+JKnv*jFIdLkXo~xq{`U`|?Sp6rj%MxACeGgg zF4AGjCy=bhHQb0dkeFfy_u&KN7w@^O$Is~VKhg0uG{6?3aeNPYM58!}SCC&kN+Y}uNO16nd&>TgX&k4m+jp6^2PA@)w7q! X&R5GxrD6JdVWPTN7)hqbi_iW68NpDC diff --git a/translations/fr-CA.po b/translations/fr-CA.po index fbb09fb..3be3a46 100644 --- a/translations/fr-CA.po +++ b/translations/fr-CA.po @@ -49,6 +49,16 @@ msgstr "Réappliquer le profil" msgid "Defaults" msgstr "Valeurs par défaut" +#: index.tsx:413 +# (Settings selection dropdown) +msgid "Profile Variant" +msgstr "Variante de profil" + +#: index.tsx:464 +# (Alternate settings profile name) +msgid "Variant name" +msgstr "Nom de la variante" + # -- components/battery.tsx -- # (Battery section title) #: components/battery.tsx:42 diff --git a/translations/fr-FR.mo b/translations/fr-FR.mo index f25dd7832c35a2ff9df0d0ab6e5f18b7cd98ff0b..b4ecd6ee626421df780fb668b08d33b54069f61e 100644 GIT binary patch delta 1155 zcmYk*Pe_zO7{~EP-F9RSz8RS7&@C^jl4OxN+d+E?6 z%0P)CI@h6~V2PqrMnoOLpi6;x=~7A{1VM+O@9*6?IPAQidFOp+-e=~Sect*c9j-RT z?iyu~x|{m2%Ivt>HC!k?F|)1Mi+eGL2^>MaGLAcN0#jJRI=t@gZ(}$24^ZQla0`A! z<_&F`Mk5_xF^)g64L8uoByVfNEbhUhsOMu?i$&DK0IeJi~4H20dKH9{i4)KgG6b z=bE{MdZC?08gt0E>=gFnh%-b5_!M*a3NyHl8t)}5XCQ+L=osq#Asochs4c(d`fs`W z`$_Vzt$5@*%BTRAP$~F;WWzpT3w}WsVQY8@$7&ZPRDmBBXag(S(ucF4? zanIk^vHt^fe4~S(C5T2J_D~a~kqp=_s&cBLol+24sy^ODRnhzUC+N^=7xqzAloAD{ zQe7eXRzK}_7B zE2-JgQ>@6+N=>Xpma|YuB)y66_qq4<&i&kT-nr-e&hOki*Y>0}{nVVy1Z6k1gF3Y& zqJp&sE|hOeBUa#d+=_p2IkqPe+(tKUz!I*+16ahvS^ET*XrDu$yNS#2HZpG-_qbV0 z$1E1`DQ?0S*oE`xgg>wo|7QPpGD{=vA@uoSG@xT>z$ejw&!B;&=sZ)n3h&qZ(}+h| zM>X>`vPQf`1A2!}@F8n2paFixW^81T0d$}{9L(B#(SQzNHy%Ofxr9DHiAC0rJKXr- zEFLHQkF$1-&H?%pw&RYJ|F`8*Iamn6g&< z;${~vX7)4MRGq>-IEFoV7oFfGdgk+JKnv*jFIdLkXo~xq{`U`|?Sp6rj%MxACeGgg zF4AGjCy=bhHQb0dkeFfy_u&KN7w@^O$Is~VKhg0uG{6?3aeNPYM58!}SCC&kN+Y}uNO16nd&>TgX&k4m+jp6^2PA@)w7q! X&R5GxrD6JdVWPTN7)hqbi_iW68NpDC diff --git a/translations/pt.pot b/translations/pt.pot index 1c15573..d39e7b3 100644 --- a/translations/pt.pot +++ b/translations/pt.pot @@ -47,6 +47,16 @@ msgstr "" msgid "Defaults" msgstr "" +#: index.tsx:413 +# (Settings selection dropdown) +msgid "Profile Variant" +msgstr "" + +#: index.tsx:464 +# (Alternate settings profile name) +msgid "Variant name" +msgstr "" + # -- components/battery.tsx -- #: components/battery.tsx:42 diff --git a/translations/ru-RU.mo b/translations/ru-RU.mo index 30e36e28468a6053f367bf29768c6dc983f53ec6..038e12dbc6729601d973fb93e05afa21b857f5b3 100644 GIT binary patch delta 16 XcmbOvGf8Gc4Lfsah}-6R_Q`AjFNFnE delta 16 XcmbOvGf8Gc4LfsbN!sRm_Q`AjG9?AH diff --git a/translations/uk-UA.mo b/translations/uk-UA.mo index cb725de6f1a8d1601e04bfbcfc2686e4208ed1ff..63c29b3951964eb109168f7eb4e3ac2bd91b9cc9 100644 GIT binary patch delta 16 XcmdlYvqfe@4Lfsah}-6R_N{CHGsOkE delta 16 XcmdlYvqfe@4LfsbN!sRm_N{CHHf07H diff --git a/translations/zh-CN.mo b/translations/zh-CN.mo index 78bd0ade13060e0e3a8c48b74519da37c89555b4..334a3d5ba3783d4e71cb8dfaa6408b9a126fbc20 100644 GIT binary patch delta 16 XcmaDQ_)2iYGB)PW5Vy@M*&LYwI${Ot delta 16 XcmaDQ_)2iYGB)PYlC;e$*&LYwJpu+w diff --git a/translations/zh-HK.mo b/translations/zh-HK.mo index b31cfc24a905744861bbf842243e0f6ccde4a23a..f832bc60bb3072eac032086e0e7732ffe6a2f000 100644 GIT binary patch delta 16 XcmeAY>Jr+pjEy-o#BK9RwnQcXFMb6~ delta 16 XcmeAY>Jr+pjE%XpByICbwnQcXG9Cr2 From ccf0c04020bfc5ae9f667801f99d1a31f939006a Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 30 Mar 2024 21:29:57 -0400 Subject: [PATCH 55/56] Add tags for persistent settings variants --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- backend/src/api/web.rs | 3 ++- backend/src/persist/general.rs | 2 ++ backend/src/settings/detect/auto_detect.rs | 1 + backend/src/settings/general.rs | 6 ++++++ backend/src/settings/traits.rs | 2 ++ package.json | 2 +- 8 files changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index d194ab7..d76f0c1 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "powertools" -version = "2.0.0-beta1" +version = "2.0.0-beta2" dependencies = [ "async-trait", "chrono", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 22c2fbd..5d5e6a9 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "powertools" -version = "2.0.0-beta1" +version = "2.0.0-beta2" edition = "2021" authors = ["NGnius (Graham) "] description = "Backend (superuser) functionality for PowerTools" diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index 9301064..8222fa2 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -265,6 +265,7 @@ fn web_config_to_settings_json( root: None, }, provider: Some(crate::persist::DriverJson::AutoDetect), + tags: meta.tags, } } @@ -328,7 +329,7 @@ fn settings_to_web_config( steam_app_id: app_id, steam_user_id: user_id, steam_username: username, - tags: vec!["wip".to_owned()], + tags: settings.tags, id: "".to_owned(), config: community_settings_core::v1::Config { cpus: settings diff --git a/backend/src/persist/general.rs b/backend/src/persist/general.rs index fe18898..8cc93b7 100644 --- a/backend/src/persist/general.rs +++ b/backend/src/persist/general.rs @@ -14,6 +14,7 @@ pub struct SettingsJson { pub gpu: GpuJson, pub battery: BatteryJson, pub provider: Option, + pub tags: Vec, } impl Default for SettingsJson { @@ -27,6 +28,7 @@ impl Default for SettingsJson { gpu: GpuJson::default(), battery: BatteryJson::default(), provider: None, + tags: Vec::new(), } } } diff --git a/backend/src/settings/detect/auto_detect.rs b/backend/src/settings/detect/auto_detect.rs index 7ebba0e..256f97a 100644 --- a/backend/src/settings/detect/auto_detect.rs +++ b/backend/src/settings/detect/auto_detect.rs @@ -69,6 +69,7 @@ pub fn auto_detect0( variant_id, variant_name, driver: DriverJson::AutoDetect, + tags: settings_opt.map(|s| s.tags.clone()).unwrap_or_else(|| Vec::new()), }); let cpu_info: String = usdpl_back::api::files::read_single("/proc/cpuinfo").unwrap_or_default(); diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index 1cc1a33..ea95661 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -37,6 +37,7 @@ pub struct General { pub variant_id: u64, pub variant_name: String, pub driver: crate::persist::DriverJson, + pub tags: Vec, } impl OnSet for General { @@ -175,6 +176,10 @@ impl TGeneral for General { fn provider(&self) -> crate::persist::DriverJson { self.driver.clone() } + + fn tags(&self) -> &[String] { + &self.tags + } } #[derive(Debug)] @@ -439,6 +444,7 @@ impl Settings { gpu: self.gpu.json(), battery: self.battery.json(), provider: Some(self.general.provider()), + tags: self.general.tags().to_owned(), } } } diff --git a/backend/src/settings/traits.rs b/backend/src/settings/traits.rs index 2c8524f..468f676 100644 --- a/backend/src/settings/traits.rs +++ b/backend/src/settings/traits.rs @@ -139,6 +139,8 @@ pub trait TGeneral: OnSet + OnResume + OnPowerEvent + OnLoad + OnUnload + Debug ) -> Result, SettingError>; fn provider(&self) -> crate::persist::DriverJson; + + fn tags(&self) -> &'_ [String]; } pub trait TBattery: OnSet + OnResume + OnPowerEvent + OnLoad + OnUnload + Debug + Send { diff --git a/package.json b/package.json index 540a2fc..be7d9cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "PowerTools", - "version": "2.0.0-beta1", + "version": "2.0.0-beta2", "description": "Power tweaks for power users", "scripts": { "build": "shx rm -rf dist && rollup -c", From 9acc08a599675504ca590db7a2fc889ef976e3e1 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 31 Mar 2024 14:26:01 -0400 Subject: [PATCH 56/56] Remove optional command run in limits --- backend/limits_core/src/json_v2/base.rs | 9 --------- backend/limits_core/src/json_v2/conditions.rs | 3 --- backend/limits_srv/pt_limits_v2.json | 15 +++------------ backend/src/settings/detect/auto_detect.rs | 9 --------- 4 files changed, 3 insertions(+), 33 deletions(-) diff --git a/backend/limits_core/src/json_v2/base.rs b/backend/limits_core/src/json_v2/base.rs index 72aa0f3..b47f450 100644 --- a/backend/limits_core/src/json_v2/base.rs +++ b/backend/limits_core/src/json_v2/base.rs @@ -24,7 +24,6 @@ impl Default for Base { dmi: None, cpuinfo: None, os: None, - command: None, file_exists: Some("/etc/powertools_dev_mode".into()), }, limits: super::Limits { @@ -48,7 +47,6 @@ impl Default for Base { dmi: None, cpuinfo: Some("model name\t: AMD Custom APU 0405\n".to_owned()), os: None, - command: None, file_exists: None, }, limits: super::Limits { @@ -72,7 +70,6 @@ impl Default for Base { dmi: None, cpuinfo: Some("model name\t: AMD Custom APU 0932\n".to_owned()), os: None, - command: None, file_exists: None, }, limits: super::Limits { @@ -96,7 +93,6 @@ impl Default for Base { dmi: None, cpuinfo: Some("model name\t+: AMD Ryzen 3 2300U\n".to_owned()), os: None, - command: None, file_exists: None, }, limits: super::Limits { @@ -140,7 +136,6 @@ impl Default for Base { dmi: None, cpuinfo: Some("model name\t+: AMD Ryzen 5 5560U\n".to_owned()), os: None, - command: None, file_exists: None, }, limits: super::Limits { @@ -184,7 +179,6 @@ impl Default for Base { dmi: None, cpuinfo: Some("model name\t+: AMD Ryzen 7 5825U\n".to_owned()), os: None, - command: None, file_exists: None, }, limits: super::Limits { @@ -228,7 +222,6 @@ impl Default for Base { dmi: None, cpuinfo: Some("model name\t+: AMD Ryzen 7 6800U( with Radeon Graphics)?\n".to_owned()), os: None, - command: None, file_exists: None, }, limits: super::Limits { @@ -272,7 +265,6 @@ impl Default for Base { dmi: None, cpuinfo: Some("model name\\s+: AMD Ryzen 7 7840U( w\\/ Radeon 780M Graphics)?\n".to_owned()), os: None, - command: None, file_exists: None, }, limits: super::Limits { @@ -316,7 +308,6 @@ impl Default for Base { dmi: None, cpuinfo: None, os: None, - command: None, file_exists: None, }, limits: super::Limits { diff --git a/backend/limits_core/src/json_v2/conditions.rs b/backend/limits_core/src/json_v2/conditions.rs index be08a1c..11607b9 100644 --- a/backend/limits_core/src/json_v2/conditions.rs +++ b/backend/limits_core/src/json_v2/conditions.rs @@ -9,8 +9,6 @@ pub struct Conditions { pub cpuinfo: Option, /// Regex pattern for /etc/os-release reading pub os: Option, - /// Custom command to run, where an exit code of 0 means a successful match - pub command: Option, /// Check if file exists pub file_exists: Option, } @@ -20,7 +18,6 @@ impl Conditions { self.dmi.is_none() && self.cpuinfo.is_none() && self.os.is_none() - && self.command.is_none() && self.file_exists.is_none() } } diff --git a/backend/limits_srv/pt_limits_v2.json b/backend/limits_srv/pt_limits_v2.json index 9250956..0fd8033 100644 --- a/backend/limits_srv/pt_limits_v2.json +++ b/backend/limits_srv/pt_limits_v2.json @@ -6,7 +6,6 @@ "dmi": null, "cpuinfo": null, "os": null, - "command": null, "file_exists": "/etc/powertools_dev_mode" }, "limits": { @@ -391,7 +390,6 @@ "dmi": null, "cpuinfo": "model name\t: AMD Custom APU 0405\n", "os": null, - "command": null, "file_exists": null }, "limits": { @@ -604,8 +602,8 @@ "extras": { "experiments": false, "quirks": [ - "pp_dpm_fclk-not-updated-on-LCD", - "pp_dpm_fclk-reversed" + "pp_dpm_fclk-reversed", + "pp_dpm_fclk-not-updated-on-LCD" ] } } @@ -641,7 +639,6 @@ "dmi": null, "cpuinfo": "model name\t: AMD Custom APU 0932\n", "os": null, - "command": null, "file_exists": null }, "limits": { @@ -870,8 +867,8 @@ "extras": { "experiments": false, "quirks": [ - "pp_dpm_fclk-not-updated-on-LCD", "pp_dpm_fclk-reversed", + "pp_dpm_fclk-not-updated-on-LCD", "clock-autodetect" ] } @@ -908,7 +905,6 @@ "dmi": null, "cpuinfo": "model name\t+: AMD Ryzen 3 2300U\n", "os": null, - "command": null, "file_exists": null }, "limits": { @@ -1062,7 +1058,6 @@ "dmi": null, "cpuinfo": "model name\t+: AMD Ryzen 5 5560U\n", "os": null, - "command": null, "file_exists": null }, "limits": { @@ -1376,7 +1371,6 @@ "dmi": null, "cpuinfo": "model name\t+: AMD Ryzen 7 5825U\n", "os": null, - "command": null, "file_exists": null }, "limits": { @@ -1770,7 +1764,6 @@ "dmi": null, "cpuinfo": "model name\t+: AMD Ryzen 7 6800U( with Radeon Graphics)?\n", "os": null, - "command": null, "file_exists": null }, "limits": { @@ -2164,7 +2157,6 @@ "dmi": null, "cpuinfo": "model name\\s+: AMD Ryzen 7 7840U( w\\/ Radeon 780M Graphics)?\n", "os": null, - "command": null, "file_exists": null }, "limits": { @@ -2552,7 +2544,6 @@ "dmi": null, "cpuinfo": null, "os": null, - "command": null, "file_exists": null }, "limits": { diff --git a/backend/src/settings/detect/auto_detect.rs b/backend/src/settings/detect/auto_detect.rs index 256f97a..5773f12 100644 --- a/backend/src/settings/detect/auto_detect.rs +++ b/backend/src/settings/detect/auto_detect.rs @@ -111,15 +111,6 @@ pub fn auto_detect0( .expect("Invalid OS regex"); matches &= pattern.is_match(&os_info); } - if let Some(cmd) = &conditions.command { - match std::process::Command::new("bash") - .args(["-c", cmd]) - .status() - { - Ok(status) => matches &= status.code().map(|c| c == 0).unwrap_or(false), - Err(e) => log::warn!("Ignoring bash limits error: {}", e), - } - } if let Some(file_exists) = &conditions.file_exists { let exists = std::path::Path::new(file_exists).exists(); matches &= exists;

(cssN&K#k1l!h7YYfTYPpc$_m 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 delta 191597 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\-O3GX#8 zN*=b&Y^%O4B100q?0esOmB;A48)xX7dth=w*+YCEdBEmDm#4S!J^5S~cnknQ-1tmv zQcTV#bHT|RENDH?0jd@fq*~1pZ<1yM`WJv*Vfx^ctqc(IqTx&dFS94i$e{UR&I93L zw?JlrfW!}T_~PMt24)%1@Q?0_;GFBuU5pg$?)t2r4CAK3uJFXN z<%UL;n_k9PF1>9xFDf$h#B(3sq<^lkIXl4e$U6B*-Ye(X5n!{a0><$bpk== z?YghPefRK40mU7a;u&;)gbM3m{G;@u6@d`q4t02;^ZqMf=*7^$xjCK>YF1Ow)CEix z>hYjz8I+g=csZ0HSx$%C5Rv_ca_8R1kKx(1P1|Kp-Fms<;HemyLx)RUOwPcpiE^q? zPqSh68nuA2U&8JaWRL!o9dCQqBAwrR)fi+ZIj%jNPh8p&GGXw|dt&OxjfPzVwRR4| zl(8`wbPk;WFsc2VfrAaoKc(n6%w|57PNy!;l>J8Y`mct+dBPQNxrZh@A09{-+rqa9>8)XupNdGzv?s8CXev=Wz zcmG#mJ=nIxP&&*)C9}ZT>${DDc1NMqHaD7n$o4P?5+@qBmug*s#80hw253!$zA%D- z>VoYRpK)CxDs%jgJaGqPDd?OL@j29Hdq6)k3ZrVSXkcXGk<0x}GwVM@5IT&nwuMrMv#qqZw_#w(hEw6q?!nxC7>%A#<{Ja#M} zUf9ZGqYP2PwKD7oQVZ@NO;shUUG-TwI+&Y$@9vym@KUGv*WN2nx69rl?UM(8+_Tqw zBW|JMPS#Y_nN$PQR){wYg-Bw^63#W|yO5!l2i095vIc1ruMC4>ZRTllnS*Y>4zz_W zq6P=hIe-5874$tC;EDt5)vOhyZy}J)Sn2~A(SzWF*v=1;9@;#qOW2BPPFRVgDqxX= z{-od4+ZvPgRk*2L`TNXEiaIkEUV`br?<1d_zqMVV!8m#hmc_dfxr-maWV&S=`JXxl)>+XhoTeC4iFGfp}XG(%r9qPnB0LX7T@%X(l382(>(*7^xpsVDYY+24m;|=N2F-8(jTi2j z#avgM%Ee@0o+6x3HUTT6pz(_2Kxl+r66G((Iw+=K?>s9hDT(r|j4NV+=99p~d9&ku;^ozq6@Aku*NpG@oq<5*`M@WA z^LmqzzOFyn4_TL?yFj2zfPAUhjcyi1nYmkJ`PCcLIXhsZp;>n_=-OiT-&e-40?~{< z3mA-}i2$H`0y%C7D(^sLy;Y!c*5{g!U~+0f0~8@v4Gqi*bsX~nwMER;K**x&5^l+ZMOrf|r5Dt?+0KUXAygy0I4;mL%gFEL33H6EeQ&#Vho zg0^@byIUkVo5GFPwfD@sT}gu<$RLkoaa(>F;VKa4zMc-EF|^)Y@Ja5mUWcq}p!?I_ z&eSM`@FRGuf2$Lrko5;_%Bv_FhAWkLJ&@l2-yc-76)wJZNC7skT@0v_+`|KTI|J$% z=`#^hXtFiXa!4Tj{3I6LCoq+;>K(qn#)GZ&93SLwH?Fsl^k)5m<=O%Ks+a&Nihfa8 zf46Q?12aXpEtgE(tFffzV@NyFu&(dU2W1d^?^7RHN(=^R`y5GmMom|l29J_GWKnIYG1uXkakO)qtx) z4!g7{Vd%`KPoLJ0C~J@Y&|`wWeY@?GT`2LzdQ}>{?l7LV2cOfua(&aH|qF!t+`0<6Br;FBLQ1kQyssD zb2{PqIl4>uhOD2FC-t!yMLwlr&X_ixesP8Q(;n+eziqqIR2+CYlRkrVG;Ft`E%r(u zhEhI?Fd`SCEWud=4>sL|X$oW)2i+4+{Xf^uPCV&R-WO~@MsaaWW+BkpH}2vBJ5Lm* zvf{}4(u;(WHc8-qIje_}+WS)6N|*E(5+O+A6~zIaUk4g)h^qHsy8r_!uuy#ShPuaE zXWok=>ZX6-%Zsc9F_bEkMYyt*xUwH<1~vJvon)3T^H4nh>ycF)!&}bk-q$lr^*XRC z%R{V;=eU<0)|RYqg3X&doWE7BRwEu_acCMd{EtIJ>=#Gslp_#5fuFa#U4g`j#8(CE zx&0Zh#{trwkjNreYDwx{VW;sZk$6{7NtT4rKT^O(6~2)1L@e#eOaL+RV3LmN{ZgOb z9}@zHMht)`#d#DcuMPg`yx$UAsyj@BAc#}_kb=E9bWgFy*YxnK%MA+a7*;_#879T>MS+^Bk^hu|X9r4cDK92?r^2WNFn zAD*V6v1h2UlgdLh9rR9}G4!Z}30?$eD#xmh^5xnO9gM zPaqo<$0Tfd@NWe^QB&^~uF|W0;|V(BadTR})4f`@83bPh)!}eLGm@dfMyXeA{0fk8 z$`Aoi8r)9AN^fB|IW>JwqzmYy@sRfmLSGZ2l55^mkf)3X-do^jiD^NjRpoMn;Jjc4 zk6JK-^X<0FyghcCsI07PX=SAoxc0746GTa^>a(m$92SMY9EbA~R99syNK zE7`>m8aqq7DGW8Pt4ksQxj+q3`uPS#>x4yuI&i0_(C7GF3EMfGX&!5h2ukeH0USkN zhki;58Sap8wO~*hsgB{LA%q1#R51;@f(j%*wve+yDNR96{=&w%lWzJCHRaldAhpKwL{=(CSyz`D-UlAal4B;tq<#PH_;bG{x#=Ko zHrJ3huezER!b~C811bXK5HkEyHMLhG17uj=ljJY)gtavKZfTX%3|7}t*Bg^RQjiO0 zU)&N6%T2JpR@^bf6+L~>^n-<)qiJs;3zFNe5BuKcN&UK;hF$!}`;PU?$z=g3t#yin z+~aF%gkZJ_bz>1b2dtVaSFXSzS4oOv@ge0im;1?$8#h{O5J$-RM8kyj`}k;xOZHve z-LHda54Asp^zYo-T6%C6%w8;(lZ3xKglw4yPEK85w1I(|64D}p4hZE`@z%SC#>w); z0>9Q+iil2YaI_L9CxddC4|R>v55Db}+oM`}&hZMw!=Qzq*V<$mF%^FovJS6;27SZ+ ziY^(w@*CX!fQZ4H!xwxkPvrB<pC`%bm1*sbov7KpTS$f6Zu?*JsN< zKDmDf-9Pkq??Iqs7$xWlu+TJnR>Bn4l>@2lhx@jW$@iw$N^K!?E06^ezj0eqMJ~T* z29KaYYtouE`J7WcCFP1JhP#HryR6$4H#5->*fHzfrUi01xhX$s{(}YvyW)+CR zL4P24<3@1^4WNeAx1h3IS_-1XhC79-w~d_N1LAYGqXNErbbouf-}z_l5<@OiWCW^+pW%lEN_#H>PB^g60+VqcEym z8?z5QBg0lZL3cTSLZ2&_`@F_>Oo%5nx}=6TCJY*g)m0~bTJaBY%M`A%4GShXA~~#8nw7GX;jOD0Zx%ImtCqS`(9rD68ki#SlIFrZVt=927(> z4Nh(RypRrZAxXtzrtQUgw{%nRdw>7+Z$?H{_6C<~oC4|Bn%7h$`J3lXoam15)1mX0g zxb*Dr`MUB1x!f@Lh>7rLdL=SLq(9w!Asc=SnifR9L*VoZfaptt($w{4$nId(fm|V0 z;N{RSyVPmnNwl|%m;7f4w_Slfw__BJ=NHArM6h~PU!{haQR0>)M*QVdS7t(J|RjashX2tCU2{&XCP=)#{$eqlRgzdx1PdqaZB)Q1eW@B$_Rl>B+F=U%1lP= z85EUQJE1BMhl|jYO!mYuYr_$v|9N;guBL_@Vo~~Dr*vB?D@RhX&Nq2^d9g>c>)*Vq z6o4li^zYxlyQ6-uy7(8xLRj14&%JwYix2u$uxpj2%x!KCg1Zil8c~7m+)C|~_LNEE zuJy~IJ1MHPdOX?4@2e)lgA)@m$H&;beXW!4C3l-ob_h^cmf(Vdi)bpR9Sg|~ZK14B zC4n4KJ)}fIr==cLdz6t;p3C2Ip>19ZT=U-urU;Pn)#&MM1jnaNILpbWc2I=Cr5|LG zmG&ci4&!xaAe`!TA748fa!ke~#6{7timW<8XrB@aaU9OEbgxdlOBFV(H|~PKax6Xf zQfTZn^;H~gZM7}l0m&Q!B2eq!S0(Iy<2Fy-VF#FR1wms1SZG7TdiPPr`}glrdj|bn z6rJKHMN6{jbgTGQu&1nY?x$Q7p{K zmh=gZ`p~rR`5?Q=n|-gJFN+0C8B97@FqAy?O0y}}vdJI;{KiZHa?DZf(A(PD%M>E& zL;T?Dz=`y$I)C(Y7Jhy=-w!rr^mK#ns-Vp~26oU-Sp;1Sca4k$paX(Ee+GR>DdoaX zyu7_H4@fiSoxs@zHeAMl7EV7ctz2Lwgr_ji&A*r5?N=+eyZS?rK{yp@NCK(wu+!`i z4=hn4$e7^sq4q<;?(wvjZzBK;pal{n4b?b;6b=KWlbsrDsSXnjv}n=4+H~NA>Phq^ zvc(Z8kwu=D$1eGdtWlxi|F?-o;aI-nhFBO@&H@2&PzHGRI@p7*O{11mQkzK$(|{8z zLs~8@Dc8Swig4RHz>J3sDqz}@%ZOqXTEM~q41Iv6#0G9dLj#&Sr*<(klI0-->Lhf0 zI8Y(0-0i@L4Sr+ABH(MY0iYbTlI;$V`HT5z&KjhHH1d)d>b&Sf&#eN(WZurT~YOT2zp z`5(vx)v++!IK6N$xD9vE{7=ageJX; zd;Mua4RlEOkU9(iYVFY87j|@lHw_&+wP@NOu9$t&e8?Fuwf`Uh-0V8wGBD*YV5{q~ zruyzsllEi~4+CA6$vdwZw5O5UsUMhuxd@i2&kP`MN+E9;u9v_bd>D5^hdOihsp5_uHBM?ZohO|f>{ot6+JgU0= z{e2f0YfQsA(*<*iyv>{*2}k&T0f0o$e3u`#g-|ktLoExLWZ~~UY{1E&Brs1s}i-?Z^6R;Z0Q>@$Uz3d7T4lD0Pz5oy)c4gepegtcFp0);p z?4h85bYfox+6GFw>Bg=8c9>lSfhm~Te2G!>LrJ*WJjED@My5wm2C$a9hAN_=-_Sy6nIDp5kibdPpxD7fGdx_BVd0bwtrv+y~CiXs$UrhJl?= z0s;!_z=I+efx%mgF(`r>A;`2Eks_2(C0 z*MAQC6KLV+Cj$`3W!^vTTL61i)`0_HpdSL3bF&%FK32x>vY*VaTRQVJLl^~tu` zNw~1-$%QaV)F%tI$3m|If>z+XyxM$t4S?4exY@iW4K%2K6ksy0APKgFRmF23QoRh} zJB1?%8WGeOA-BGsF-r(>Y%8msjAkXI#Kl5AAK|DW@Sb=mt~Q2`X*}-ALg9RLgOz}E z>fV^QdOVPLMumCQ6X>XRKeKD^x297iS`P^fN8QA5L|BygV>GsTT4IUx@zMn%7}PlX zMg+;smFD(7?l#3|Vd)3i7^%oA5{m*T33u<@&I_dIVX2JZVS&Qf%V1fX4RL#fkVt~MdVm)k({sv7@$?DkGuP+X)S-qHZ_?)-U>=Q+J=>b zrjl158FX_8V7P!*Tu7Teb2rFH2o<+ScUC9k(&EYCVeBd+eUnyCH9z&s<1CbP;FD3~ zcp+z?lz+}L7c=JiRR9q5{(8F{xY4{wc7VfKY*++ECB|0GarELfq?l^$U4NFqsi4E(y9q^6bNwY{Uzs&&4k!JvLEH z8Lg<{NB1wC)V*HaEt-Z!x$F$REqRqX4vIp1(!&ara&l*a&#=m~W?RHK1%z3#~ zEI!mh2poL%i;xoDURKFE>uL+`g_{XnzN9)X$>$BT7rBGqHfgEm>u9&#fw&}|R9HS` zqu|T!=~>+95n&-ncpn1==e#aBBdx9mSf7gpyx%v$+DO-EPoA1agDgm1)mzwHo*%knth+482;R$;= zWUKFKu<(hbU5m&BY(xJ&73DG3U-+1ump)!#!Jv|_Gy2ZEqU3d!+C_cS4!D$&ZF^H+ zZkK)m2#p6cp*ffmBB%iCdQDZ8-@>=^#62ELfYj?%r|6UZJSUjoLCX)JLkQ&d0x+aX znbKDF(d+>Oei$XJMie~DZggYraZQ7vP6HoAZ?`h&hGf$mSrdALhmxJ09N2R5_UW9j z)%lo)UB2jBQsNHPzr;ZM_}-SjS)c30E$-eevLLFycK(+{iRoX}d?{LzvRPwXy<5-O zZ^b#Dbom_!4V$DO^LW8dCxr{UxyM;cd-p$HUr)(k`JIEWNx$X&aw{@neJn}CY!d^W zm){OwiZ>;$?$%vWtp4pTO2JlwsIUMf!XWAuEEUS+qr=4W>Ge%g0zP{W3yhu`p-4Y6(aj%y8ztVS61zt%4EghtSR%?yY_uPzsZ&d9Z zeW8d56G{|5JWxc8Bdeyq{A^|CQ1LUjI%NkJBtqK?7{jljeU|?A zVqvx)vH#90-lABLo%R{^>%gtlr?p}{=3!Ay+*PJb9+yflKOc19HblCLv{JIXet&_Y zh3GxOMT*zlk`IecF8UoaZjnlGE=2CTiX}^6svE4%9lo;++Y6S!5LteiY;hO#!4O}3 zKluSitKau0T)q5-7z^m_P7$YtPxAG~w{u^ify{w{=*?VRRRTPCrU?T`xPF}n>^B@# zfTe{`QsD|9CM%k!GxJ#dYgY2n$n~l#|KWFGlrTsD7y`CIcqQ0`VEANE&v+~Uac|+G zNPNi;-mRG)W}jFA{SGj)lRQvONdx*^|JNkylRdq*%k5@Rb84}eu3myY5MwIRLmd$S_9O@nkuQ#$QEObljo z{??=p`xAwvH)@f~zz4OwD~J94ctuut;q=&fbMs;{pbmgw!H0x~&+`0h%eSv+o_@N= z76yD%F8Y`U%S$p4!$gS-OMHhuvjCnhVyuf%H$T2TgXxSKMo20C9JS+hnp1at3=iRW za4a#NNT`A?fa^>no`7z{2eW*J=3E;1;`M@x8U>nb^dCxhyMh_$Y19!Wh%35c0=UN&%q1P!^sREpFfZ>)v_r1eSm{A}~U`3^{~k2moB6 zV`Xb|WxhZvzis080oL>3Va}$05P{Eil@}Mw_2sSP9nAbluVubKU*7gZOX<|H@Vp*g zI%~lnG6{XG^K_z9uKoHO*zNp!Ho8>Y=x=2Z&SNC-gIDWQq1_xM6|UJV}Jgx_a& z6I8cf-%O^x1;IEB5hZTi=&K+fIX>WkQuh=NjIxyvESK&|_k-iN#u4H+?Ksd++9fcL z*OE?V)}1Kf@PF5{z}g;QQ&d5N(bn1xP@6g64L7b z4A2*6AZDDs9H!-af&s`94o$gSnK;meAp%y9T&Fu}L<{7xI^xbr4&{MlEo?tIIfF-3ck0W3s1u(2wk~ zeLu&CoLn*&XR7XG_l2d330PKp`S`OQ!RGqo@}kQtl7R`mfk+%uS5v^B4WFJ zFDwePDYXA1@L$u)e)7U(z?@}djf4eRT!DGedYA=nzv!$8zSXPDEe*mbF8qb2b@+h)H!bEDm{ZjEC5pjAqm8UKqRUfLkBL}VL=`9Z8Ef;U(7kfm_; zSo80$nBLrdek_~kbrQxWv7XZ1uTKIaki0}>u;P_W1J#JdNh!O(9+K3`BsnGi(@ttba@pmhc*gUW>Q|-BCbQNI3A=}q}yfb+{^@zLXR4r=5z z($qH)@?qx}E0;5ui7ctcUxf6x7rUv0=O1fVf3W{s1H7iX)u~oRq#5fx^4n5;wA$)- z1hy{iv@B^(l0wa+hb1CO__IDMeKMM5#s9u~@vNM9`0XllON`tKZi3A$egA$f{QbN=<&u+N5MPB;zjr=?@>h}@$@!1F;)zol z+Zzb1%-69w@>M78+fynz3HJ_OY-cmu9Z>rQKK(s*De9M6;PG>47)d1rZO(X zlL^r$i@p*-{Uy$%tQyN)$DDVyD?MYy6)y~IWPPG{sHCn zbMXvSAHN$=&s!INJHl-VKB302EQqmIR0uF>{E6v)yMs{o#1>%(a5+7XxHT=s_|LiV zoAfuUe4ZAMhjBZ*#q=lmP8Cfm0}6i!taql*emag*Y-H-czU6Z6K*8@Mu7>b~H6x2P z4Ry!YaNO3`3TeTCGk|pz#+Z;sh0;%@W9k@6>8EJGS?3?gOou@+^8iy`uw`p-4gKO_ zA}qg%M&ukM8Pyz-FL=-SZuyBg#Zt@*u-H-5KayJ5IIB!BD|+L+qrl{m@Z^~E=YQwy zOeIG5oEy70T5ARjrCCJIQ+y5G)QV%d=0RlXN8K&JFh@plmtk($9SO4yi^*iWVdALJ zPua~zsxHqEq5mR5fR#K~Kz8c_?9?=hgu#Y{{74)S!a4*Xdc=4tloFfyEF7u7Mx*8! znpjHw?aOWXJ#bRp&O}%Q2}H0G+6j>mEDqb3Ec#AAMJGj+KtO@PCg!>AkIDmj#WM_6 zb|yG$JvaXNUyYx`A2Zjh8DglINWn==|3{50dvr_d01jle5#I)>d@L>hbN^)JB#;+B zBGKbZ>Om%cSII2P;&7xXWVkli0(pA3i6WS=^H`!Tw_Qk$u%ES!Fn6&n0h|9KYZ$qD zsgk%NOsXnOZ?&_^uqVl&k$MzrBVV_{TL=C_^Cg0R5f(-EZh?ZC93}6&8NfJ3^)E>~ zr8Z3*7k_*zBMY{gnsd=-1ZVrI?C0I*|1Lz?dLIvmN#QFk&SHJkY}dbk3HFMM(x%gw z*TmRxdB?Cv7=e}n;~+d;VU+jCl`&;VM6RDmH&Tz^Ur)tTx~BYFOmEvf zc6j@jmZByZfiH&2`Z?9tX52UmC?&YX25!*Z2%*9+cI+y|FrvGQE5MG>;)S}$dx((@ zltvt*GnrIXw!b(q&bpA?GhJa|7IRt3g|h`0I7F2$$4OkY(I2dJM>l_$0(J{OK7Xn% zts@|_Z`0s_83`+HX>_Cop?ocbB#gHJA08%?m9t1Bnz z7W_whwwM|ty7*bp@i0cB;JUiiICjo7n+&}>dfcb0%w<9yc8hmwl<3>^ofh9KPoqXw z6YyAmHod2NEcnbF^DR8GCy@u~Dml*%k#m9w`rv4{iGT=JhPfMx-N<5hm^!gp@^@8E zf4Hy#zDU%I&)(3K8%3-|suU(eb-XnGh9pDptSlvT)c|F|C^qez7xWXr3S*&=+~h*> zC#%kH(=Z@U)R0Ui* zBK55ry^=9#p+#ydLq@=;b)D%q5|S?R67!*jK#zH(f1;I#ed(H`!$t zH13yl*8lT8AnI7Z%tsvl5CAJ!^y))jo!j`CMM=|fTjX`ilF~EqdEzL3n_2GHtf{fE zaC|<~@z;inLWT;n{G;e1Cc?t-IzkS(5!|ldVXm>P#7ULU?vl&kfKT(pk7|JTu@ts* z69_L5DwtzqO0qpkgBJD8$LJKU1mfTK8$KW>B|Yosc9brD|2#T$MYe>IP=#x8(29Ti z<^@Djx$7VCjV$@Gv`mwixB9PjVJ?CQ6A&TDjRd;DOhftRVKOqZHA9wOW%5oGZnfA2 z&E8zPko_lUJzBsVf|WP8&M>jC$iUK+JeVkIWpKcklgS-lT@ajMs$62OucsFBdLwQBIdd%$O`h6{G$&>gbpV+hF z)Psa0-c%bzmRuoR*uUElfdCQc5Jry6~yw*8JZ%-0i}(M^xlW=KmHh==(C@C@9n^{M~(x5v*_ zotGv6zyo{0AmUy5GC)_dTC^Zbk{5fX1W3xoWGu|jEnjF>cblXwqq33m<=*rPJszmw zF+u#KXCh{z7K>>%=e@7==`>eUlLt%@+|larc-9>0Z+L;BWlxG?t)PGxrLh5?hTB9j z%Jv6ugR<$DXttF z^h?6A0YeZO1PoZe{Cb-i`R@&*-GTVG1FN(iAx%42GNh5&tT!=FwdN4s1I^i~k91lE z@+Z0n1LOa)es8U@nLA*clM00h_q*Wp%O=w13o~HjkcA}S61Vc_ak9_1U@@jT{iDUxVET~Z~l*C(?h+ZcXEoEpp%uQ6pYII zvOX;6I3AyaL#a(_G6G7S&0g>x)Bjp?dfcViwC)5FvYDfCvoKh=@DDVKm5%T3%CM%& z)f-1FH`txCq#2R!Vqh-3>+xKR3%tY))9?nio@e0`1$DfA)~wus{A@t!WI(z{rXj>2 z)Hc&RmTksx*g5PWc<-MbpyI@k7fb6;)jBevfvmi+?ZO>v1i&2S$WoF-q-R8Z|J*jU zzUV!^F6oPsdVpZ9z4fW`UmL^P$u?2#N!QaCw$bmlG>OkBsI4FCs&sulFMYl4r1+@# zsNnEWJa}SYw6fbd6Tv5=2w@0ejVAcWMo|7i^F*27RsAz~Qev!Rm?O>A^x{6aPWoN@ z#Y!l#)PUdqnX=nLzj4shC)?oq-O@Y*NrX@~{8}H}tp-kOHzXvU zgXtXwfSs3s0rbmN@?m}82pMd{SQtP_bN0U$x&R1nfgJ5`?-nBam(mN!`>p}c43dt+ zqA0s(qOfh)EiOFiW6S!K2M>0<)ytuq@UG$pol(uOUV*yTmNRGg+4KEHhUv;!vyk++ zBh9Qb%;6|c-t^N-rYaW3xYl?(yNn1qAPL$m5j{L|2L>8I5&r(z5f%{4#SB3gbqGxB zH(EmNpEF|TEd-lg#a1=BHKNQ`RmET~!;JS?RN3C0?kx3awLQJhdpREbH}qXzCl#89 z)c-Q10@baflGCS;F3mXGCFJz*9~T`JdL0b>q|_9cA9Gf#Wl@VA#l4`2q3lpoGDi|Z z_ahfxh$0;{*&*M#7AP3mFODe@<|1-DiZltoc9nlL0Yh>=&Bz558%6vAjBVeirV@bk zcKF+%xveXXDb?GY66K*#%C+#v+D;&VU&F*W&E`Ko|9p}G7fn~-;csKz+{RPb9K;PU z2xKLM=_eF-FvWz~SfUCC>FO!H9OTm426{@j zkGEWR96k6svzjw!>tg>&*+Qm^^z*izlq9Veqw;4m$B%fjN2up2zV%-XjI#97F8q#XX(P;}xist0*T*ILQ3qBFj)v@3Ki2 zg0F6{WepSY3yJKsm53a-8x)h=!+qhM6~1#+MjEPwnK3)-Kc*Y<=B(SoDP1k@wo@O( zA0TB4mu1si;(m<=$|xK1-G@646-@pYdZ8OCT6;Ubwd)F20@k04aB93L|5=}wBe7h+ zdxJYg;Ssv&(=W`a=63`_2^O>q8VVSA*sp=BUvr3B4lXQEw&@YcE|k?g_8_@Ijk`)M5tE~& z5=vt3+AdPM5Lt=b=*lj`x5j_G7@3Y~C7Gmn?AGokrS`gm;IOaj)A3k0e`O*?>EJ^d z_Y(<%huB+^Ja5=F*$SZxPd;`F9{cofN1qNq1sM7tvOe_^=OfjNoQltqVa%Ma}o}y~J1{ zLNBtdtbE7M8Vyy|c-ds`-CTk*p8Eq$pQN;LBPq^eTk7#f+?ytBBBZbKM#9zB826whenW_!lth+v(l}-Y@*czZ?`uh`%ICE*yuC z%7fAQzH_%V%E60CEjW9}W1ny!qnwZEr1rg8ya--DgYfVkbG@Q_H%k)Qw94E?RVrs> z!fmkdOx2ebzwM5=>8BQ?)5PD_vAB2n$2L=KGjhy57T(6=kYf+VTT6ZJgPGC7uaoUk znNv5n;w4(JcS;smnYRPp_l_#?cwF>P8SnH7DmLzH4`2&W7MCLPBd{tyE_cmyceifZ zdM=VcQb92Vej3z|GLg)YAzwtF?~vHGerW#bN~QB>P4~}B@`dKYG30dP5ovwEednck zm?*)O5&BywEm_=&%-i~cqI0s!0O0%iv< z0I6hkkSi_u=br=U0KeL4uQPdLbV7LM4eJl48OM>+{`>tqn!gktjNUHlYrb=MlF$PA z`$EWa8GoOrr=JJQz}VTp-}|3efM&?4lW$5J|G44+$3JAvzdv{?^4KZ98r@GBMQnk7 zv@7xc?4C@D|MyK29@e*qWmH zQAuXbI<|0w;<_e{Ap>O55C$!b0$B)w(iH;CSoLX@NI;Zy#{w=SgdjkG6m*s@D3@)p zFbwBiQV+V!6{gbV_Gu0%HyGHP&KydC%nB2@vIj?Sr3f}Wr(FSvvT+PZtKil^it2wRYl!K7}OB$L5@&8 z+o3=5Z?=54didoD_%SO+X}V06aTDsT4TtT6fVEe1Ulko<7ew4+#zZQ=eO~0f)_T8JJEurCZ z0p@`vCb_f+PMkag^}^xO#p&47A3(M3Ph0TVoeX^`C>{u&3w;Lcs4sAr3uxaLp&q;{CLx2V9etmOKy)&5*&XuZZPaly0vlQ84gW57zJ~|G7Y=3S-o8-@w|*NbGvt}_ z^qqsk-c{84`3>;312CVTmvfL8V4 zm(n)0Q@j`TC05bB_EkcOfvafA-F7)QH2sADQccv&?26fjUuqe@d(Ahp=SS?%=1 z{nlI0lMk3BaP)2B?U2aFgDS#MrRj;T3h=}s;Y1zoJS|Ry=TIwyHb}M5h~{(@{I|k{ z_IA@w{x*cCoppoQ3Ggupa~=(r;KSPC2ZEMkz8(<;GakdjBgdRDdea;Ke#u|w`X%M~ zx@QX_C}HP3I^Efhip)Qbjtax1O4^N@@U3)mbp^F@sLgZvlgg1$rmUgiq9cAxq97}5 ze);tUhC^nK>(P6%4|Nz0RK2$bPP}L5!C|dV$!yi%3S~JYF4iLp>tM92gGT5%`MNj0 z(%Q9m(h3F^F&aa7;&$W4muvzf9jDAWSR&VB=b_4EimS1t9s}!(j8A5!ri2)<%g>;O zq~eo-|N8-Mf^LNl_y1{q+ItFvDn11AuDTI38#P@X~I z8~q=^DFQW>GTaLNQQZ*mka%-$qVJ0?56!{~^gX%|YXU|b%8|quY|6J5s)&Kip73pG zh#%G9ow=T;sNFkj!RJp>F5n{up!()4bSQAvlpnNAoa2^rpxhx)LGYYpE)3t=pEB? z=WE0a17ZC#yFV@5E|)frWC9oEtpJ)xq-iC-XygMBw}XJ0X33jWA~vJ_pA8z_qwxtE zuk5SPyWc@id?I*4(6?Xjxd=R2$=Q{>$dgDH?lPm`_UC3Xawm<7u5~$}K!WhAak$ov z+q0FntJ=A?bX4!n*>tUGpn7!xb2@i-%+n$*zfdZ5S7LTgUZfYlUw{~2ZwGI&DiZjY zC)juUBl^F7Rc95CN?(;^l&pBBWkkB9Y3hh2X7m4}kLKlJ`t{yl8QO3PRwqUR@$^}g z6glYcuc*2`J7m904vl50j*Ce%_oG*M5z>8GmpzBt?LUurZ;O=A4tgGxp+Z zV1td-vqqs`y4r(Gn>pi6rzr@YJD;4Ktoqes+*oaK#o>`;sYgfd5TF0oki?nJK}*h|UGIlX|VpDgxm zz=j-h97w0q(BJhCpRXU!WHZsb_Dn+VlQ(`*O3dQko$NTC8vd?Jjg3|c*Jrn}8U%$$ zTBUa<>%^1CtuwBNq)Lw1Wo_bTzfd!>@=?3;r0!SssB0&qLre9qaJ;Ju)Ih^x)mNxc z$C(}?*}bRdSTg7C*CO?$ZCn080>pj2RElJIQ1wcm=u*Tk7kkb4;I)c}+uT?^&?~t- zh;1$BVHm?Mg`YpK?*%GBrxp;lxeP@?9fu%nIX7?D-Jm>6gi>w@p-~ZaE?Sth%pdG!?k+8@jaOK~E zB9?z+^JZhsI7&qY?!0IFL~p;PAQu@YlYuJbE4Nv2F-G-7sALmlI)vnP@;h5MmV6!8 z)z81*@<-7o;B<#dT0@x<1fbC)I4 zZ&8;#zKHKeix-RQU>YEz1N1&{(rY)wRpq!T}w zfJ4GMpgw>U5W)bYS2@q%4XT_#lUIHc!gD3f>>TC@(~A#);4$gK@qMBKsAd5#7|!QW z>4~}YHYHE>#7YnKP+Sc4!HrdGBFkF{FhVlp$CXPOv1t8iwUx2p+Mb*Kvl8c6&N0K}#l)QrkwJsSE6Q0bL8mQ9-P|a;Ka=WP%pT3uno7Se z6eQrpeBG)u-#{9R`Wad(DZDN9n7`rN^r9o&8)I=+7y2UX9&s-tjW_7SIY9@di^6ow zRSel8Vq&`3!tLIk+CxD@{|}f!sj0og0Luuc;8-r#(iLNQb9vsR>2HV4g)TKh+qf;2 zAH0(&8!u(Cyos>>8~dQt=dJH+wWYN~&eOA!DBAxRbGlufe_IpY^3n0gl{Po~E^ z>U$Uo(#4ds9xGkE5?`?F+i=|@yb2g8)EseKs~OIM&OI`5%!CS0uZJkdmfaUL4B(|9 z1=l#RGdOm0sC2Avu*5_JhEI6U7ihsiY2jBQHR%E$FA8G4F2TovS<|Ltl;F9%*fIGp3ut+P#g^ zTF@x0A3w=c)})E$@9aFfEHpRzVEI)~2zB@^bNWIF9i!+IPP zI$)_3?*844<*d=Op6`3ABBArkzfM$8Q)*I&L5vF$>c%6KIl9`!u)XJxl=IS5?4FP1 z*7p)#sBu4_8)8tx#<+K!wBZSzi)cNbyKuMFzxg2&kw{&4sNCjYV^s6LcGtAj8(yeH zc*sg`#W%on^jPoXF<>$oS~GYK{OXCOe`tT?>u zek3!uaQc6$MW`E7+p5%QuuPK zD2u>6Eq|>16ez(O1#3ziP1}5;wAXz` zg(t~z<7ju~bSZhS4oM)n_Sw&}Qe!Wg`lJ#+NEL1!??Sc{{j=0~+QF7SZ+FCVEWGJ> zn;xMO2Z|XVEO~dP0b6jx!q@jTO4xti*{e9~t3(|+mxTlCel*_crzvT;F|(w{V>6mEc6OdagOm7#hxHJC6dt%abDHCqd6O_vSuEI}P}_ zn#jS0)nUFH>BFqpThwSi3S>_Z=Kux#jN&3ui3v%fnDIq{P?9j}6b@R|p_2jT2M9;# zYOYCW)k@Enu#&*ONo*}))rObXR-_)uRCuDPv5aDEF~d%-Nu4K55@e|qihnsg--h;dQ z+K2g%PwOUC!ZQSRk$dN(oJW_6ic3e7Y8_(&RVwH^9P3NhYlY~oXs=&&WIM8#tSgcc zXJinH2KPprqM~&~J?#bEp+Vs%sugWF4pR9_mYBPNEaZrJa?Fac(GzlCo*zHMal8v3 zXa1C_x)r&&FdWc~<_!rKS)QQYdi=&|zCY*uW3vo`XBFa|!$@lwdAdli$b%m7tEuTl zMREc8bJ z(xUo{PeVg({b$56V0Y31bAh`z+gRkh#c9t4tehS91J@z9N6&fG`-KGG0C$~$m;zXs z!TFXn&4(8g+c9hfN2W9xG%Bv4Td}zwWWZR z^>Y9m4E&U$`ib<5ZYe}_d1o@*)hY$40_rdnaIjk?NSS6DMbHAaUh$~@3En1Z!;~y& zXgw-jc3PR$I8vaa{JHdPq}`wNSUK8atg z_NN)VK{SEEfbix#s(J-=-XxF|E@8t-gJ9`HLOI95U{Q;rdAkUD`BjC!rC`CO_ZOWHU=h##cLK}`bxvDQ^Y1S6EF z|LTRZl|ndr-Eq!;%1|qxHq=?}k%FGaGR!bPf_ynsNPUCo=3OD{)zvEy&O!X~mZtwp2Isd8fB-$QA zGH}r!t*faBs^Xsl?-el^BD55#lrOP*k5=XzL^TFHuk+GI>p^*)lkp78xu_zR&-ZCUS^;AY;4s5rVZPNx>@q9 zl_GZFE`=@x@3+42DaCz$Ujx1=h*7{CfDW`{vn%O31t5gpTo|bwLd+8N4$piy*jgMq z4r}FyGTrxN{?{-139lH@m6WJnb7u_Imy2rN|E>Aht&`ED#)TEuX>qg|YFC2ZlAJ=a zOrIZ&=#*~6nKI1TtcHVR#(YDZgR5Wu`|7i|L~pVMXuvBo3VpADsi8u$qr-y}g!*}$;juq{$T2Z3pn@!P zB81An#1}i}?$#YuiWo;Bud?qhexdZ={qv?EE1(YIuFc3q9 ziqRT5fSL;xLqg^CT@{gKW@bj!xlr`yszgu$4+i`sR-rQ=Yi0SyN(NH>E!IyW0u zTJ1tnF^Y=)4wItb1{P4EK|@ODN0=l62dTGm`yoz6g&^b6qaB7IFolfdKO>gIRn_x@ z^WO(fv7BE`y775&aT_EQK@vFVpT}J~6foX^c6hVCV!Lq98s@>UK>o_vK#=Xh89p8{ z{vedID8>sGpEbBQ_Ip0*GIW-R*IPd^u?5-)=)JGPeBEM5AEx{7uYW8tJBvzeurjV3 zrHz}WqE}#t1>Yr?`a(Kp!2kl=L3LVH@Z_7ge3VoevViOu9s%zGOr99J)%tB>L6wyr zlLuQTEW53-yH>bU2jUbTpI$!xXW=tBV1aF@;LTjLi#awyryOum_{s3W4<$OiM%*^7 zB#`IyW6&eD&dl=AHVa1s9URzHpbzTEerh}29tC_k;8jlXNed=t6TuXJn*oRcoW z;&AIdRpZ(i(XN1zna>V!g}gIV(_CGhiyR$#L-ZJI#ae^2(~wsb0>*g&AJr;FQ|JLa z3T`%X2neG8nsK2I#>W1_!UAgg$3c?Na}6ZA(Wvv19xV}!Mccp#QWHXk#Oa?1XZhTY zLH0s1YA553=FzY^OpDl$Yg?R7w8|%CqPb?>8EKa!2L#D0Ura(G+a=iC>%GQ~qsmtB zsDa&hZL7Jt$E+~oh)qfO=v|%iBQGrb4Mm(Di#v3J?-xg>|f9MNw&NBlb~0U zI-DZaUOCBjC{v^t#nk>|b7twcrfTlhVedKyR617&E371}NQ)s@18%TQ-+-{##Q!7C$P@C$e#;E zw<1r@fmm-1&cqpz_Dzq9KJ3Mbs_y1S6167W<#BOwDRtUaj*+4x)ec}JM3h>ZYybB| zf;)XXL)__;fatEi@z5Bf2|9acJ3j_FxwZ4TG4LoKZg(lTpNI&hjc&X7NP3_dy{@S4 z7eq@kaf8<%5IWy(4JDO3d<Zg;zKr}I|8Bajk=dRH zb*?y}lya~~0xv5{P$G9gfdu;=ML;Q^&f{*P^6^IB89?Dc9hN5VFN?b8!5(Z|(LFI| zbpgRx&vqxmVThbm$m)sJpi%3oC~#x~gN}GaJ&;#vrM!)5W)$cz{(GC>Tc76&G?1!Q zs;j|e=8Rwoh9fPl@8wt&^2`tns05<%N2}f7fv^FG$65%i1;tl?p&`Gydk-%hkOar5 zW=$^}+_)BtdS)xaF$gqHux*4xJ_eouwq#Jhcaw>9roXID5GY)@jRLc z2}hGPh=#$6*M=~e^>{uVM`Ua5*PA(XceU!tVLZZq#{%`KiZ{0O0fg24W7#It-UqV4 z9 z_^;B#;o%T7zGB?Y^G4Rg$gt%FdNbPd$(T{F6JjO_gH0eZV}<5v6>w~itf zFS-Z}p+fb)k$!cPyEA`-)0T$kD$-G;EB~hQd+&^upAfldm0f|qC~b^d-eAHt#sKQc zf|LBVYe~GTAx_EzfBqp8Y@75(6ZaiIAwgQ1`(^`Q(SSOXBEzM#nT7BKE$%XwP)`^? z|3y2#_&FYR!e=vl?F#?YdvN^Zqygym=jUKVR|n4sPgp5(jjU|39<4Y*oP~!$y&WZ> zwlhZ#Hij$h$bp)IEX1t+I+P8AXPL}uSs+4o_)wKfR{?o|tsgKZTN)7n60v(4n~r$s{d z{>KJVBw3r+H?VlCXm|T6~&b*!< zxY>Q98>XLL>K<8le(|V=J%SkD3SrP)_ zh6ffgdhez?qJ9-qGE~HrY{7RA?{YF=dcR9vierfiIWCX2!P+7q69hHvVujnxrRB%Z z25w{EW}e`=m_+eeJm&^6M-SHbqpD26Ea`B)Be&(f9o+uLZCy;llnmQhrMftRt4Sx8 z$H@E=wl?uEPqQE7ethYm8(=&7o1CDy+d4Y8#mza?=JCKzP4fl3UZXeI$y&}!aoEn9 z;+wLxSCy%KUXLM7`5mGcDzlTEO;YUzD%(H*GAN{L*{ogyklO35-0cG}A?}1wVa(9_ z8dw>MVFNkv?`VH#Ag#L0ue6Z=$R(JE;CtmURXp_OHuoXSaSR+!NWE>fS5E5H0+xx^SHRfQvvq+N_$H7;C48 zqwBCuM5?bbuT_g0{I}#l%Fsds@FfEJ@kAuR) zi2=_;ZEI26A;(NUQ?uSvjAxTt=4GgKe~mQ;p8TC`(&gs;>m^9*Yn`%cZhdW?ctps* zD{?JAhvGEN%_RKiaH0ubY%(gcjeP=W!`(!#x;=nOI5@@u zHP)O=?(M(Bn(v4J#GJ{d!NtV-)Sn0v#9-f+*Y(OjwiFG9NCc>Zp}0*eA!_Km4WWv+ z1yW=X=%S#0iwWd8^+emEDB5BXt9SR5d&IUD(=A7{Rmr4TenB-im-BX3ex-tyj0I+>=3wVAG` zP~mN_>GD}ahIg9O&l)|UAuH^5g0S=wTnxKt)UgNPSe1gfU(^!lzezBORO({8kIM@9 zAXba~z@CpI0R92+HQ+EB$`{L?oz`uB3jr|k^1CGddmC+O5_agY-gStra1Hh11Y7f} z-d|RCKB_i|3;J`Su5RA*Z;s)q;|t}oO{>i`V@{>GV2(mO(byVQ^PuVqz-4^;I&q`A0_4aO(7B8sZ0q&4g#Xjc|0(asHm|0YwFL z#QQs_HB$vR7<-qJw@IPh$g-ksZ%8C##o61BD z1L|*#(Yce*R~OgEGdWVhQUrN;N`C%T(P}bgl5Np`PSlV@jhOLyqxm7f^R} zW!&fCx}P%J0FStWNv~32GT|U(hhl*IAq1BNYB!*upa4S-lyF!%Z&#eZ0!JwwB#KBo zkA$WO8@rbvK=1)}aaw-*^9zg)6w*?kAe;5i@)&b-!^RaUu#j`F3pI1uIX93_0pvUd z;@R9}tX}0-0A-7R>vVF19-I~Y^{4Q&bGn*oL7J}TU%zzPzNjMJhd^>zJVMI5SOv|W5Jq3o- zK`#pDzrN(Z$KHRE|FJ7RKvAfxow3!E5akMr^!79DC*`8K?ks;}e0n(^0HRtLKT=<6 z+1+@w3J3O6<1$OEge@w68F|LRU(%^=b{f+puta5QaFbD%oE zBo1jr;Dzs4GnzmB=u>h?ll6Qr0Y>bS6P7sBd*AI{mI8a+4C<7kk|pooUutpQQam-_ zoQ8v>AH>I#LJfSMAqVUl7nKWTj?9NjoRXA3C~zK<{pc9aK171e+HXwBzoTL|teB;{zo!)V3&vI&Nn5&!v0*k^wyP0W=(fW=_U>ovAk1 zZLoN81DkxbV}=s`iL7TUYTpvUc8{ca=hyATp6efBIX}}F`f^LUmq=~28omBu=V@*k z;ldm<8oscwiqWVFB9?h+XVaH~>a*RP{Pi-U%Zq`78p8q|pa}j|04GPQ!uB^Np3N6} z$zrH(@}XA9*l7P;{*02SqV}Md@Zm=XX6Upp#k1WHA~=?Nd!nh_`O)j2XtX`r7F3FG zlLQx?OQJ1O=g8anbmdsK4fflei}*n8x^d%1pbZvj_^454*|-YM>A_hZ6YCtpWJcO; zt5a=YQM(3v2tY%qB??^HFdlUoa8L(Siy=<^&(F zt`V(1B?w5Wp@mMbk!3j zuD`>grwlX(L5AUTK5oB)qGR5PPxEoaAO3O^?{tN2ylsu^&S=?r)W-?606||_=e(zp zpWEA%dV&=4E;oclD?^GDHc7cm{~wIs&NRy@_VEg$2EJTZH;~pFJ z>~8DRP6Zufn+$#juW$M})D0T#ZSWAnSJii&@ZYl8UY)|(o;ZUTaID=w1UeBjW;OX+ zbtIN&KrJa05G4pB|LZFSM6pJ0!eV~AHxch*|qD`gytj~ou#yW0Ik5V-C{`M`&0yK=s%tS10e)_ z<=nQg>AEn21{dKPa$nLd?x@69{xqyxyw5+{yIWSNhLQ(kz|W^>VX)kBkdFc@j)Bd& zfp8dgN=g~fW)1myTVsmXL)&bTeZYq^0t`X`TtDs#m=-YK%`I5b%n%f@l$nm?cKw<6 z5cQp(W9kCqc&bhWE9PmqEd(3Gb<)B7<9c=7-1KiGPu>)}%UzfJ=*}xB!WN? za6YTkcK+}X5VSgvACoKK{R$Uy3NX`-44@WnRSYOX8MchM^?nW@0avhz`b|FyFb-&8 z;jlJ;a=AlY4Po%BaT(jreenK!e@5R^xb6)m!chUqhngOm|6SmlJVqIjkBzGA(~~a6 zaFC!}ju+;2Nsn%P01I`fm(%h|;r*aH`K!+$s#wnmxaOWyE5>+IpON?8H@C;q9rNlo z+C!ep^5*q-1OYLRt+4B|g;a+GgohM%Y=L_l3v@%1#n>x=O`zie7#~j|45wO)%Nl}f z#{Et*OW&HD79hP(NlE#WDm4e0pT z-;dEAbOpEX*21)VE!c1>R`9ec2ZwPjfaKBb4Unk!FKPGw{0u=?^rQQaY6GAKOF--y ztDR>ex#K1e@NSe2?r9_us^V-+Dq5asU{hvP^Y&=W}g`1KLlQ2)KaoRIXrDuw7B4&}L|ySKIcf>aR#cQ*T1xfO zp|2AxzW?TlY<_is)q@rc@R1tZ+#>fe`T0*1#H?G9L#&y!OUp^bpThDSJsUHVWViik zwE_|ObEB1*PnP=_ST`Bcq>-?s+a)Szp_8L0M;Jr_7(zE+49avFuJrMEw5dC^QR|gm z)M;zp;Z*F%)XIS=zT=ncqW4paSQ|LSjj+IBuhDRRa&UJ9k78nd1KxzBF z#3$6+PZ57%v__?c>s4yBKq@ren5#nxZ|(6&1yZ6TX)0boFc&SEoOHo(N}RFi!Fua4*ZXmDDd2 zO1`YuvAqAj#cxYq>>MvRc|*5FQl@%LoK= zD$&yo%kX4PpKacubXwUYDz!|%l?io(pq)o zhVbsUMC3d9RRGV()AK3|1O}m1)+$At{dhiqrVYjE%$Wzsh=+IH_--)g-lRLxLYTzg3TSG&JtMH&EY`B%| zm;ulTq!?i03v>Z#Wr5*lLjz5RF)*|FED_|u6FOcL`7pNd{JgoGPsb_zl_%J#-f)m` zq@X(bEYvaei=}O7q!+ZC)UXOv0Yj!bzpnUYn%End9- zEq~N?5U4+B7qNZLxi`Sz;3Cjdi-f>DG6E*hQ)oKadwD9ENsOq7S!ze$Nk>??If#jf zEzA<5XidKGok+;E!xBTg1i1nq;T?B&UIL`_Sh(Xfv>Fj2z0fD6oxROgc61i16!7N+ z(cBCWH_+idbjL8b0JZ>Rz}#bz%~n7mJ~Z#8sAw7u4?=AxRgPbvP2!n~NA|hFK9d`z z9E{Z?o^1)7DlOQy_NbF#so;FVlfYa+!A!UFc2It^P>;~qj7>pEALbf8l>z8x~>n|~4lf`9Ct>;%n z-n0aDW$TH6PQ<$@#Asx;>j&yP4YsNg@GR26W)`}*+fT>nS%*-RIn)Su%jy(K7al?o zE*Mw43@$#fxKGhGS@LqGgBvXYLVLwt6N1!pVjvd9P)%q#RxQc}6x(^`Gw?YqXAM(> z#3RK5XCf#&o}av|oWwIu7%CBt;%AS}ItLaB3B0Pth=htrsL`lk+Xdg8;QDs2w@=T zm-XA5XOprl{yj)Y4!`5cdY&nXB!cj;Hi(??{-%W=hbo%Ud5qksb`sBD;03xo;MUij zgZKC6*XUV#O+R)kzcqvi9e~29s7>m95mgBfna(5Wk|O;Xg_G?1ZBYOuaXjp`S)F`? z@>O6>k|;zHB|=`+Buv*L9B!{n76}WPzm9D>_NVjx+41&X<3tWAfad)worqYvE>U}n zQ&7TQ-}%)c{)gwu9*H0IqLu+Z^cuX!>&%Jc?AoL}k^Q}$x!;)tcOH9MboKCOQM5(I z?-<`~ZdW-z9zW{xno_4O6l_Ch{tW8COdAxKK#{rNyLl0M1;{UJCt{Ngc-3bV7n6~) zBq%Vphg!R$nY~1ol65?fN|P{J{WDefZ3I{?l6atW&Sb9c##7IFP`Uo&!;{{4(x@)P z`H6zm3IH3>f53?Zz+}bSo%_tK-Y$&rlMw$Uu>^Y#GXMR_y#l3Fto*?>Uiqt4_)65) zGvS{5Binmsws`$B>pt+WQTI8Zg;9UK1`FLm6(I1Y^cy>tITkEwN4@T+C$Lwf=IS4uItmaSG z4-qNS97UpGiI6G={HhFN$t-n) zy6NsWYJq4%*w%`QU;}}Gbt?in7i*9tBj^)?fyL_P?lh zn&UxFpoF6SwZ||vg?CjX#7)6yr22-J!77%si!}oDM`LUg93g+ zv5T;U!ij)sS)>QHD_EldT^bB2U#~PVqkJgnyJ0&m+B^c=;#Oq83$gz6TwewP-zt38 zufq;{$_%Nt1|F20RaUhBKuBGB?E&k(`5Y7;i%;oU!-+n01IEn^OeCfYA3xyN?QGl{@13ZrCY^)+zm7ZJiKbe$ zA?LR?u$@rFH-s>Ta>vJ7APAf(H=I78$7>7h_GsSGC!B2IfJ4EL__&!CaSCWXp|SIm z?+a5HvCfmPwhuoM&(uWKjX1gM%JItU%hu|0aI!3#fk+&g9AxBkhKbq0unPo|^Uojo*(EtK?R?FYqsI^T}J zne6f#c%}}^0-z88q|SES=-ubEsn!$724BdA@obS^5V?irJEkF!eS}T}L~9`tg%yRF z95c{qAUrWJSO5^JuEzm}zqL!1VBJ#S1{TG(5S7=^(?i5y9_TjHX#-#J3>^t=FxpWo{GJo*`1AGA27)fMIkpOl1FXy_zTE|so3k%lU0?Op4NFDAdB^AeI z&g{usiQlm}21rIqu7@@Qk~CW;JbPWsWkQ@nAiDKet>O}bO`*|NdM-`qAChG&R?nrSP3xcJHJNgFh$d~v%kt1vXSXpIcN@;c4-G4 zGl&PJ>ZmVOuzvm_44ulUhZp6Y)RNv`7Z&UuJO!iZ!i~b zN)o>@Dh&`$H*<<Bv4D;2JZ zj95JJ$9=8~(T8Gs|5w@l?Q@jD*$ZnU`O@Jwt2a3c9rfznC7;MY(ebb|FmT|oxwCgJ zWqYqZ@8dMB$AIWO)7Q*p=7;ANhby?DD(u=qZnq+vKVX<97;Ug|m+yg1mj5Q>x_=h$ znp`}QQVZ7Z-IhP-A7XMNzpwkytDf+%c&Hk?HsvjP^Fz&Qv!?x{+u(}RiM1=)m;9TaU*J>5R1)a_NP5Ia2@ zK#f!+sP0RQ3ciF$S*)E12|j(ejlGq5lbBb{hrO)$hQ zj@uieR5wTyDW*u+d470a-T{-A?+6s$)ghqYNNL;Em!8Hz3NT;q8YE0x*OwZP9SZ~- z==>s~X5AznzHb3a-!T{}e)Wq=Mo6!uAudHdshx?LIh{w{Zx3T4lwyn7-s^9=bwoik zaalbHwzYlFjQq7gpaPa0!6cX*!D*t}5(*+Bz_cO_mF%}A=e)A#w(#79t6(+GW zO3@nvof~quBht#@uu%B1C-1>huu#=Al!YAbh&<6b-CKziO^c3XS;Vu>Tq4eqX0CwWc^FvY zDtP@Zci)BEEKw`;R=(p=pO)ZtHcPc^A_a$~N;rI`I>Gk}2_+1C|)ku97ymaOCN~XD+GzV8Z3rt=TTC zK?*)~jp=*Ci#L16$AV@rs*N{6p50DxGD3@QF;rsu%vM5VwvU(dy_bea>X7meB2kY9 z6BZu7A-l}E3WKlL^_5BPSe(u5K4F)`E>17`WgvM@nS<1uhj5@N@wtfrZP8(VM~%6t zhrYxPquO&VOY>K&;Wpj59M?L&C|3nzCFtwfigy*Q2rSZi#6p11SF^J}U*CgCS4Was zDcYtPGIMXd6Bh}v>8>>QwD}lpCcHTimeMev#2|5s2H5E2aI=h^BSb|<0F%HmaGx4^ zrSM8%a9>jUIv3YwcVS}dDmCFd zc)}q<#k^e|$#CfAs60JYpeIlcti=K$9nIy-1}uB^Hdy-8?`au!jBpO=S}wlkq~%&d z?~Sh7(6n4r)51^7e-2+towUN+;eQjarK;y$m3{($`Rw8?wd*H$A8mfDd?B>P_po=Z zFIPY#<+<_Ai@9l1ura(;a~}n4)fwRYd`Ht1B!`D_i0{5&PuEsEORh=khao5e(&v26 zI|z1ggk7n=L&8=v`|v=t6Fk}O+7`B( z4}ITScgBf?2F;zWS-Qo5e-MaNRnOiP#^-wy+qCi$%C<+kzpEcJs2daoL`l4-wQ88z z)Av-Wfw9&CzGi0MtZKSBbyCc!_jvVs4{{%uE`GBy5vE#R7<&FJP_2(!UmTW6)O}2m zd8}<1zu1rm(Cl2&n z8cs~lznaps(I;E0Rvu;HTM=||NIPFph~a=Iu&}cANLuR2M3x`beJ!Cz!?US8>Np82 z=l!dTpNj9R{Pb8B$=B)(|I=W47fTwhe(b6w!Tsvj{F3jIf{i7A1OTD6ICT7(01F}B z5LZ&aN0RI%#62#|#^g*N(DPa+9){LGJ5k{5A@GF-mR-j6B_8qHXDb||oVAnLhCgASX?8s}?A?}y zOIHaUSzW88&&!tL$kfcf$F_y{E_~TG#o6 zYm#6i&wK)b9)4PpnwB9l(@e*>Z-q-tEA3cT9PCfy3OPE(E7>97U-Lym9po)~$s=Zq z*T1C?gK72M5Pfs?GR>yrW8>+?C0Ej(Z710dUpRv}4OX8{r!WK70aYh2&301j_y;X* z*wXjBQrY&Q+NjXtGvqa0(V`taXu65xk~}C8FipWzhk3+2^5VU`7Mla8SfmPO>q;Ze0_}vP_-p{{9!tV8r;D@CD7= zZcgLEbcV{ng2U9essW=LtfH7p`n2Ruqc0=V8MfbTTyS~x<3nqtM9mV8{=NI-%16() zYAjB~cgRn8Y$Lw}{uGBQ7MM@h)J*ghecBwkN14O!np3t=z1T7Bz-$o6+GWG;|K|jp zRk#?XQ77u|^PlY14xDhb?9~6y?rf)9fHB-py7MSKAL$`J&h`#QE~YrHn+_jNML%*(=r%O7MXL=+ z5A)ididdJ4z1Z^1*V#d0yp$!dN|}m!sKbyaot(eO!0qto0+n($p?L4NWfG%V?@yf( zJyQLq1YJXQl}TIrB|F#5&2CDncfPJF)0O9ERN*6PzILJE!gnt1&hg}W$kiQ20P|~Z z{AQwz(aI0!?`SeTD>#;=rGUVrdKH3PcPd_ZR#usKN{Q6E&QqTiXIB!`UHRe( zo9}Lobep(Olapsu*&set1-7qB1#z}jA}eWb*gowP+)&N8UMf2SJcwDixd@sZaZq%O z*M+NjIqvAZX*rKE^xYakW|V5!y(jSioM0cra=wl|rWVb)S_#F*I0&Xs_ z!!g+dp0o+}>38e2J0rja*Z<$P&^~+? zqRFQy8Su(uMC&8pXDJ{N;SZ`PnW3y2dF8kiEnI31W4wms0R_XS+DPo}uIzPr=YQ;W@)VJjEoQ`T7qf&%w!V zT@Dg)+_v8w4+wprA zNjyq898h;u^(Xh_v37rjQUB3Ly=jUsU0jum?*hps2N;f3JKtFvK5>lAn*-AQyrJ>O zgB$g|0{FR_XqFpr3T|X{*5n9~VeJ|&e!9yWcPMIil^z3f6Jtkybjk^jTI}xbJpfPO zM?&j*=H?f~8lulkU?_@N-MOB@N2{t-XcU|)0PzSc-~1o@TVkHc-7(F^fb)(_TpNEe z3M;NX_9(i$`a~uu33LiTn9&-%NUI7g{n!fD_6zmENCK!Dyg$wH{)Yoe3Bn|{MaPWJ z#lw%zZQ>woiQo7uFb5Z=>>?dANCd7hfxKHVs*0^xNlyInTEjq*ST8RRm^f&dS*6H& zo#L6J=W#rar{H2lbGeA;&PDCKyuE(k6$6BNw<+CJ`=%DW=Rz`ti?Ud|$lQ6eGG*k+ zfDQw@MFMmnQTemP&%`MHg?2comqvNQ~aSLGQn4w|yFGPSS{CMX0Nd9Q8NWK2`WhJXtojU6H zip}T0+F1pW;%<|3@7YTBW&Trn&L+8*-@p1jG7$jIY`EIcgq2axOSkKA7(;)(+je z2d92~Q(|#I*alG;-6O!jimJUS-_VIc1U^*+k=%DQn1e9@F_aA+cK9KtOst9j`DZl8 U+)ro|VqpLRPgg&ebxsLQ004b7n*aa+ diff --git a/assets/thumbnail.xcf b/assets/thumbnail.xcf index 4a9ac7b3540295907756dfa09ea6ca36faf615fc..b114bb58e6f615d3b61ca1e946a1554802017c26 100644 GIT binary patch delta 25345 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 TYMG(_ z&Gu-!<~-mtw<+!?c96ZHVjW=Xp2R?LTkM;ycCt!~n?6(7lu;`xA>BNQoajrFtA7xFr zsaUMW&8(hh=Mv%rGVU-3)^l_r`LlQMl1$*{7sR)0Y&z-c)z}^02X7Kv$=uM}uQiFx zZkW9iN3<2oF(+P>{+t%#aU4wV%n`)J&td5`TujBh_9WtWIJlN~<6sgM^|&Jzj~&7* zBe5md)u)AFt2yEd@qKX@MjplP%dziH%-V=K`9UqkH`+a!itoiXbiA5^Q}b~;d%`!1 zHTErEMSkls{3;Vq{(+Y&@E(;>@6U2$wQFZg+92L_boQT``Ur`qk}+2XK0`^>Jobn> zj-^oxdou3zH?im&ENQ@%KHSRYRBPuGpP@qPC5fAzQx)9*e(%rj0Dl-0@kh-gW~mi_ z#%$teU&eeY{$NND`nnJ!Cn@~$;Nwt)zG z#<63u;5#h*6eqL!^fx9F7oWmK&A5)r>RZ@DmcEy@(d$|>pdo;bGO_RF0vC9v>sJTT zfBhHag{bIFGV!KPlIN7@&GI14Kavl~_3!2fV@ol-l-vmY)g55OP%@)OBk@q~V2p21 zTqNV>+#z1bS{gS0HI4N=knur(`l~eD^&0L;#p-lNXa5=d{vlDDh+jsDtf_I3${0u9 zC11~OH;z*g<5WlT=honFlkw^Z44iZ|RgR$iB;yB^lGt;gic0J^0Ja+%P{-vN%xUahlg@KBuWp^E*v*n(j2C KI#6ORN%;>w#$#y! From 46659a474d66bfc22f0082c8c7003de67a3372b1 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 9 Oct 2023 11:19:29 -0400 Subject: [PATCH 05/56] Unset battery charge limit properly, fix #127 --- backend/limits_core/src/json/base.rs | 2 +- backend/src/settings/steam_deck/battery.rs | 16 ++++++++++++++++ backend/src/state/steam_deck/battery.rs | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/limits_core/src/json/base.rs b/backend/limits_core/src/json/base.rs index 803d8bd..c08372a 100644 --- a/backend/limits_core/src/json/base.rs +++ b/backend/limits_core/src/json/base.rs @@ -174,7 +174,7 @@ impl Default for Base { super::DeveloperMessage { id: 1, title: "Welcome".to_owned(), - body: "Thanks for installing PowerTools! For more information, please check the wiki. For bugs and requests, please create an issue on GitHub.".to_owned(), + body: "Thanks for installing PowerTools! For more information, please check the wiki. For bugs and requests, please create an issue.".to_owned(), url: Some("https://git.ngni.us/NG-SD-Plugins/PowerTools/wiki".to_owned()), } ], diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index d03bfd6..548a656 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -586,6 +586,7 @@ impl OnPowerEvent for Battery { .unwrap_or_else(|mut e| errors.append(&mut e)); let attr_exists = MAX_BATTERY_CHARGE_LEVEL_ATTR.exists(&*self.sysfs_hwmon); log::info!("Does battery limit attribute (max_battery_charge_level) exist? {}", attr_exists); + let mut charge_limit_set_now = false; for ev in &mut self.events { if attr_exists { if let EventTrigger::BatteryAbove(level) = ev.trigger { @@ -597,12 +598,27 @@ impl OnPowerEvent for Battery { setting: crate::settings::SettingVariant::Battery, } )); + self.state.charge_limit_set = true; + charge_limit_set_now = true; } } } ev.on_power_event(new_mode) .unwrap_or_else(|mut e| errors.append(&mut e)); } + if self.state.charge_limit_set != charge_limit_set_now { + // only true when charge_limit_set is false and self.state.charge_limit_set is true + self.state.charge_limit_set = false; + if attr_exists { + self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_LEVEL_ATTR, 100) + .unwrap_or_else(|e| errors.push( + SettingError { + msg: format!("Failed to reset (write to) {:?}: {}", MAX_BATTERY_CHARGE_LEVEL_ATTR, e), + setting: crate::settings::SettingVariant::Battery, + } + )); + } + } if errors.is_empty() { Ok(()) } else { diff --git a/backend/src/state/steam_deck/battery.rs b/backend/src/state/steam_deck/battery.rs index 3c66e36..19ce82f 100644 --- a/backend/src/state/steam_deck/battery.rs +++ b/backend/src/state/steam_deck/battery.rs @@ -3,6 +3,7 @@ pub struct Battery { pub charge_rate_set: bool, pub charge_mode_set: bool, pub charger_state: ChargeState, + pub charge_limit_set: bool, } impl std::default::Default for Battery { @@ -11,6 +12,7 @@ impl std::default::Default for Battery { charge_rate_set: true, charge_mode_set: true, charger_state: ChargeState::Unknown, + charge_limit_set: true, } } } From a8ad9a9e6202ae09281f92f725cec9a0c5fcca69 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Wed, 18 Oct 2023 22:56:13 -0400 Subject: [PATCH 06/56] Demote log level of max_charge_lvl check to debug --- backend/src/settings/steam_deck/battery.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index 548a656..d5b800e 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -585,7 +585,7 @@ impl OnPowerEvent for Battery { } .unwrap_or_else(|mut e| errors.append(&mut e)); let attr_exists = MAX_BATTERY_CHARGE_LEVEL_ATTR.exists(&*self.sysfs_hwmon); - log::info!("Does battery limit attribute (max_battery_charge_level) exist? {}", attr_exists); + log::debug!("Does battery limit attribute (max_battery_charge_level) exist? {}", attr_exists); let mut charge_limit_set_now = false; for ev in &mut self.events { if attr_exists { From a90932d8134e6bbaf3302c3462ca37eee6583f4f Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Tue, 24 Oct 2023 18:33:56 -0400 Subject: [PATCH 07/56] Replace pt_oc functionality with cross-provider equivalent (& resultant refactor) --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- backend/limits_core/Cargo.lock | 2 +- backend/limits_core/Cargo.toml | 2 +- backend/limits_core/src/json_v2/base.rs | 267 ++++ .../limits_core/src/json_v2/battery_limit.rs | 69 + backend/limits_core/src/json_v2/conditions.rs | 26 + backend/limits_core/src/json_v2/config.rs | 8 + backend/limits_core/src/json_v2/cpu_limit.rs | 126 ++ .../limits_core/src/json_v2/devel_message.rs | 14 + backend/limits_core/src/json_v2/gpu_limit.rs | 132 ++ backend/limits_core/src/json_v2/limits.rs | 28 + backend/limits_core/src/json_v2/mod.rs | 21 + backend/limits_core/src/json_v2/range.rs | 8 + backend/limits_core/src/json_v2/target.rs | 9 + backend/limits_core/src/lib.rs | 1 + backend/limits_srv/Cargo.lock | 4 +- backend/limits_srv/Cargo.toml | 4 +- backend/limits_srv/pt_limits.json | 4 +- backend/limits_srv/pt_limits_v2.json | 1250 +++++++++++++++++ backend/limits_srv/src/main.rs | 54 +- backend/src/consts.rs | 2 + backend/src/settings/detect/auto_detect.rs | 496 ++++--- backend/src/settings/detect/limits_worker.rs | 2 +- backend/src/settings/detect/utility.rs | 4 + backend/src/settings/driver.rs | 95 +- backend/src/settings/general.rs | 56 +- backend/src/settings/generic/battery.rs | 30 +- backend/src/settings/generic/cpu.rs | 54 +- backend/src/settings/generic/gpu.rs | 60 +- backend/src/settings/generic/mod.rs | 8 + backend/src/settings/generic/traits.rs | 3 +- backend/src/settings/generic_amd/cpu.rs | 14 +- backend/src/settings/generic_amd/gpu.rs | 13 +- backend/src/settings/generic_amd/mod.rs | 7 + backend/src/settings/mod.rs | 2 +- backend/src/settings/steam_deck/battery.rs | 141 +- backend/src/settings/steam_deck/cpu.rs | 180 +-- backend/src/settings/steam_deck/gpu.rs | 156 +- backend/src/settings/steam_deck/mod.rs | 9 +- backend/src/settings/steam_deck/oc_limits.rs | 190 --- backend/src/settings/steam_deck/util.rs | 8 + backend/src/settings/traits.rs | 6 + backend/src/settings/unknown/battery.rs | 21 +- backend/src/settings/unknown/cpu.rs | 44 +- backend/src/settings/unknown/gpu.rs | 19 +- backend/src/settings/unknown/mod.rs | 8 + 47 files changed, 2727 insertions(+), 934 deletions(-) create mode 100644 backend/limits_core/src/json_v2/base.rs create mode 100644 backend/limits_core/src/json_v2/battery_limit.rs create mode 100644 backend/limits_core/src/json_v2/conditions.rs create mode 100644 backend/limits_core/src/json_v2/config.rs create mode 100644 backend/limits_core/src/json_v2/cpu_limit.rs create mode 100644 backend/limits_core/src/json_v2/devel_message.rs create mode 100644 backend/limits_core/src/json_v2/gpu_limit.rs create mode 100644 backend/limits_core/src/json_v2/limits.rs create mode 100644 backend/limits_core/src/json_v2/mod.rs create mode 100644 backend/limits_core/src/json_v2/range.rs create mode 100644 backend/limits_core/src/json_v2/target.rs create mode 100644 backend/limits_srv/pt_limits_v2.json delete mode 100644 backend/src/settings/steam_deck/oc_limits.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 4da9134..04fb53e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -808,7 +808,7 @@ dependencies = [ [[package]] name = "limits_core" -version = "2.0.1" +version = "3.0.0" dependencies = [ "serde", "serde_json", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 2f48805..382f3de 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -26,7 +26,7 @@ log = "0.4" simplelog = "0.12" # limits & driver functionality -limits_core = { version = "2", path = "./limits_core" } +limits_core = { version = "3", path = "./limits_core" } regex = "1" libryzenadj = { version = "0.12" } # ureq's tls feature does not like musl targets diff --git a/backend/limits_core/Cargo.lock b/backend/limits_core/Cargo.lock index a3d5955..1fa968e 100644 --- a/backend/limits_core/Cargo.lock +++ b/backend/limits_core/Cargo.lock @@ -10,7 +10,7 @@ checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" [[package]] name = "limits_core" -version = "2.0.1" +version = "3.0.0" dependencies = [ "serde", "serde_json", diff --git a/backend/limits_core/Cargo.toml b/backend/limits_core/Cargo.toml index 7d2f721..8f6db2c 100644 --- a/backend/limits_core/Cargo.toml +++ b/backend/limits_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "limits_core" -version = "2.0.1" +version = "3.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/backend/limits_core/src/json_v2/base.rs b/backend/limits_core/src/json_v2/base.rs new file mode 100644 index 0000000..81643d9 --- /dev/null +++ b/backend/limits_core/src/json_v2/base.rs @@ -0,0 +1,267 @@ +use std::default::Default; +use serde::{Deserialize, Serialize}; + +/// Base JSON limits information +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Base { + /// System-specific configurations + pub configs: Vec, + /// Server messages + pub messages: Vec, + /// URL from which to grab the next update + pub refresh: Option, +} + +impl Default for Base { + fn default() -> Self { + Base { + configs: vec![ + super::Config { + name: "Steam Deck Custom".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: Some("model name\t: AMD Custom APU 0405\n".to_owned()), + os: None, + command: None, + file_exists: Some("./limits_override.json".into()), + }, + limits: super::Limits { + cpu: super::Limit { + provider: super::CpuLimitType::SteamDeckAdvance, + limits: super::GenericCpusLimit::default_for(super::CpuLimitType::SteamDeckAdvance), + }, + gpu: super::Limit { + provider: super::GpuLimitType::SteamDeckAdvance, + limits: super::GenericGpuLimit::default_for(super::GpuLimitType::SteamDeckAdvance), + }, + battery: super::Limit { + provider: super::BatteryLimitType::SteamDeckAdvance, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::SteamDeckAdvance), + }, + } + }, + super::Config { + name: "Steam Deck".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: Some("model name\t: AMD Custom APU 0405\n".to_owned()), + os: None, + command: None, + file_exists: None, + }, + limits: super::Limits { + cpu: super::Limit { + provider: super::CpuLimitType::SteamDeck, + limits: super::GenericCpusLimit::default_for(super::CpuLimitType::SteamDeck), + }, + gpu: super::Limit { + provider: super::GpuLimitType::SteamDeck, + limits: super::GenericGpuLimit::default_for(super::GpuLimitType::SteamDeck), + }, + battery: super::Limit { + provider: super::BatteryLimitType::SteamDeck, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::SteamDeck), + }, + } + }, + super::Config { + name: "AMD R3 2300U".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: Some("model name\t+: AMD Ryzen 3 2300U\n".to_owned()), + os: None, + command: None, + file_exists: None, + }, + limits: super::Limits { + cpu: super::CpuLimit { + provider: super::CpuLimitType::GenericAMD, + limits: super::GenericCpusLimit { + cpus: vec![ + super::GenericCpuLimit { + clock_min: Some(super::RangeLimit { min: Some(1000), max: Some(3700) }), + clock_max: Some(super::RangeLimit { min: Some(1000), max: Some(3700) }), + clock_step: Some(100), + skip_resume_reclock: false, + }; 4], + global_governors: true, + } + }, + gpu: super::GpuLimit { + provider: super::GpuLimitType::GenericAMD, + limits: super::GenericGpuLimit { + fast_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(25_000_000) }), + slow_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(25_000_000) }), + ppt_step: Some(1_000_000), + clock_min: Some(super::RangeLimit { min: Some(400), max: Some(1100) }), + clock_max: Some(super::RangeLimit { min: Some(400), max: Some(1100) }), + clock_step: Some(100), + ..Default::default() + } + }, + battery: super::Limit { + provider: super::BatteryLimitType::Generic, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::Generic), + } + }, + }, + super::Config { + name: "AMD R5 5560U".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: Some("model name\t+: AMD Ryzen 5 5560U\n".to_owned()), + os: None, + command: None, + file_exists: None, + }, + limits: super::Limits { + cpu: super::CpuLimit { + provider: super::CpuLimitType::GenericAMD, + limits: super::GenericCpusLimit { + cpus: vec![ + super::GenericCpuLimit { + clock_min: Some(super::RangeLimit { min: Some(1000), max: Some(4000) }), + clock_max: Some(super::RangeLimit { min: Some(1000), max: Some(4000) }), + clock_step: Some(100), + skip_resume_reclock: false, + }; 12], // 6 cores with SMTx2 + global_governors: true, + } + }, + gpu: super::GpuLimit { + provider: super::GpuLimitType::GenericAMD, + limits: super::GenericGpuLimit { + fast_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(25_000_000) }), + slow_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(25_000_000) }), + ppt_step: Some(1_000_000), + clock_min: Some(super::RangeLimit { min: Some(400), max: Some(1600) }), + clock_max: Some(super::RangeLimit { min: Some(400), max: Some(1600) }), + clock_step: Some(100), + ..Default::default() + } + }, + battery: super::Limit { + provider: super::BatteryLimitType::Generic, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::Generic), + } + } + }, + super::Config { + name: "AMD R7 5825U".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: Some("model name\t+: AMD Ryzen 7 5825U\n".to_owned()), + os: None, + command: None, + file_exists: None, + }, + limits: super::Limits { + cpu: super::CpuLimit { + provider: super::CpuLimitType::GenericAMD, + limits: super::GenericCpusLimit { + cpus: vec![ + super::GenericCpuLimit { + clock_min: Some(super::RangeLimit { min: Some(1000), max: Some(4500) }), + clock_max: Some(super::RangeLimit { min: Some(1000), max: Some(4500) }), + clock_step: Some(100), + skip_resume_reclock: false, + }; 16], // 8 cores with SMTx2 + global_governors: true, + } + }, + gpu: super::GpuLimit { + provider: super::GpuLimitType::GenericAMD, + limits: super::GenericGpuLimit { + fast_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(28_000_000) }), + slow_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(28_000_000) }), + ppt_step: Some(1_000_000), + clock_min: Some(super::RangeLimit { min: Some(400), max: Some(2200) }), + clock_max: Some(super::RangeLimit { min: Some(400), max: Some(2200) }), + clock_step: Some(100), + ..Default::default() + } + }, + battery: super::Limit { + provider: super::BatteryLimitType::Generic, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::Generic), + } + } + }, + super::Config { + name: "AMD R7 6800U".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: Some("model name\t+: AMD Ryzen 7 6800U\n".to_owned()), + os: None, + command: None, + file_exists: None, + }, + limits: super::Limits { + cpu: super::CpuLimit { + provider: super::CpuLimitType::GenericAMD, + limits: super::GenericCpusLimit { + cpus: vec![ + super::GenericCpuLimit { + clock_min: Some(super::RangeLimit { min: Some(1000), max: Some(4700) }), + clock_max: Some(super::RangeLimit { min: Some(1000), max: Some(4700) }), + clock_step: Some(100), + skip_resume_reclock: false, + }; 16], // 8 cores with SMTx2 + global_governors: true, + } + }, + gpu: super::GpuLimit { + provider: super::GpuLimitType::GenericAMD, + limits: super::GenericGpuLimit { + fast_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(28_000_000) }), + slow_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(28_000_000) }), + ppt_step: Some(1_000_000), + clock_min: Some(super::RangeLimit { min: Some(400), max: Some(2200) }), + clock_max: Some(super::RangeLimit { min: Some(400), max: Some(2200) }), + clock_step: Some(100), + ..Default::default() + } + }, + battery: super::Limit { + provider: super::BatteryLimitType::Generic, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::Generic), + } + } + }, + super::Config { + name: "Fallback".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: None, + os: None, + command: None, + file_exists: None, + }, + limits: super::Limits { + cpu: super::Limit { + provider: super::CpuLimitType::Unknown, + limits: super::GenericCpusLimit::default_for(super::CpuLimitType::Unknown), + }, + gpu: super::Limit { + provider: super::GpuLimitType::Unknown, + limits: super::GenericGpuLimit::default_for(super::GpuLimitType::Unknown), + }, + battery: super::Limit { + provider: super::BatteryLimitType::Unknown, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::Unknown), + } + } + } + ], + messages: vec![ + super::DeveloperMessage { + id: 1, + title: "Welcome".to_owned(), + body: "Thanks for installing PowerTools! For more information, please check the wiki. For bugs and requests, please create an issue.".to_owned(), + url: Some("https://git.ngni.us/NG-SD-Plugins/PowerTools/wiki".to_owned()), + } + ], + refresh: Some("http://limits.ngni.us:45000/powertools/v2".to_owned()) + } + } +} diff --git a/backend/limits_core/src/json_v2/battery_limit.rs b/backend/limits_core/src/json_v2/battery_limit.rs new file mode 100644 index 0000000..f064fd9 --- /dev/null +++ b/backend/limits_core/src/json_v2/battery_limit.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; +use super::RangeLimit; + +#[derive(Serialize, Deserialize, Debug, Clone)] +//#[serde(tag = "target")] +pub enum BatteryLimitType { + SteamDeck, + SteamDeckAdvance, + Generic, + Unknown, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct GenericBatteryLimit { + pub charge_rate: Option>, + pub charge_modes: Vec, + pub charge_limit: Option>, // battery charge % + pub extra_readouts: bool, +} + +impl GenericBatteryLimit { + pub fn default_for(t: BatteryLimitType) -> Self { + match t { + BatteryLimitType::SteamDeck | BatteryLimitType::SteamDeckAdvance => Self::default_steam_deck(), + _t => Self::default(), + } + } + + fn default_steam_deck() -> Self { + Self { + charge_rate: Some(RangeLimit { + min: Some(250), + max: Some(2500), + }), + charge_modes: vec![ + "normal".to_owned(), + "discharge".to_owned(), + "idle".to_owned(), + ], + charge_limit: Some(RangeLimit { + min: Some(10.0), + max: Some(90.0), + }), + extra_readouts: false, + } + } + + pub fn apply_override(&mut self, limit_override: Self) { + if let Some(range) = limit_override.charge_rate { + if range.min.is_none() && range.max.is_none() { + self.charge_rate = None; + } else { + self.charge_rate = Some(range); + } + } + if self.charge_modes.len() != limit_override.charge_modes.len() && !limit_override.charge_modes.is_empty() { + // assume limit_override.cpus wants to override even the cpu count + self.charge_modes = limit_override.charge_modes; + } + if let Some(range) = limit_override.charge_limit { + if range.min.is_none() && range.max.is_none() { + self.charge_limit = None; + } else { + self.charge_limit = Some(range); + } + } + self.extra_readouts = limit_override.extra_readouts; + } +} diff --git a/backend/limits_core/src/json_v2/conditions.rs b/backend/limits_core/src/json_v2/conditions.rs new file mode 100644 index 0000000..be08a1c --- /dev/null +++ b/backend/limits_core/src/json_v2/conditions.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +/// Conditions under which a config applies (ANDed together) +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Conditions { + /// Regex pattern for dmidecode output + pub dmi: Option, + /// Regex pattern for /proc/cpuinfo reading + pub cpuinfo: Option, + /// Regex pattern for /etc/os-release reading + pub os: Option, + /// Custom command to run, where an exit code of 0 means a successful match + pub command: Option, + /// Check if file exists + pub file_exists: Option, +} + +impl Conditions { + pub fn is_empty(&self) -> bool { + self.dmi.is_none() + && self.cpuinfo.is_none() + && self.os.is_none() + && self.command.is_none() + && self.file_exists.is_none() + } +} diff --git a/backend/limits_core/src/json_v2/config.rs b/backend/limits_core/src/json_v2/config.rs new file mode 100644 index 0000000..cf1e8e1 --- /dev/null +++ b/backend/limits_core/src/json_v2/config.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Config { + pub name: String, + pub conditions: super::Conditions, + pub limits: super::Limits, +} diff --git a/backend/limits_core/src/json_v2/cpu_limit.rs b/backend/limits_core/src/json_v2/cpu_limit.rs new file mode 100644 index 0000000..84f7ea0 --- /dev/null +++ b/backend/limits_core/src/json_v2/cpu_limit.rs @@ -0,0 +1,126 @@ +use serde::{Deserialize, Serialize}; + +use super::RangeLimit; + +#[derive(Serialize, Deserialize, Debug, Clone)] +//#[serde(tag = "target")] +pub enum CpuLimitType { + SteamDeck, + SteamDeckAdvance, + Generic, + GenericAMD, + Unknown, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct GenericCpusLimit { + pub cpus: Vec, + pub global_governors: bool, +} + +impl GenericCpusLimit { + pub fn default_for(t: CpuLimitType) -> Self { + match t { + CpuLimitType::SteamDeck | CpuLimitType::SteamDeckAdvance => { + Self { + cpus: [(); 8].iter().enumerate().map(|(i, _)| GenericCpuLimit::default_for(&t, i)).collect(), + global_governors: true, + } + }, + t => { + let cpu_count = Self::cpu_count().unwrap_or(8); + let mut cpus = Vec::with_capacity(cpu_count); + for i in 0..cpu_count { + cpus.push(GenericCpuLimit::default_for(&t, i)); + } + Self { + cpus, + global_governors: true, + } + } + } + } + + fn cpu_count() -> Option { + let mut data: String = std::fs::read_to_string("/sys/devices/system/cpu/present") + .unwrap_or_else(|_| "0-7".to_string() /* Steam Deck's default */); + if let Some(dash_index) = data.find('-') { + let data = data.split_off(dash_index + 1); + if let Ok(max_cpu) = data.parse::() { + return Some(max_cpu + 1); + } + } + None + } + + pub fn apply_override(&mut self, limit_override: Self) { + if self.cpus.len() != limit_override.cpus.len() && !limit_override.cpus.is_empty() { + // assume limit_override.cpus wants to override even the cpu count + self.cpus = limit_override.cpus; + } else { + self.cpus.iter_mut() + .zip(limit_override.cpus.into_iter()) + .for_each(|(cpu, limit_override)| cpu.apply_override(limit_override)); + } + self.global_governors = limit_override.global_governors; + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct GenericCpuLimit { + pub clock_min: Option>, + pub clock_max: Option>, + pub clock_step: Option, + pub skip_resume_reclock: bool, +} + +impl GenericCpuLimit { + pub fn default_for(t: &CpuLimitType, _index: usize) -> Self { + match t { + CpuLimitType::SteamDeck | CpuLimitType::SteamDeckAdvance => Self::default_steam_deck(), + _ => Self { + clock_min: None, + clock_max: None, + clock_step: Some(100), + skip_resume_reclock: false, + }, + } + } + + fn default_steam_deck() -> Self { + Self { + clock_min: Some(RangeLimit { + min: Some(1400), + max: Some(3500), + }), + clock_max: Some(RangeLimit { + min: Some(400), + max: Some(3500), + }), + clock_step: Some(100), + skip_resume_reclock: false, + } + } + + pub fn apply_override(&mut self, limit_override: Self) { + if let Some(range) = limit_override.clock_min { + if range.min.is_none() && range.max.is_none() { + self.clock_min = None; + } else { + self.clock_min = Some(range); + } + } + if let Some(range) = limit_override.clock_max { + if range.min.is_none() && range.max.is_none() { + self.clock_max = None; + } else { + self.clock_max = Some(range); + } + } + if let Some(val) = limit_override.clock_step { + self.clock_step = Some(val); + } + self.clock_step = limit_override.clock_step; + self.skip_resume_reclock = limit_override.skip_resume_reclock; + } +} diff --git a/backend/limits_core/src/json_v2/devel_message.rs b/backend/limits_core/src/json_v2/devel_message.rs new file mode 100644 index 0000000..904968d --- /dev/null +++ b/backend/limits_core/src/json_v2/devel_message.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +/// Message from the developers +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DeveloperMessage { + /// Message identifier + pub id: u64, + /// Message title + pub title: String, + /// Message content + pub body: String, + /// Link for further information + pub url: Option, +} diff --git a/backend/limits_core/src/json_v2/gpu_limit.rs b/backend/limits_core/src/json_v2/gpu_limit.rs new file mode 100644 index 0000000..39e09c1 --- /dev/null +++ b/backend/limits_core/src/json_v2/gpu_limit.rs @@ -0,0 +1,132 @@ +use serde::{Deserialize, Serialize}; +use super::RangeLimit; + +#[derive(Serialize, Deserialize, Debug, Clone)] +//#[serde(tag = "target")] +pub enum GpuLimitType { + SteamDeck, + SteamDeckAdvance, + Generic, + GenericAMD, + Unknown, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct GenericGpuLimit { + pub fast_ppt: Option>, + pub fast_ppt_default: Option, + pub slow_ppt: Option>, + pub slow_ppt_default: Option, + pub ppt_divisor: Option, + pub ppt_step: Option, + pub tdp: Option>, + pub tdp_boost: Option>, + pub tdp_step: Option, + pub clock_min: Option>, + pub clock_max: Option>, + pub clock_step: Option, + pub skip_resume_reclock: bool, +} + +impl GenericGpuLimit { + pub fn default_for(t: GpuLimitType) -> Self { + match t { + GpuLimitType::SteamDeck | GpuLimitType::SteamDeckAdvance => Self::default_steam_deck(), + _t => Self::default(), + } + } + + fn default_steam_deck() -> Self { + Self { + fast_ppt: Some(RangeLimit { + min: Some(1000000), + max: Some(30_000_000), + }), + fast_ppt_default: Some(15_000_000), + slow_ppt: Some(RangeLimit { + min: Some(1000000), + max: Some(29_000_000), + }), + slow_ppt_default: Some(15_000_000), + ppt_divisor: Some(1_000_000), + ppt_step: Some(1), + tdp: None, + tdp_boost: None, + tdp_step: None, + clock_min: Some(RangeLimit { + min: Some(400), + max: Some(1600), + }), + clock_max: Some(RangeLimit { + min: Some(400), + max: Some(1600), + }), + clock_step: Some(100), + skip_resume_reclock: false, + } + } + + pub fn apply_override(&mut self, limit_override: Self) { + if let Some(range) = limit_override.fast_ppt { + if range.min.is_none() && range.max.is_none() { + self.fast_ppt = None; + } else { + self.fast_ppt = Some(range); + } + } + if let Some(def) = limit_override.fast_ppt_default { + self.fast_ppt_default = Some(def); + } + if let Some(range) = limit_override.slow_ppt { + if range.min.is_none() && range.max.is_none() { + self.slow_ppt = None; + } else { + self.slow_ppt = Some(range); + } + } + if let Some(def) = limit_override.slow_ppt_default { + self.slow_ppt_default = Some(def); + } + if let Some(val) = limit_override.ppt_divisor { + self.ppt_divisor = Some(val); + } + if let Some(val) = limit_override.ppt_step { + self.ppt_step = Some(val); + } + if let Some(range) = limit_override.tdp { + if range.min.is_none() && range.max.is_none() { + self.tdp = None; + } else { + self.tdp = Some(range); + } + } + if let Some(range) = limit_override.tdp_boost { + if range.min.is_none() && range.max.is_none() { + self.tdp_boost = None; + } else { + self.tdp_boost = Some(range); + } + } + if let Some(val) = limit_override.tdp_step { + self.tdp_step = Some(val); + } + if let Some(range) = limit_override.clock_min { + if range.min.is_none() && range.max.is_none() { + self.clock_min = None; + } else { + self.clock_min = Some(range); + } + } + if let Some(range) = limit_override.clock_max { + if range.min.is_none() && range.max.is_none() { + self.clock_max = None; + } else { + self.clock_max = Some(range); + } + } + if let Some(val) = limit_override.clock_step { + self.clock_step = Some(val); + } + self.skip_resume_reclock = limit_override.skip_resume_reclock; + } +} diff --git a/backend/limits_core/src/json_v2/limits.rs b/backend/limits_core/src/json_v2/limits.rs new file mode 100644 index 0000000..b983603 --- /dev/null +++ b/backend/limits_core/src/json_v2/limits.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Limits { + pub cpu: CpuLimit, + pub gpu: GpuLimit, + pub battery: BatteryLimit, +} + +impl Limits { + pub fn apply_override(&mut self, limit_override: Option) { + if let Some(limit_override) = limit_override { + self.cpu.limits.apply_override(limit_override.cpu.limits); + self.gpu.limits.apply_override(limit_override.gpu.limits); + self.battery.limits.apply_override(limit_override.battery.limits); + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Limit { + pub provider: P, + pub limits: L, +} + +pub type CpuLimit = Limit; +pub type GpuLimit = Limit; +pub type BatteryLimit = Limit; diff --git a/backend/limits_core/src/json_v2/mod.rs b/backend/limits_core/src/json_v2/mod.rs new file mode 100644 index 0000000..c5bfeb6 --- /dev/null +++ b/backend/limits_core/src/json_v2/mod.rs @@ -0,0 +1,21 @@ +mod base; +mod battery_limit; +mod conditions; +mod config; +mod cpu_limit; +mod devel_message; +mod gpu_limit; +mod limits; +mod range; +mod target; + +pub use base::Base; +pub use battery_limit::{BatteryLimitType, GenericBatteryLimit}; +pub use conditions::Conditions; +pub use cpu_limit::{CpuLimitType, GenericCpusLimit, GenericCpuLimit}; +pub use devel_message::DeveloperMessage; +pub use gpu_limit::{GpuLimitType, GenericGpuLimit}; +pub use config::Config; +pub use limits::{Limits, Limit, CpuLimit, GpuLimit, BatteryLimit}; +pub use range::RangeLimit; +pub use target::Target; diff --git a/backend/limits_core/src/json_v2/range.rs b/backend/limits_core/src/json_v2/range.rs new file mode 100644 index 0000000..62de907 --- /dev/null +++ b/backend/limits_core/src/json_v2/range.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +/// Base JSON limits information +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub struct RangeLimit { + pub min: Option, + pub max: Option, +} diff --git a/backend/limits_core/src/json_v2/target.rs b/backend/limits_core/src/json_v2/target.rs new file mode 100644 index 0000000..768df6f --- /dev/null +++ b/backend/limits_core/src/json_v2/target.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum Target { + SteamDeck, + SteamDeckAdvance, + Generic, + Unknown, +} diff --git a/backend/limits_core/src/lib.rs b/backend/limits_core/src/lib.rs index 22fdbb3..11adaf7 100644 --- a/backend/limits_core/src/lib.rs +++ b/backend/limits_core/src/lib.rs @@ -1 +1,2 @@ pub mod json; +pub mod json_v2; diff --git a/backend/limits_srv/Cargo.lock b/backend/limits_srv/Cargo.lock index 1b8b3ee..9c3cf2d 100644 --- a/backend/limits_srv/Cargo.lock +++ b/backend/limits_srv/Cargo.lock @@ -433,7 +433,7 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "limits_core" -version = "2.0.1" +version = "3.0.0" dependencies = [ "serde", "serde_json", @@ -441,7 +441,7 @@ dependencies = [ [[package]] name = "limits_srv" -version = "2.0.1" +version = "3.0.0" dependencies = [ "chrono", "limits_core", diff --git a/backend/limits_srv/Cargo.toml b/backend/limits_srv/Cargo.toml index 2d529a8..18f52a6 100644 --- a/backend/limits_srv/Cargo.toml +++ b/backend/limits_srv/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "limits_srv" -version = "2.0.1" +version = "3.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -limits_core = { version = "2.0.1", path = "../limits_core" } +limits_core = { version = "3.0.0", path = "../limits_core" } chrono = { version = "0.4" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/backend/limits_srv/pt_limits.json b/backend/limits_srv/pt_limits.json index 2b34975..eea7711 100644 --- a/backend/limits_srv/pt_limits.json +++ b/backend/limits_srv/pt_limits.json @@ -293,8 +293,8 @@ { "id": 1, "title": "Welcome", - "body": "Thanks for installing PowerTools! For more information, please check the wiki. For bugs and requests, please create an issue on GitHub.", - "url": "https://github.com/NGnius/PowerTools/wiki" + "body": "Thanks for installing PowerTools! For more information, please check the wiki. For bugs and requests, please create an issue.", + "url": "https://git.ngni.us/NG-SD-Plugins/PowerTools/wiki" } ], "refresh": "http://limits.ngni.us:45000/powertools/v1" diff --git a/backend/limits_srv/pt_limits_v2.json b/backend/limits_srv/pt_limits_v2.json new file mode 100644 index 0000000..fc515c6 --- /dev/null +++ b/backend/limits_srv/pt_limits_v2.json @@ -0,0 +1,1250 @@ +{ + "configs": [ + { + "name": "Steam Deck Custom", + "conditions": { + "dmi": null, + "cpuinfo": "model name\t: AMD Custom APU 0405\n", + "os": null, + "command": null, + "file_exists": "./limits_override.json" + }, + "limits": { + "cpu": { + "provider": "SteamDeckAdvance", + "limits": { + "cpus": [ + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + ], + "global_governors": true + } + }, + "gpu": { + "provider": "SteamDeckAdvance", + "limits": { + "fast_ppt": { + "min": 1000000, + "max": 30000000 + }, + "fast_ppt_default": 15000000, + "slow_ppt": { + "min": 1000000, + "max": 29000000 + }, + "slow_ppt_default": 15000000, + "ppt_divisor": 1000000, + "ppt_step": 1, + "tdp": null, + "tdp_boost": null, + "tdp_step": null, + "clock_min": { + "min": 400, + "max": 1600 + }, + "clock_max": { + "min": 400, + "max": 1600 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + }, + "battery": { + "provider": "SteamDeckAdvance", + "limits": { + "charge_rate": { + "min": 250, + "max": 2500 + }, + "charge_modes": [ + "normal", + "discharge", + "idle" + ], + "charge_limit": { + "min": 10.0, + "max": 90.0 + }, + "extra_readouts": false + } + } + } + }, + { + "name": "Steam Deck", + "conditions": { + "dmi": null, + "cpuinfo": "model name\t: AMD Custom APU 0405\n", + "os": null, + "command": null, + "file_exists": null + }, + "limits": { + "cpu": { + "provider": "SteamDeck", + "limits": { + "cpus": [ + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1400, + "max": 3500 + }, + "clock_max": { + "min": 400, + "max": 3500 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + ], + "global_governors": true + } + }, + "gpu": { + "provider": "SteamDeck", + "limits": { + "fast_ppt": { + "min": 1000000, + "max": 30000000 + }, + "fast_ppt_default": 15000000, + "slow_ppt": { + "min": 1000000, + "max": 29000000 + }, + "slow_ppt_default": 15000000, + "ppt_divisor": 1000000, + "ppt_step": 1, + "tdp": null, + "tdp_boost": null, + "tdp_step": null, + "clock_min": { + "min": 400, + "max": 1600 + }, + "clock_max": { + "min": 400, + "max": 1600 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + }, + "battery": { + "provider": "SteamDeck", + "limits": { + "charge_rate": { + "min": 250, + "max": 2500 + }, + "charge_modes": [ + "normal", + "discharge", + "idle" + ], + "charge_limit": { + "min": 10.0, + "max": 90.0 + }, + "extra_readouts": false + } + } + } + }, + { + "name": "AMD R3 2300U", + "conditions": { + "dmi": null, + "cpuinfo": "model name\t+: AMD Ryzen 3 2300U\n", + "os": null, + "command": null, + "file_exists": null + }, + "limits": { + "cpu": { + "provider": "GenericAMD", + "limits": { + "cpus": [ + { + "clock_min": { + "min": 1000, + "max": 3700 + }, + "clock_max": { + "min": 1000, + "max": 3700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 3700 + }, + "clock_max": { + "min": 1000, + "max": 3700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 3700 + }, + "clock_max": { + "min": 1000, + "max": 3700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 3700 + }, + "clock_max": { + "min": 1000, + "max": 3700 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + ], + "global_governors": true + } + }, + "gpu": { + "provider": "GenericAMD", + "limits": { + "fast_ppt": { + "min": 1000000, + "max": 25000000 + }, + "fast_ppt_default": null, + "slow_ppt": { + "min": 1000000, + "max": 25000000 + }, + "slow_ppt_default": null, + "ppt_divisor": null, + "ppt_step": 1000000, + "tdp": null, + "tdp_boost": null, + "tdp_step": null, + "clock_min": { + "min": 400, + "max": 1100 + }, + "clock_max": { + "min": 400, + "max": 1100 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + }, + "battery": { + "provider": "Generic", + "limits": { + "charge_rate": null, + "charge_modes": [], + "charge_limit": null, + "extra_readouts": false + } + } + } + }, + { + "name": "AMD R5 5560U", + "conditions": { + "dmi": null, + "cpuinfo": "model name\t+: AMD Ryzen 5 5560U\n", + "os": null, + "command": null, + "file_exists": null + }, + "limits": { + "cpu": { + "provider": "GenericAMD", + "limits": { + "cpus": [ + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4000 + }, + "clock_max": { + "min": 1000, + "max": 4000 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + ], + "global_governors": true + } + }, + "gpu": { + "provider": "GenericAMD", + "limits": { + "fast_ppt": { + "min": 1000000, + "max": 25000000 + }, + "fast_ppt_default": null, + "slow_ppt": { + "min": 1000000, + "max": 25000000 + }, + "slow_ppt_default": null, + "ppt_divisor": null, + "ppt_step": 1000000, + "tdp": null, + "tdp_boost": null, + "tdp_step": null, + "clock_min": { + "min": 400, + "max": 1600 + }, + "clock_max": { + "min": 400, + "max": 1600 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + }, + "battery": { + "provider": "Generic", + "limits": { + "charge_rate": null, + "charge_modes": [], + "charge_limit": null, + "extra_readouts": false + } + } + } + }, + { + "name": "AMD R7 5825U", + "conditions": { + "dmi": null, + "cpuinfo": "model name\t+: AMD Ryzen 7 5825U\n", + "os": null, + "command": null, + "file_exists": null + }, + "limits": { + "cpu": { + "provider": "GenericAMD", + "limits": { + "cpus": [ + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4500 + }, + "clock_max": { + "min": 1000, + "max": 4500 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + ], + "global_governors": true + } + }, + "gpu": { + "provider": "GenericAMD", + "limits": { + "fast_ppt": { + "min": 1000000, + "max": 28000000 + }, + "fast_ppt_default": null, + "slow_ppt": { + "min": 1000000, + "max": 28000000 + }, + "slow_ppt_default": null, + "ppt_divisor": null, + "ppt_step": 1000000, + "tdp": null, + "tdp_boost": null, + "tdp_step": null, + "clock_min": { + "min": 400, + "max": 2200 + }, + "clock_max": { + "min": 400, + "max": 2200 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + }, + "battery": { + "provider": "Generic", + "limits": { + "charge_rate": null, + "charge_modes": [], + "charge_limit": null, + "extra_readouts": false + } + } + } + }, + { + "name": "AMD R7 6800U", + "conditions": { + "dmi": null, + "cpuinfo": "model name\t+: AMD Ryzen 7 6800U\n", + "os": null, + "command": null, + "file_exists": null + }, + "limits": { + "cpu": { + "provider": "GenericAMD", + "limits": { + "cpus": [ + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 1000, + "max": 4700 + }, + "clock_max": { + "min": 1000, + "max": 4700 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + ], + "global_governors": true + } + }, + "gpu": { + "provider": "GenericAMD", + "limits": { + "fast_ppt": { + "min": 1000000, + "max": 28000000 + }, + "fast_ppt_default": null, + "slow_ppt": { + "min": 1000000, + "max": 28000000 + }, + "slow_ppt_default": null, + "ppt_divisor": null, + "ppt_step": 1000000, + "tdp": null, + "tdp_boost": null, + "tdp_step": null, + "clock_min": { + "min": 400, + "max": 2200 + }, + "clock_max": { + "min": 400, + "max": 2200 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + }, + "battery": { + "provider": "Generic", + "limits": { + "charge_rate": null, + "charge_modes": [], + "charge_limit": null, + "extra_readouts": false + } + } + } + }, + { + "name": "Fallback", + "conditions": { + "dmi": null, + "cpuinfo": null, + "os": null, + "command": null, + "file_exists": null + }, + "limits": { + "cpu": { + "provider": "Unknown", + "limits": { + "cpus": [ + { + "clock_min": null, + "clock_max": null, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": null, + "clock_max": null, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": null, + "clock_max": null, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": null, + "clock_max": null, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": null, + "clock_max": null, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": null, + "clock_max": null, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": null, + "clock_max": null, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": null, + "clock_max": null, + "clock_step": 100, + "skip_resume_reclock": false + } + ], + "global_governors": true + } + }, + "gpu": { + "provider": "Unknown", + "limits": { + "fast_ppt": null, + "fast_ppt_default": null, + "slow_ppt": null, + "slow_ppt_default": null, + "ppt_divisor": null, + "ppt_step": null, + "tdp": null, + "tdp_boost": null, + "tdp_step": null, + "clock_min": null, + "clock_max": null, + "clock_step": null, + "skip_resume_reclock": false + } + }, + "battery": { + "provider": "Unknown", + "limits": { + "charge_rate": null, + "charge_modes": [], + "charge_limit": null, + "extra_readouts": false + } + } + } + } + ], + "messages": [ + { + "id": 1, + "title": "Welcome", + "body": "Thanks for installing PowerTools! For more information, please check the wiki. For bugs and requests, please create an issue.", + "url": "https://git.ngni.us/NG-SD-Plugins/PowerTools/wiki" + } + ], + "refresh": "http://limits.ngni.us:45000/powertools/v2" +} \ No newline at end of file diff --git a/backend/limits_srv/src/main.rs b/backend/limits_srv/src/main.rs index 10a03af..6e33b45 100644 --- a/backend/limits_srv/src/main.rs +++ b/backend/limits_srv/src/main.rs @@ -4,46 +4,61 @@ use std::sync::{RwLock, Arc}; use serde::Serialize; use warp::Filter; -use limits_core::json::Base; - -static VISIT_COUNT: AtomicU64 = AtomicU64::new(0); +static VISIT_V1_COUNT: AtomicU64 = AtomicU64::new(0); +static VISIT_V2_COUNT: AtomicU64 = AtomicU64::new(0); static START_TIME: AtomicI64 = AtomicI64::new(0); -fn get_limits(base: Base) -> impl warp::Reply { - VISIT_COUNT.fetch_add(1, Ordering::AcqRel); +fn get_limits_v1(base: &limits_core::json::Base) -> impl warp::Reply { + VISIT_V1_COUNT.fetch_add(1, Ordering::AcqRel); //println!("Limits got"); - warp::reply::json(&base) + warp::reply::json(base) +} + +fn get_limits_v2(base: &limits_core::json_v2::Base) -> impl warp::Reply { + VISIT_V2_COUNT.fetch_add(1, Ordering::AcqRel); + //println!("Limits got"); + warp::reply::json(base) } #[derive(Serialize)] struct Visits { - visits: u64, + visits_v1: u64, + visits_v2: u64, since: i64, // Unix time (since epoch) } fn get_visits() -> impl warp::Reply { - let count = VISIT_COUNT.load(Ordering::Relaxed); + let count_v1 = VISIT_V1_COUNT.load(Ordering::Relaxed); + let count_v2 = VISIT_V2_COUNT.load(Ordering::Relaxed); let start = START_TIME.load(Ordering::Relaxed); //println!("Count got"); warp::reply::json(&Visits { - visits: count, + visits_v1: count_v1, + visits_v2: count_v2, since: start, }) } #[allow(opaque_hidden_inferred_bound)] -fn routes(base: Arc>) -> impl Filter + Clone { +fn routes(base: Arc>, base2: Arc>) -> impl Filter + Clone { warp::get().and( warp::path!("powertools" / "v1") .map(move || { - let base = base.read().expect("Failed to acquire base limits read lock").clone(); - get_limits(base) + let base = base.read().expect("Failed to acquire base limits read lock"); + get_limits_v1(&base) }) .or( warp::path!("powertools" / "count") .map(get_visits) ) + .or( + warp::path!("powertools" / "v2") + .map(move || { + let base2 = base2.read().expect("Failed to acquire base limits read lock"); + get_limits_v2(&base2) + }) + ) ).recover(recovery) } @@ -59,10 +74,14 @@ pub async fn recovery(reject: warp::Rejection) -> Result limits_core::json::Base { +fn get_limits() -> limits_core::json_v2::Base { let limits_path = super::utility::limits_path(); match File::open(&limits_path) { Ok(f) => match serde_json::from_reader(f) { @@ -18,7 +18,7 @@ fn get_limits() -> limits_core::json::Base { limits_path.display(), e ); - limits_core::json::Base::default() + limits_core::json_v2::Base::default() } }, Err(e) => { @@ -32,6 +32,31 @@ fn get_limits() -> limits_core::json::Base { } } +fn get_limits_overrides() -> Option { + let limits_override_path = super::utility::limits_override_path(); + match File::open(&limits_override_path) { + Ok(f) => match serde_json::from_reader(f) { + Ok(lim) => Some(lim), + Err(e) => { + log::warn!( + "Failed to parse limits override file `{}`, cannot use for auto_detect: {}", + limits_override_path.display(), + e + ); + None + } + }, + Err(e) => { + log::info!( + "Failed to open limits override file `{}`: {}", + limits_override_path.display(), + e + ); + None + } + } +} + #[inline] pub fn auto_detect_provider() -> DriverJson { let provider = auto_detect0( @@ -51,7 +76,13 @@ pub fn auto_detect0( json_path: std::path::PathBuf, name: String, ) -> Driver { - let mut builder = DriverBuilder::new(json_path, name); + let mut general_driver = Box::new(General { + persistent: false, + path: json_path, + name, + driver: DriverJson::AutoDetect, + events: Default::default(), + }); let cpu_info: String = usdpl_back::api::files::read_single("/proc/cpuinfo").unwrap_or_default(); log::debug!("Read from /proc/cpuinfo:\n{}", cpu_info); @@ -65,268 +96,229 @@ pub fn auto_detect0( log::debug!("Read dmidecode:\n{}", dmi_info); let limits = get_limits(); + let limits_override = get_limits_overrides(); // build driver based on limits conditions for conf in limits.configs { let conditions = conf.conditions; let mut matches = true; - if conditions.is_empty() { - matches = !builder.is_complete(); - } else { - if let Some(dmi) = &conditions.dmi { - let pattern = RegexBuilder::new(dmi) - .multi_line(true) - .build() - .expect("Invalid DMI regex"); - matches &= pattern.is_match(&dmi_info); - } - if let Some(cpuinfo) = &conditions.cpuinfo { - let pattern = RegexBuilder::new(cpuinfo) - .multi_line(true) - .build() - .expect("Invalid CPU regex"); - matches &= pattern.is_match(&cpu_info); - } - if let Some(os) = &conditions.os { - let pattern = RegexBuilder::new(os) - .multi_line(true) - .build() - .expect("Invalid OS regex"); - matches &= pattern.is_match(&os_info); - } - if let Some(cmd) = &conditions.command { - match std::process::Command::new("bash") - .args(["-c", cmd]) - .status() - { - Ok(status) => matches &= status.code().map(|c| c == 0).unwrap_or(false), - Err(e) => log::warn!("Ignoring bash limits error: {}", e), - } - } - if let Some(file_exists) = &conditions.file_exists { - let exists = std::path::Path::new(file_exists).exists(); - matches &= exists; + if let Some(dmi) = &conditions.dmi { + let pattern = RegexBuilder::new(dmi) + .multi_line(true) + .build() + .expect("Invalid DMI regex"); + matches &= pattern.is_match(&dmi_info); + } + if let Some(cpuinfo) = &conditions.cpuinfo { + let pattern = RegexBuilder::new(cpuinfo) + .multi_line(true) + .build() + .expect("Invalid CPU regex"); + matches &= pattern.is_match(&cpu_info); + } + if let Some(os) = &conditions.os { + let pattern = RegexBuilder::new(os) + .multi_line(true) + .build() + .expect("Invalid OS regex"); + matches &= pattern.is_match(&os_info); + } + if let Some(cmd) = &conditions.command { + match std::process::Command::new("bash") + .args(["-c", cmd]) + .status() + { + Ok(status) => matches &= status.code().map(|c| c == 0).unwrap_or(false), + Err(e) => log::warn!("Ignoring bash limits error: {}", e), } } + if let Some(file_exists) = &conditions.file_exists { + let exists = std::path::Path::new(file_exists).exists(); + matches &= exists; + } + if matches { + let mut relevant_limits = conf.limits.clone(); + relevant_limits.apply_override(limits_override); if let Some(settings) = &settings_opt { - *builder.general.persistent() = true; - builder.general.name(settings.name.clone()); - for limit in conf.limits { - match limit { - Limits::Cpu(cpus) => { - let cpu_driver: Box = match cpus { - CpuLimit::SteamDeck => { - Box::new(crate::settings::steam_deck::Cpus::from_json( - settings.cpus.clone(), - settings.version, - )) - } - CpuLimit::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Cpus::from_json( - settings.cpus.clone(), - settings.version, - )) - } - CpuLimit::Generic(x) => Box::new(crate::settings::generic::Cpus::< - crate::settings::generic::Cpu, - >::from_json_and_limits( - settings.cpus.clone(), - settings.version, - x, - )), - CpuLimit::GenericAMD(x) => Box::new( - crate::settings::generic_amd::Cpus::from_json_and_limits( - settings.cpus.clone(), - settings.version, - x, - ), - ), - CpuLimit::Unknown => { - Box::new(crate::settings::unknown::Cpus::from_json( - settings.cpus.clone(), - settings.version, - )) - } - }; - builder.cpus = Some(cpu_driver); - } - Limits::Gpu(gpu) => { - let driver: Box = match gpu { - GpuLimit::SteamDeck => { - Box::new(crate::settings::steam_deck::Gpu::from_json( - settings.gpu.clone(), - settings.version, - )) - } - GpuLimit::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Gpu::from_json( - settings.gpu.clone(), - settings.version, - )) - } - GpuLimit::Generic(x) => { - Box::new(crate::settings::generic::Gpu::from_json_and_limits( - settings.gpu.clone(), - settings.version, - x, - )) - } - GpuLimit::GenericAMD(x) => Box::new( - crate::settings::generic_amd::Gpu::from_json_and_limits( - settings.gpu.clone(), - settings.version, - x, - ), - ), - GpuLimit::Unknown => { - Box::new(crate::settings::unknown::Gpu::from_json( - settings.gpu.clone(), - settings.version, - )) - } - }; - builder.gpu = Some(driver); - } - Limits::Battery(batt) => { - let driver: Box = match batt { - BatteryLimit::SteamDeck => { - Box::new(crate::settings::steam_deck::Battery::from_json( - settings.battery.clone(), - settings.version, - )) - } - BatteryLimit::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Battery::from_json( - settings.battery.clone(), - settings.version, - )) - } - BatteryLimit::Generic(x) => Box::new( - crate::settings::generic::Battery::from_json_and_limits( - settings.battery.clone(), - settings.version, - x, - ), - ), - BatteryLimit::Unknown => { - Box::new(crate::settings::unknown::Battery) - } - }; - builder.battery = Some(driver); - } + *general_driver.persistent() = true; + let cpu_driver: Box = match relevant_limits.cpu.provider { + CpuLimitType::SteamDeck => { + Box::new(crate::settings::steam_deck::Cpus::from_json_and_limits( + settings.cpus.clone(), + settings.version, + relevant_limits.cpu.limits, + )) } - } + CpuLimitType::SteamDeckAdvance => { + Box::new(crate::settings::steam_deck::Cpus::from_json_and_limits( + settings.cpus.clone(), + settings.version, + relevant_limits.cpu.limits, + )) + } + CpuLimitType::Generic => Box::new(crate::settings::generic::Cpus::< + crate::settings::generic::Cpu, + >::from_json_and_limits( + settings.cpus.clone(), + settings.version, + relevant_limits.cpu.limits, + )), + CpuLimitType::GenericAMD => Box::new( + crate::settings::generic_amd::Cpus::from_json_and_limits( + settings.cpus.clone(), + settings.version, + relevant_limits.cpu.limits, + ), + ), + CpuLimitType::Unknown => { + Box::new(crate::settings::unknown::Cpus::from_json_and_limits( + settings.cpus.clone(), + settings.version, + relevant_limits.cpu.limits, + )) + } + }; + + let gpu_driver: Box = match relevant_limits.gpu.provider { + GpuLimitType::SteamDeck => { + Box::new(crate::settings::steam_deck::Gpu::from_json_and_limits( + settings.gpu.clone(), + settings.version, + relevant_limits.gpu.limits, + )) + } + GpuLimitType::SteamDeckAdvance => { + Box::new(crate::settings::steam_deck::Gpu::from_json_and_limits( + settings.gpu.clone(), + settings.version, + relevant_limits.gpu.limits, + )) + } + GpuLimitType::Generic => { + Box::new(crate::settings::generic::Gpu::from_json_and_limits( + settings.gpu.clone(), + settings.version, + relevant_limits.gpu.limits, + )) + } + GpuLimitType::GenericAMD => Box::new( + crate::settings::generic_amd::Gpu::from_json_and_limits( + settings.gpu.clone(), + settings.version, + relevant_limits.gpu.limits, + ), + ), + GpuLimitType::Unknown => { + Box::new(crate::settings::unknown::Gpu::from_json_and_limits( + settings.gpu.clone(), + settings.version, + relevant_limits.gpu.limits, + )) + } + }; + let battery_driver: Box = match relevant_limits.battery.provider { + BatteryLimitType::SteamDeck => { + Box::new(crate::settings::steam_deck::Battery::from_json_and_limits( + settings.battery.clone(), + settings.version, + relevant_limits.battery.limits, + )) + } + BatteryLimitType::SteamDeckAdvance => { + Box::new(crate::settings::steam_deck::Battery::from_json_and_limits( + settings.battery.clone(), + settings.version, + relevant_limits.battery.limits, + )) + } + BatteryLimitType::Generic => Box::new( + crate::settings::generic::Battery::from_json_and_limits( + settings.battery.clone(), + settings.version, + relevant_limits.battery.limits, + ), + ), + BatteryLimitType::Unknown => { + Box::new(crate::settings::unknown::Battery::from_json_and_limits( + settings.battery.clone(), + settings.version, + relevant_limits.battery.limits, + )) + } + }; + + return Driver { + general: general_driver, + cpus: cpu_driver, + gpu: gpu_driver, + battery: battery_driver, + }; } else { - for limit in conf.limits { - match limit { - Limits::Cpu(cpus) => { - let cpu_driver: Box = match cpus { - CpuLimit::SteamDeck => { - Box::new(crate::settings::steam_deck::Cpus::system_default()) - } - CpuLimit::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Cpus::system_default()) - } - CpuLimit::Generic(x) => { - Box::new(crate::settings::generic::Cpus::< - crate::settings::generic::Cpu, - >::from_limits(x)) - } - CpuLimit::GenericAMD(x) => { - Box::new(crate::settings::generic_amd::Cpus::from_limits(x)) - } - CpuLimit::Unknown => { - Box::new(crate::settings::unknown::Cpus::system_default()) - } - }; - builder.cpus = Some(cpu_driver); - } - Limits::Gpu(gpu) => { - let driver: Box = match gpu { - GpuLimit::SteamDeck => { - Box::new(crate::settings::steam_deck::Gpu::system_default()) - } - GpuLimit::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Gpu::system_default()) - } - GpuLimit::Generic(x) => { - Box::new(crate::settings::generic::Gpu::from_limits(x)) - } - GpuLimit::GenericAMD(x) => { - Box::new(crate::settings::generic_amd::Gpu::from_limits(x)) - } - GpuLimit::Unknown => { - Box::new(crate::settings::unknown::Gpu::system_default()) - } - }; - builder.gpu = Some(driver); - } - Limits::Battery(batt) => { - let driver: Box = match batt { - BatteryLimit::SteamDeck => { - Box::new(crate::settings::steam_deck::Battery::system_default()) - } - BatteryLimit::SteamDeckAdvance => { - Box::new(crate::settings::steam_deck::Battery::system_default()) - } - BatteryLimit::Generic(x) => { - Box::new(crate::settings::generic::Battery::from_limits(x)) - } - BatteryLimit::Unknown => { - Box::new(crate::settings::unknown::Battery) - } - }; - builder.battery = Some(driver); - } + let cpu_driver: Box = match relevant_limits.cpu.provider { + CpuLimitType::SteamDeck => { + Box::new(crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits)) } - } + CpuLimitType::SteamDeckAdvance => { + Box::new(crate::settings::steam_deck::Cpus::from_limits(relevant_limits.cpu.limits)) + } + CpuLimitType::Generic => { + Box::new(crate::settings::generic::Cpus::< + crate::settings::generic::Cpu, + >::from_limits(relevant_limits.cpu.limits)) + } + CpuLimitType::GenericAMD => { + Box::new(crate::settings::generic_amd::Cpus::from_limits(relevant_limits.cpu.limits)) + } + CpuLimitType::Unknown => { + Box::new(crate::settings::unknown::Cpus::from_limits(relevant_limits.cpu.limits)) + } + }; + let gpu_driver: Box = match relevant_limits.gpu.provider { + GpuLimitType::SteamDeck => { + Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits)) + } + GpuLimitType::SteamDeckAdvance => { + Box::new(crate::settings::steam_deck::Gpu::from_limits(relevant_limits.gpu.limits)) + } + GpuLimitType::Generic => { + Box::new(crate::settings::generic::Gpu::from_limits(relevant_limits.gpu.limits)) + } + GpuLimitType::GenericAMD => { + Box::new(crate::settings::generic_amd::Gpu::from_limits(relevant_limits.gpu.limits)) + } + GpuLimitType::Unknown => { + Box::new(crate::settings::unknown::Gpu::from_limits(relevant_limits.gpu.limits)) + } + }; + let battery_driver: Box = match relevant_limits.battery.provider { + BatteryLimitType::SteamDeck => { + Box::new(crate::settings::steam_deck::Battery::from_limits(relevant_limits.battery.limits)) + } + BatteryLimitType::SteamDeckAdvance => { + Box::new(crate::settings::steam_deck::Battery::from_limits(relevant_limits.battery.limits)) + } + BatteryLimitType::Generic => { + Box::new(crate::settings::generic::Battery::from_limits(relevant_limits.battery.limits)) + } + BatteryLimitType::Unknown => { + Box::new(crate::settings::unknown::Battery::from_limits(relevant_limits.battery.limits)) + } + }; + return Driver { + general: general_driver, + cpus: cpu_driver, + gpu: gpu_driver, + battery: battery_driver, + }; } } } - builder.build() -} - -struct DriverBuilder { - general: Box, - cpus: Option>, - gpu: Option>, - battery: Option>, -} - -impl DriverBuilder { - fn new(json_path: std::path::PathBuf, profile_name: String) -> Self { - Self { - general: Box::new(General { - persistent: false, - path: json_path, - name: profile_name, - driver: DriverJson::AutoDetect, - events: Default::default(), - }), - cpus: None, - gpu: None, - battery: None, - } - } - - fn is_complete(&self) -> bool { - self.cpus.is_some() && self.gpu.is_some() && self.battery.is_some() - } - - fn build(self) -> Driver { - Driver { - general: self.general, - cpus: self - .cpus - .unwrap_or_else(|| Box::new(crate::settings::unknown::Cpus::system_default())), - gpu: self - .gpu - .unwrap_or_else(|| Box::new(crate::settings::unknown::Gpu::system_default())), - battery: self - .battery - .unwrap_or_else(|| Box::new(crate::settings::unknown::Battery)), - } + Driver { + general: general_driver, + cpus: Box::new(crate::settings::unknown::Cpus::system_default()), + gpu: Box::new(crate::settings::unknown::Gpu::system_default()), + battery: Box::new(crate::settings::unknown::Battery), } } diff --git a/backend/src/settings/detect/limits_worker.rs b/backend/src/settings/detect/limits_worker.rs index 8a52b96..c3dbd0e 100644 --- a/backend/src/settings/detect/limits_worker.rs +++ b/backend/src/settings/detect/limits_worker.rs @@ -2,7 +2,7 @@ use std::thread::{self, JoinHandle}; #[cfg(feature = "online")] use std::time::Duration; -use limits_core::json::Base; +use limits_core::json_v2::Base; #[cfg(feature = "online")] pub fn spawn() -> JoinHandle<()> { diff --git a/backend/src/settings/detect/utility.rs b/backend/src/settings/detect/utility.rs index c278e66..7d42be0 100644 --- a/backend/src/settings/detect/utility.rs +++ b/backend/src/settings/detect/utility.rs @@ -4,6 +4,10 @@ pub fn limits_path() -> std::path::PathBuf { crate::utility::settings_dir().join(crate::consts::LIMITS_FILE) } +pub fn limits_override_path() -> std::path::PathBuf { + crate::utility::settings_dir().join(crate::consts::LIMITS_OVERRIDE_FILE) +} + // NOTE: eats errors pub fn get_dev_messages() -> Vec { let limits_path = limits_path(); diff --git a/backend/src/settings/driver.rs b/backend/src/settings/driver.rs index 4192f6f..90a3b7f 100644 --- a/backend/src/settings/driver.rs +++ b/backend/src/settings/driver.rs @@ -1,4 +1,4 @@ -use super::{auto_detect0, General, SettingError, TBattery, TCpus, TGeneral, TGpu}; +use super::{auto_detect0, TBattery, TCpus, TGeneral, TGpu}; use crate::persist::{DriverJson, SettingsJson}; pub struct Driver { @@ -12,96 +12,9 @@ impl Driver { pub fn init( settings: SettingsJson, json_path: std::path::PathBuf, - ) -> Result { - Ok(match settings.version { - 0 => Self::version0(settings, json_path)?, - _ => Self { - general: Box::new(General { - persistent: settings.persistent, - path: json_path, - name: settings.name, - driver: DriverJson::SteamDeck, - events: settings.events.unwrap_or_default(), - }), - cpus: Box::new(super::steam_deck::Cpus::from_json( - settings.cpus, - settings.version, - )), - gpu: Box::new(super::steam_deck::Gpu::from_json( - settings.gpu, - settings.version, - )), - battery: Box::new(super::steam_deck::Battery::from_json( - settings.battery, - settings.version, - )), - }, - }) - } - - fn version0( - settings: SettingsJson, - json_path: std::path::PathBuf, - ) -> Result { - let name = settings.name.clone(); - if let Some(provider) = &settings.provider { - match provider { - DriverJson::SteamDeck => Ok(Self { - general: Box::new(General { - persistent: settings.persistent, - path: json_path, - name: settings.name, - driver: DriverJson::SteamDeck, - events: settings.events.unwrap_or_default(), - }), - cpus: Box::new(super::steam_deck::Cpus::from_json( - settings.cpus, - settings.version, - )), - gpu: Box::new(super::steam_deck::Gpu::from_json( - settings.gpu, - settings.version, - )), - battery: Box::new(super::steam_deck::Battery::from_json( - settings.battery, - settings.version, - )), - }), - // There's nothing special about SteamDeckAdvance, it just appears different - DriverJson::SteamDeckAdvance => Ok(Self { - general: Box::new(General { - persistent: settings.persistent, - path: json_path, - name: settings.name, - driver: DriverJson::SteamDeckAdvance, - events: settings.events.unwrap_or_default(), - }), - cpus: Box::new(super::steam_deck::Cpus::from_json( - settings.cpus, - settings.version, - )), - gpu: Box::new(super::steam_deck::Gpu::from_json( - settings.gpu, - settings.version, - )), - battery: Box::new(super::steam_deck::Battery::from_json( - settings.battery, - settings.version, - )), - }), - DriverJson::Generic | DriverJson::GenericAMD => { - Ok(super::detect::auto_detect0(Some(settings), json_path, name)) - } - DriverJson::Unknown => { - Ok(super::detect::auto_detect0(Some(settings), json_path, name)) - } - DriverJson::AutoDetect => { - Ok(super::detect::auto_detect0(Some(settings), json_path, name)) - } - } - } else { - Ok(super::detect::auto_detect0(Some(settings), json_path, name)) - } + ) -> Self { + let name_bup = settings.name.clone(); + auto_detect0(Some(settings), json_path, name_bup) } pub fn system_default(json_path: std::path::PathBuf, name: String) -> Self { diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index 1f47bc2..a4b7844 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -156,27 +156,19 @@ impl OnSet for Settings { impl Settings { #[inline] pub fn from_json(other: SettingsJson, json_path: PathBuf) -> Self { - let name_bup = other.name.clone(); - match super::Driver::init(other, json_path.clone()) { - Ok(x) => { - log::info!( - "Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", - x.general.provider(), - x.cpus.provider(), - x.gpu.provider(), - x.battery.provider() - ); - Self { - general: x.general, - cpus: x.cpus, - gpu: x.gpu, - battery: x.battery, - } - } - Err(e) => { - log::error!("Driver init error: {}", e); - Self::system_default(json_path, name_bup) - } + let x = super::Driver::init(other, json_path.clone()); + log::info!( + "Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", + x.general.provider(), + x.cpus.provider(), + x.gpu.provider(), + x.battery.provider() + ); + Self { + general: x.general, + cpus: x.cpus, + gpu: x.gpu, + battery: x.battery, } } @@ -219,22 +211,12 @@ impl Settings { *self.general.persistent() = false; self.general.name(name); } else { - match super::Driver::init(settings_json, json_path.clone()) { - Ok(x) => { - log::info!("Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), x.cpus.provider(), x.gpu.provider(), x.battery.provider()); - self.general = x.general; - self.cpus = x.cpus; - self.gpu = x.gpu; - self.battery = x.battery; - } - Err(e) => { - log::error!("Driver init error: {}", e); - self.general.name(name); - *self.general.persistent() = false; - self.general.path(json_path); - return Err(e); - } - }; + let x = super::Driver::init(settings_json, json_path.clone()); + log::info!("Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), x.cpus.provider(), x.gpu.provider(), x.battery.provider()); + self.general = x.general; + self.cpus = x.cpus; + self.gpu = x.gpu; + self.battery = x.battery; } } else { if system_defaults { diff --git a/backend/src/settings/generic/battery.rs b/backend/src/settings/generic/battery.rs index 81464dd..2c64075 100644 --- a/backend/src/settings/generic/battery.rs +++ b/backend/src/settings/generic/battery.rs @@ -1,10 +1,10 @@ use std::convert::Into; -use limits_core::json::GenericBatteryLimit; +use limits_core::json_v2::GenericBatteryLimit; use sysfuss::SysEntity; use crate::persist::BatteryJson; -use crate::settings::TBattery; +use crate::settings::{TBattery, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError}; #[derive(Debug, Clone)] @@ -56,24 +56,26 @@ impl Battery { } } } +} - pub fn from_limits(limits: limits_core::json::GenericBatteryLimit) -> Self { - // TODO - Self { - limits, - sysfs: Self::find_psu_sysfs(None::<&'static str>), - } - } - - pub fn from_json_and_limits( - other: BatteryJson, +impl ProviderBuilder for Battery { + fn from_json_and_limits( + persistent: BatteryJson, _version: u64, - limits: limits_core::json::GenericBatteryLimit, + limits: GenericBatteryLimit, ) -> Self { // TODO Self { limits, - sysfs: Self::find_psu_sysfs(other.root) + sysfs: Self::find_psu_sysfs(persistent.root) + } + } + + fn from_limits(limits: GenericBatteryLimit) -> Self { + // TODO + Self { + limits, + sysfs: Self::find_psu_sysfs(None::<&'static str>), } } } diff --git a/backend/src/settings/generic/cpu.rs b/backend/src/settings/generic/cpu.rs index 00f7dbe..40d2056 100644 --- a/backend/src/settings/generic/cpu.rs +++ b/backend/src/settings/generic/cpu.rs @@ -1,13 +1,13 @@ use std::convert::{AsMut, AsRef, Into}; -use limits_core::json::GenericCpuLimit; +use limits_core::json_v2::{GenericCpusLimit, GenericCpuLimit}; use super::FromGenericCpuInfo; use crate::api::RangeLimit; use crate::persist::CpuJson; use crate::settings::{min_max_from_json, MinMax}; use crate::settings::{OnResume, OnSet, SettingError}; -use crate::settings::{TCpu, TCpus}; +use crate::settings::{TCpu, TCpus, ProviderBuilder}; const CPU_PRESENT_PATH: &str = "/sys/devices/system/cpu/present"; const CPU_SMT_PATH: &str = "/sys/devices/system/cpu/smt/control"; @@ -87,31 +87,15 @@ impl + AsRef + TCpu + FromGenericCpuInfo> Cpus { Err(_) => (false, false), } } +} - pub fn from_limits(limits: limits_core::json::GenericCpuLimit) -> Self { - let cpu_count = Self::cpu_count().unwrap_or(8); - let (_, can_smt) = Self::system_smt_capabilities(); - let mut new_cpus = Vec::with_capacity(cpu_count); - for i in 0..cpu_count { - let new_cpu = C::from_limits(i, limits.clone()); - new_cpus.push(new_cpu); - } - Self { - cpus: new_cpus, - smt: true, - smt_capable: can_smt, - } - } - - pub fn from_json_and_limits( - mut other: Vec, - version: u64, - limits: limits_core::json::GenericCpuLimit, - ) -> Self { +impl + AsRef + TCpu + FromGenericCpuInfo> ProviderBuilder, GenericCpusLimit> for Cpus { + fn from_json_and_limits(mut other: Vec, version: u64, limits: GenericCpusLimit) -> Self { let (_, can_smt) = Self::system_smt_capabilities(); let mut result = Vec::with_capacity(other.len()); let max_cpus = Self::cpu_count(); let smt_guess = crate::settings::util::guess_smt(&other) && can_smt; + let fallback_cpu_limit = GenericCpuLimit::default(); for (i, cpu) in other.drain(..).enumerate() { // prevent having more CPUs than available if let Some(max_cpus) = max_cpus { @@ -119,7 +103,10 @@ impl + AsRef + TCpu + FromGenericCpuInfo> Cpus { break; } } - let new_cpu = C::from_json_and_limits(cpu, version, i, limits.clone()); + let cpu_limit = limits.cpus.get(i) + .or_else(|| limits.cpus.get(0)) + .unwrap_or_else(|| &fallback_cpu_limit).clone(); + let new_cpu = C::from_json_and_limits(cpu, version, i, cpu_limit); result.push(new_cpu); } if let Some(max_cpus) = max_cpus { @@ -136,6 +123,25 @@ impl + AsRef + TCpu + FromGenericCpuInfo> Cpus { smt_capable: can_smt, } } + + fn from_limits(limits: GenericCpusLimit) -> Self { + let cpu_count = Self::cpu_count().unwrap_or(8); + let (_, can_smt) = Self::system_smt_capabilities(); + let mut new_cpus = Vec::with_capacity(cpu_count); + let fallback_cpu_limit = GenericCpuLimit::default(); + for i in 0..cpu_count { + let cpu_limit = limits.cpus.get(i) + .or_else(|| limits.cpus.get(0)) + .unwrap_or_else(|| &fallback_cpu_limit).clone(); + let new_cpu = C::from_limits(i, cpu_limit); + new_cpus.push(new_cpu); + } + Self { + cpus: new_cpus, + smt: true, + smt_capable: can_smt, + } + } } impl + AsRef + TCpu + crate::settings::OnPowerEvent> @@ -345,7 +351,7 @@ impl Cpu { .clock_max .clone() .map(|x| RangeLimit::new(x.min.unwrap_or(0), x.max.unwrap_or(5_000))), - clock_step: self.limits.clock_step, + clock_step: self.limits.clock_step.unwrap_or(100), governors: self.governors(), } } diff --git a/backend/src/settings/generic/gpu.rs b/backend/src/settings/generic/gpu.rs index b8fcb6c..0bee174 100644 --- a/backend/src/settings/generic/gpu.rs +++ b/backend/src/settings/generic/gpu.rs @@ -1,11 +1,11 @@ use std::convert::Into; -use limits_core::json::GenericGpuLimit; +use limits_core::json_v2::GenericGpuLimit; use sysfuss::{BasicEntityPath, SysEntity}; use crate::api::RangeLimit; use crate::persist::GpuJson; -use crate::settings::TGpu; +use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{min_max_from_json, MinMax}; use crate::settings::{OnResume, OnSet, SettingError}; @@ -49,8 +49,34 @@ impl Gpu { } } } +} - pub fn from_limits(limits: limits_core::json::GenericGpuLimit) -> Self { +impl ProviderBuilder for Gpu { + fn from_json_and_limits(persistent: GpuJson, version: u64, limits: GenericGpuLimit) -> Self { + let clock_lims = if limits.clock_min.is_some() && limits.clock_max.is_some() { + persistent.clock_limits.map(|x| min_max_from_json(x, version)) + } else { + None + }; + Self { + slow_memory: false, + fast_ppt: if limits.fast_ppt.is_some() { + persistent.fast_ppt + } else { + None + }, + slow_ppt: if limits.slow_ppt.is_some() { + persistent.slow_ppt + } else { + None + }, + clock_limits: clock_lims, + limits, + sysfs: Self::find_card_sysfs(persistent.root) + } + } + + fn from_limits(limits: GenericGpuLimit) -> Self { Self { slow_memory: false, fast_ppt: None, @@ -60,34 +86,6 @@ impl Gpu { sysfs: Self::find_card_sysfs(None::<&'static str>), } } - - pub fn from_json_and_limits( - other: GpuJson, - version: u64, - limits: limits_core::json::GenericGpuLimit, - ) -> Self { - let clock_lims = if limits.clock_min.is_some() && limits.clock_max.is_some() { - other.clock_limits.map(|x| min_max_from_json(x, version)) - } else { - None - }; - Self { - slow_memory: false, - fast_ppt: if limits.fast_ppt.is_some() { - other.fast_ppt - } else { - None - }, - slow_ppt: if limits.slow_ppt.is_some() { - other.slow_ppt - } else { - None - }, - clock_limits: clock_lims, - limits, - sysfs: Self::find_card_sysfs(other.root) - } - } } impl Into for Gpu { diff --git a/backend/src/settings/generic/mod.rs b/backend/src/settings/generic/mod.rs index 6989a23..9a99d47 100644 --- a/backend/src/settings/generic/mod.rs +++ b/backend/src/settings/generic/mod.rs @@ -7,3 +7,11 @@ pub use battery::Battery; pub use cpu::{Cpu, Cpus}; pub use gpu::Gpu; pub use traits::FromGenericCpuInfo; + +fn _impl_checker() { + fn impl_provider_builder, J, L>() {} + + impl_provider_builder::(); + impl_provider_builder::, Vec, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::(); +} diff --git a/backend/src/settings/generic/traits.rs b/backend/src/settings/generic/traits.rs index a8674bb..58d91ad 100644 --- a/backend/src/settings/generic/traits.rs +++ b/backend/src/settings/generic/traits.rs @@ -1,6 +1,7 @@ use crate::persist::CpuJson; -use limits_core::json::GenericCpuLimit; +use limits_core::json_v2::GenericCpuLimit; +// similar to crate::settings::ProviderBuilder pub trait FromGenericCpuInfo { fn from_limits(cpu_index: usize, limits: GenericCpuLimit) -> Self; diff --git a/backend/src/settings/generic_amd/cpu.rs b/backend/src/settings/generic_amd/cpu.rs index 48e119c..07aef41 100644 --- a/backend/src/settings/generic_amd/cpu.rs +++ b/backend/src/settings/generic_amd/cpu.rs @@ -2,24 +2,24 @@ use crate::persist::CpuJson; use crate::settings::generic::{Cpu as GenericCpu, Cpus as GenericCpus, FromGenericCpuInfo}; use crate::settings::MinMax; use crate::settings::{OnResume, OnSet, SettingError}; -use crate::settings::{TCpu, TCpus}; +use crate::settings::{TCpu, TCpus, ProviderBuilder}; #[derive(Debug)] pub struct Cpus { generic: GenericCpus, } -impl Cpus { - pub fn from_limits(limits: limits_core::json::GenericCpuLimit) -> Self { +impl ProviderBuilder, limits_core::json_v2::GenericCpusLimit> for Cpus { + fn from_limits(limits: limits_core::json_v2::GenericCpusLimit) -> Self { Self { generic: GenericCpus::from_limits(limits), } } - pub fn from_json_and_limits( + fn from_json_and_limits( other: Vec, version: u64, - limits: limits_core::json::GenericCpuLimit, + limits: limits_core::json_v2::GenericCpusLimit, ) -> Self { Self { generic: GenericCpus::from_json_and_limits(other, version, limits), @@ -75,7 +75,7 @@ pub struct Cpu { } impl FromGenericCpuInfo for Cpu { - fn from_limits(cpu_index: usize, limits: limits_core::json::GenericCpuLimit) -> Self { + fn from_limits(cpu_index: usize, limits: limits_core::json_v2::GenericCpuLimit) -> Self { let gen = GenericCpu::from_limits(cpu_index, limits.clone()); Self { generic: gen } } @@ -84,7 +84,7 @@ impl FromGenericCpuInfo for Cpu { other: CpuJson, version: u64, cpu_index: usize, - limits: limits_core::json::GenericCpuLimit, + limits: limits_core::json_v2::GenericCpuLimit, ) -> Self { let gen = GenericCpu::from_json_and_limits(other, version, cpu_index, limits); Self { generic: gen } diff --git a/backend/src/settings/generic_amd/gpu.rs b/backend/src/settings/generic_amd/gpu.rs index 9af3bc2..9d0090b 100644 --- a/backend/src/settings/generic_amd/gpu.rs +++ b/backend/src/settings/generic_amd/gpu.rs @@ -4,7 +4,7 @@ use std::sync::Mutex; use crate::persist::GpuJson; use crate::settings::generic::Gpu as GenericGpu; use crate::settings::MinMax; -use crate::settings::TGpu; +use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError, SettingVariant}; fn ryzen_adj_or_log() -> Option> { @@ -35,8 +35,8 @@ impl std::fmt::Debug for Gpu { } } -impl Gpu { - pub fn from_limits(limits: limits_core::json::GenericGpuLimit) -> Self { +impl ProviderBuilder for Gpu { + fn from_limits(limits: limits_core::json_v2::GenericGpuLimit) -> Self { Self { generic: GenericGpu::from_limits(limits), implementor: ryzen_adj_or_log(), @@ -44,10 +44,10 @@ impl Gpu { } } - pub fn from_json_and_limits( + fn from_json_and_limits( other: GpuJson, version: u64, - limits: limits_core::json::GenericGpuLimit, + limits: limits_core::json_v2::GenericGpuLimit, ) -> Self { Self { generic: GenericGpu::from_json_and_limits(other, version, limits), @@ -55,6 +55,9 @@ impl Gpu { state: Default::default(), } } +} + +impl Gpu { fn set_all(&mut self) -> Result<(), Vec> { let mutex = match &self.implementor { diff --git a/backend/src/settings/generic_amd/mod.rs b/backend/src/settings/generic_amd/mod.rs index 6a8e412..0f443a9 100644 --- a/backend/src/settings/generic_amd/mod.rs +++ b/backend/src/settings/generic_amd/mod.rs @@ -3,3 +3,10 @@ mod gpu; pub use cpu::{Cpu, Cpus}; pub use gpu::Gpu; + +fn _impl_checker() { + fn impl_provider_builder, J, L>() {} + + impl_provider_builder::, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::(); +} diff --git a/backend/src/settings/mod.rs b/backend/src/settings/mod.rs index c3a0044..5aa3483 100644 --- a/backend/src/settings/mod.rs +++ b/backend/src/settings/mod.rs @@ -17,7 +17,7 @@ pub use general::{General, SettingVariant, Settings}; pub use min_max::{min_max_from_json, MinMax}; pub use error::SettingError; -pub use traits::{OnPowerEvent, OnResume, OnSet, PowerMode, TBattery, TCpu, TCpus, TGeneral, TGpu}; +pub use traits::{OnPowerEvent, OnResume, OnSet, PowerMode, TBattery, TCpu, TCpus, TGeneral, TGpu, ProviderBuilder}; #[cfg(test)] mod tests { diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index d5b800e..07ac1a9 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -4,11 +4,12 @@ use std::sync::Arc; use sysfuss::{PowerSupplyAttribute, PowerSupplyPath, HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, SysEntity, SysEntityAttributesExt, SysAttribute}; use sysfuss::capability::attributes; -use super::oc_limits::{BatteryLimits, OverclockLimits}; +use limits_core::json_v2::GenericBatteryLimit; + use super::util::ChargeMode; use crate::api::RangeLimit; use crate::persist::{BatteryEventJson, BatteryJson}; -use crate::settings::TBattery; +use crate::settings::{TBattery, ProviderBuilder}; use crate::settings::{OnPowerEvent, OnResume, OnSet, PowerMode, SettingError}; #[derive(Debug, Clone)] @@ -16,9 +17,8 @@ pub struct Battery { pub charge_rate: Option, pub charge_mode: Option, events: Vec, - limits: BatteryLimits, + limits: GenericBatteryLimit, state: crate::state::steam_deck::Battery, - driver_mode: crate::persist::DriverJson, sysfs_bat: PowerSupplyPath, sysfs_hwmon: Arc, } @@ -217,55 +217,10 @@ const MAXIMUM_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute = HwMonAttribute::custom( const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute = HwMonAttribute::custom("maximum_battery_charge_rate"); const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute = HwMonAttribute::custom("max_battery_charge_level"); -impl Battery { - #[inline] - pub fn from_json(other: BatteryJson, version: u64) -> Self { - let (oc_limits, is_default) = OverclockLimits::load_or_default(); - let oc_limits = oc_limits.battery; - let driver = if is_default { - crate::persist::DriverJson::SteamDeck - } else { - crate::persist::DriverJson::SteamDeckAdvance - }; - let hwmon_sys = Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)); - match version { - 0 => Self { - charge_rate: other.charge_rate, - charge_mode: other - .charge_mode - .map(|x| Self::str_to_charge_mode(&x)) - .flatten(), - events: other - .events - .into_iter() - .map(|x| EventInstruction::from_json(x, version, hwmon_sys.clone())) - .collect(), - limits: oc_limits, - state: crate::state::steam_deck::Battery::default(), - driver_mode: driver, - sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), - sysfs_hwmon: hwmon_sys, - }, - _ => Self { - charge_rate: other.charge_rate, - charge_mode: other - .charge_mode - .map(|x| Self::str_to_charge_mode(&x)) - .flatten(), - events: other - .events - .into_iter() - .map(|x| EventInstruction::from_json(x, version, hwmon_sys.clone())) - .collect(), - limits: oc_limits, - state: crate::state::steam_deck::Battery::default(), - driver_mode: driver, - sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), - sysfs_hwmon: hwmon_sys, - }, - } - } +const MAX_CHARGE_RATE: u64 = 2500; +const MIN_CHARGE_RATE: u64 = 250; +impl Battery { fn find_battery_sysfs(root: Option>) -> PowerSupplyPath { let root = crate::settings::util::root_or_default_sysfs(root); match root.power_supply(attributes(BATTERY_NEEDS.into_iter().copied())) { @@ -360,7 +315,7 @@ impl Battery { MAXIMUM_BATTERY_CHARGE_RATE_ATTR }; let path = attr.path(&*self.sysfs_hwmon); - self.sysfs_hwmon.set(attr, self.limits.charge_rate.max,).map_err( + self.sysfs_hwmon.set(attr, self.limits.charge_rate.and_then(|lim| lim.max).unwrap_or(2500)).map_err( |e| SettingError { msg: format!("Failed to write to `{}`: {}", path.display(), e), setting: crate::settings::SettingVariant::Battery, @@ -407,7 +362,7 @@ impl Battery { fn clamp_all(&mut self) { if let Some(charge_rate) = &mut self.charge_rate { *charge_rate = - (*charge_rate).clamp(self.limits.charge_rate.min, self.limits.charge_rate.max); + (*charge_rate).clamp(self.limits.charge_rate.and_then(|lim| lim.min).unwrap_or(MIN_CHARGE_RATE), self.limits.charge_rate.and_then(|lim| lim.max).unwrap_or(MAX_CHARGE_RATE)); } } @@ -489,26 +444,6 @@ impl Battery { } } - pub fn system_default() -> Self { - let (oc_limits, is_default) = OverclockLimits::load_or_default(); - let oc_limits = oc_limits.battery; - let driver = if is_default { - crate::persist::DriverJson::SteamDeck - } else { - crate::persist::DriverJson::SteamDeckAdvance - }; - Self { - charge_rate: None, - charge_mode: None, - events: Vec::new(), - limits: oc_limits, - state: crate::state::steam_deck::Battery::default(), - driver_mode: driver, - sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), - sysfs_hwmon: Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)), - } - } - fn find_limit_event(&self) -> Option { for (i, event) in self.events.iter().enumerate() { match event.trigger { @@ -550,6 +485,58 @@ impl Into for Battery { } } +impl ProviderBuilder for Battery { + fn from_json_and_limits(persistent: BatteryJson, version: u64, limits: GenericBatteryLimit) -> Self { + let hwmon_sys = Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)); + match version { + 0 => Self { + charge_rate: persistent.charge_rate, + charge_mode: persistent + .charge_mode + .map(|x| Self::str_to_charge_mode(&x)) + .flatten(), + events: persistent + .events + .into_iter() + .map(|x| EventInstruction::from_json(x, version, hwmon_sys.clone())) + .collect(), + limits: limits, + state: crate::state::steam_deck::Battery::default(), + sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), + sysfs_hwmon: hwmon_sys, + }, + _ => Self { + charge_rate: persistent.charge_rate, + charge_mode: persistent + .charge_mode + .map(|x| Self::str_to_charge_mode(&x)) + .flatten(), + events: persistent + .events + .into_iter() + .map(|x| EventInstruction::from_json(x, version, hwmon_sys.clone())) + .collect(), + limits: limits, + state: crate::state::steam_deck::Battery::default(), + sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), + sysfs_hwmon: hwmon_sys, + }, + } + } + + fn from_limits(limits: GenericBatteryLimit) -> Self { + Self { + charge_rate: None, + charge_mode: None, + events: Vec::new(), + limits: limits, + state: crate::state::steam_deck::Battery::default(), + sysfs_bat: Self::find_battery_sysfs(None::<&'static str>), + sysfs_hwmon: Arc::new(Self::find_hwmon_sysfs(None::<&'static str>)), + } + } +} + impl OnSet for Battery { fn on_set(&mut self) -> Result<(), Vec> { self.clamp_all(); @@ -631,8 +618,8 @@ impl TBattery for Battery { fn limits(&self) -> crate::api::BatteryLimits { crate::api::BatteryLimits { charge_current: Some(RangeLimit { - min: self.limits.charge_rate.min, - max: self.limits.charge_rate.max, + min: self.limits.charge_rate.and_then(|lim| lim.min).unwrap_or(MIN_CHARGE_RATE), + max: self.limits.charge_rate.and_then(|lim| lim.max).unwrap_or(MAX_CHARGE_RATE), }), charge_current_step: 50, charge_modes: vec![ @@ -844,6 +831,6 @@ impl TBattery for Battery { } fn provider(&self) -> crate::persist::DriverJson { - self.driver_mode.clone() + crate::persist::DriverJson::SteamDeck } } diff --git a/backend/src/settings/steam_deck/cpu.rs b/backend/src/settings/steam_deck/cpu.rs index c6ac04e..3a5cc56 100644 --- a/backend/src/settings/steam_deck/cpu.rs +++ b/backend/src/settings/steam_deck/cpu.rs @@ -2,13 +2,15 @@ use std::convert::Into; use sysfuss::{BasicEntityPath, SysEntity, SysEntityAttributesExt}; -use super::oc_limits::{CpuLimits, CpusLimits, OverclockLimits}; +use limits_core::json_v2::{GenericCpusLimit, GenericCpuLimit}; + use super::POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT; +use super::util::{range_max_or_fallback, range_min_or_fallback}; use crate::api::RangeLimit; use crate::persist::CpuJson; use crate::settings::{min_max_from_json, MinMax}; use crate::settings::{OnResume, OnSet, SettingError}; -use crate::settings::{TCpu, TCpus}; +use crate::settings::{TCpu, TCpus, ProviderBuilder}; const CPU_PRESENT_PATH: &str = "/sys/devices/system/cpu/present"; const CPU_SMT_PATH: &str = "/sys/devices/system/cpu/smt/control"; @@ -17,13 +19,17 @@ const CARD_EXTENSIONS: &[&'static str] = &[ super::DPM_FORCE_LIMITS_ATTRIBUTE ]; +const MAX_CLOCK: u64 = 3500; +const MIN_MAX_CLOCK: u64 = 200; // minimum value allowed for maximum CPU clock, MHz +const MIN_MIN_CLOCK: u64 = 1400; // minimum value allowed for minimum CPU clock, MHz +const CLOCK_STEP: u64 = 100; + #[derive(Debug, Clone)] pub struct Cpus { pub cpus: Vec, pub smt: bool, pub smt_capable: bool, - pub(super) limits: CpusLimits, - driver_mode: crate::persist::DriverJson, + pub(super) limits: GenericCpusLimit, } impl OnSet for Cpus { @@ -94,85 +100,42 @@ impl Cpus { Err(_) => (false, false), } } +} - pub fn system_default() -> Self { +impl ProviderBuilder, GenericCpusLimit> for Cpus { + fn from_json_and_limits(mut persistent: Vec, version: u64, limits: GenericCpusLimit) -> Self { POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.reset(); - let (oc_limits, is_default) = OverclockLimits::load_or_default(); - let oc_limits = oc_limits.cpus; - let driver = if is_default { - crate::persist::DriverJson::SteamDeck - } else { - crate::persist::DriverJson::SteamDeckAdvance - }; - if let Some(max_cpu) = Self::cpu_count() { - let mut sys_cpus = Vec::with_capacity(max_cpu); - for i in 0..max_cpu { - sys_cpus.push(Cpu::system_default( - i, - oc_limits - .cpus - .get(i) - .map(|x| x.to_owned()) - .unwrap_or_default(), - )); - } - let (_, can_smt) = Self::system_smt_capabilities(); - Self { - cpus: sys_cpus, - smt: true, - smt_capable: can_smt, - limits: oc_limits, - driver_mode: driver, - } - } else { - Self { - cpus: vec![], - smt: false, - smt_capable: false, - limits: oc_limits, - driver_mode: driver, - } - } - } - - #[inline] - pub fn from_json(mut other: Vec, version: u64) -> Self { - POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.reset(); - let (oc_limits, is_default) = OverclockLimits::load_or_default(); - let oc_limits = oc_limits.cpus; - let driver = if is_default { - crate::persist::DriverJson::SteamDeck - } else { - crate::persist::DriverJson::SteamDeckAdvance - }; let (_, can_smt) = Self::system_smt_capabilities(); - let mut result = Vec::with_capacity(other.len()); + let mut result = Vec::with_capacity(persistent.len()); let max_cpus = Self::cpu_count(); - let smt_guess = crate::settings::util::guess_smt(&other) && can_smt; - for (i, cpu) in other.drain(..).enumerate() { + let smt_guess = crate::settings::util::guess_smt(&persistent) && can_smt; + for (i, cpu) in persistent.drain(..).enumerate() { // prevent having more CPUs than available if let Some(max_cpus) = max_cpus { if i == max_cpus { break; } } - let new_cpu = Cpu::from_json( - cpu, - version, - i, - oc_limits - .cpus - .get(i) - .map(|x| x.to_owned()) - .unwrap_or_default(), - ); + let new_cpu = if let Some(cpu_limit) = limits.cpus.get(i) { + Cpu::from_json_and_limits( + cpu, + version, + i, + cpu_limit.to_owned() + ) + } else { + Cpu::from_json( + cpu, + version, + i, + ) + }; result.push(new_cpu); } if let Some(max_cpus) = max_cpus { if result.len() != max_cpus { - let mut sys_cpus = Cpus::system_default(); - for i in result.len()..sys_cpus.cpus.len() { - result.push(sys_cpus.cpus.remove(i)); + for i in result.len()..max_cpus { + result.push(Cpu::system_default(i)); } } } @@ -180,8 +143,39 @@ impl Cpus { cpus: result, smt: smt_guess, smt_capable: can_smt, - limits: oc_limits, - driver_mode: driver, + limits: limits, + } + } + + fn from_limits(limits: GenericCpusLimit) -> Self { + POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.reset(); + if let Some(max_cpu) = Self::cpu_count() { + let mut sys_cpus = Vec::with_capacity(max_cpu); + for i in 0..max_cpu { + let new_cpu = if let Some(cpu_limit) = limits.cpus.get(i) { + Cpu::from_limits( + i, + cpu_limit.to_owned() + ) + } else { + Cpu::system_default(i) + }; + sys_cpus.push(new_cpu); + } + let (_, can_smt) = Self::system_smt_capabilities(); + Self { + cpus: sys_cpus, + smt: true, + smt_capable: can_smt, + limits: limits, + } + } else { + Self { + cpus: vec![], + smt: false, + smt_capable: false, + limits: limits, + } } } } @@ -224,7 +218,7 @@ impl TCpus for Cpus { } fn provider(&self) -> crate::persist::DriverJson { - self.driver_mode.clone() + crate::persist::DriverJson::SteamDeck } } @@ -233,7 +227,7 @@ pub struct Cpu { pub online: bool, pub clock_limits: Option>, pub governor: String, - limits: CpuLimits, + limits: GenericCpuLimit, index: usize, state: crate::state::steam_deck::Cpu, sysfs: BasicEntityPath, @@ -249,7 +243,7 @@ enum ClockType { impl Cpu { #[inline] - fn from_json(other: CpuJson, version: u64, i: usize, oc_limits: CpuLimits) -> Self { + fn from_json_and_limits(other: CpuJson, version: u64, i: usize, oc_limits: GenericCpuLimit) -> Self { match version { 0 => Self { online: other.online, @@ -272,6 +266,12 @@ impl Cpu { } } + #[inline] + fn from_json(other: CpuJson, version: u64, i: usize) -> Self { + let oc_limits = GenericCpuLimit::default_for(&limits_core::json_v2::CpuLimitType::SteamDeck, i); + Self::from_json_and_limits(other, version, i, oc_limits) + } + fn find_card_sysfs(root: Option>) -> BasicEntityPath { let root = crate::settings::util::root_or_default_sysfs(root); match root.class("drm", sysfuss::capability::attributes(crate::settings::util::CARD_NEEDS.into_iter().map(|s| s.to_string()))) { @@ -338,8 +338,8 @@ impl Cpu { } // min clock if let Some(min) = clock_limits.min { - let valid_min = if min < self.limits.clock_min.min { - self.limits.clock_min.min + let valid_min = if min < range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK) { + range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK) } else { min }; @@ -367,10 +367,10 @@ impl Cpu { // disable manual clock limits log::debug!("Setting CPU {} to default clockspeed", self.index); // max clock - self.set_clock_limit(self.index, self.limits.clock_max.max, ClockType::Max) + self.set_clock_limit(self.index, range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), ClockType::Max) .unwrap_or_else(|e| errors.push(e)); // min clock - self.set_clock_limit(self.index, self.limits.clock_min.min, ClockType::Min) + self.set_clock_limit(self.index, range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK), ClockType::Min) .unwrap_or_else(|e| errors.push(e)); } // TODO remove this when it's no longer needed @@ -395,10 +395,10 @@ impl Cpu { // disable manual clock limits log::debug!("Setting CPU {} to default clockspeed", self.index); // max clock - self.set_clock_limit(self.index, self.limits.clock_max.max, ClockType::Max) + self.set_clock_limit(self.index, range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), ClockType::Max) .unwrap_or_else(|e| errors.push(e)); // min clock - self.set_clock_limit(self.index, self.limits.clock_min.min, ClockType::Min) + self.set_clock_limit(self.index, range_min_or_fallback(&self.limits.clock_min, MIN_MIN_CLOCK), ClockType::Min) .unwrap_or_else(|e| errors.push(e)); self.set_confirm().unwrap_or_else(|e| errors.push(e)); @@ -493,11 +493,11 @@ impl Cpu { if let Some(clock_limits) = &mut self.clock_limits { if let Some(min) = clock_limits.min { clock_limits.min = - Some(min.clamp(self.limits.clock_max.min, self.limits.clock_min.max)); + Some(min.clamp(range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), range_max_or_fallback(&self.limits.clock_min, MAX_CLOCK))); } if let Some(max) = clock_limits.max { clock_limits.max = - Some(max.clamp(self.limits.clock_max.min, self.limits.clock_max.max)); + Some(max.clamp(range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK))); } } } @@ -514,7 +514,7 @@ impl Cpu { } }*/ - fn system_default(cpu_index: usize, oc_limits: CpuLimits) -> Self { + fn from_limits(cpu_index: usize, oc_limits: GenericCpuLimit) -> Self { Self { online: true, clock_limits: None, @@ -526,17 +526,21 @@ impl Cpu { } } + fn system_default(cpu_index: usize) -> Self { + Self::from_limits(cpu_index, GenericCpuLimit::default_for(&limits_core::json_v2::CpuLimitType::SteamDeck, cpu_index)) + } + fn limits(&self) -> crate::api::CpuLimits { crate::api::CpuLimits { clock_min_limits: Some(RangeLimit { - min: self.limits.clock_max.min, // allows min to be set by max (it's weird, blame the kernel) - max: self.limits.clock_min.max, + min: range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), // allows min to be set by max (it's weird, blame the kernel) + max: range_max_or_fallback(&self.limits.clock_min, MAX_CLOCK), }), clock_max_limits: Some(RangeLimit { - min: self.limits.clock_max.min, - max: self.limits.clock_max.max, + min: range_min_or_fallback(&self.limits.clock_max, MIN_MAX_CLOCK), + max: range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), }), - clock_step: self.limits.clock_step, + clock_step: self.limits.clock_step.unwrap_or(CLOCK_STEP), governors: self.governors(), } } diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index f1f428b..96316f0 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -2,11 +2,12 @@ use std::convert::Into; use sysfuss::{BasicEntityPath, HwMonPath, SysEntity, capability::attributes, SysEntityAttributesExt, SysAttribute}; -use super::oc_limits::{GpuLimits, OverclockLimits}; +use limits_core::json_v2::GenericGpuLimit; + use super::POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT; use crate::api::RangeLimit; use crate::persist::GpuJson; -use crate::settings::TGpu; +use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{min_max_from_json, MinMax}; use crate::settings::{OnResume, OnSet, SettingError}; @@ -20,9 +21,8 @@ pub struct Gpu { pub slow_ppt: Option, pub clock_limits: Option>, pub slow_memory: bool, - limits: GpuLimits, + limits: GenericGpuLimit, state: crate::state::steam_deck::Gpu, - driver_mode: crate::persist::DriverJson, sysfs_card: BasicEntityPath, sysfs_hwmon: HwMonPath } @@ -45,41 +45,16 @@ enum ClockType { Max = 1, } -impl Gpu { - #[inline] - pub fn from_json(other: GpuJson, version: u64) -> Self { - let (oc_limits, is_default) = OverclockLimits::load_or_default(); - let driver = if is_default { - crate::persist::DriverJson::SteamDeck - } else { - crate::persist::DriverJson::SteamDeckAdvance - }; - match version { - 0 => Self { - fast_ppt: other.fast_ppt, - slow_ppt: other.slow_ppt, - clock_limits: other.clock_limits.map(|x| min_max_from_json(x, version)), - slow_memory: other.slow_memory, - limits: oc_limits.gpu, - state: crate::state::steam_deck::Gpu::default(), - driver_mode: driver, - sysfs_card: Self::find_card_sysfs(other.root.clone()), - sysfs_hwmon: Self::find_hwmon_sysfs(other.root), - }, - _ => Self { - fast_ppt: other.fast_ppt, - slow_ppt: other.slow_ppt, - clock_limits: other.clock_limits.map(|x| min_max_from_json(x, version)), - slow_memory: other.slow_memory, - limits: oc_limits.gpu, - state: crate::state::steam_deck::Gpu::default(), - driver_mode: driver, - sysfs_card: Self::find_card_sysfs(other.root.clone()), - sysfs_hwmon: Self::find_hwmon_sysfs(other.root), - }, - } - } +const MAX_CLOCK: u64 = 1600; +const MIN_CLOCK: u64 = 200; +const MAX_FAST_PPT: u64 = 30_000_000; +const MIN_FAST_PPT: u64 = 1_000_000; +const MAX_SLOW_PPT: u64 = 29_000_000; +const MIN_SLOW_PPT: u64 = 1_000_000; +const MIDDLE_PPT: u64 = 15_000_000; +const PPT_DIVISOR: u64 = 1_000; +impl Gpu { fn find_card_sysfs(root: Option>) -> BasicEntityPath { let root = crate::settings::util::root_or_default_sysfs(root); match root.class("drm", attributes(crate::settings::util::CARD_NEEDS.into_iter().map(|s| s.to_string()))) { @@ -160,10 +135,10 @@ impl Gpu { POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT.enforce_level(&self.sysfs_card)?; // disable manual clock limits // max clock - self.set_clock_limit(self.limits.clock_max.max, ClockType::Max) + self.set_clock_limit(self.limits.clock_max.and_then(|lim| lim.max).unwrap_or(MAX_CLOCK), ClockType::Max) .unwrap_or_else(|e| errors.push(e)); // min clock - self.set_clock_limit(self.limits.clock_min.min, ClockType::Min) + self.set_clock_limit(self.limits.clock_min.and_then(|lim| lim.min).unwrap_or(MIN_CLOCK), ClockType::Min) .unwrap_or_else(|e| errors.push(e)); self.set_confirm().unwrap_or_else(|e| errors.push(e)); @@ -251,7 +226,7 @@ impl Gpu { }); } else if self.state.fast_ppt_set { self.state.fast_ppt_set = false; - let fast_ppt = self.limits.fast_ppt_default; + let fast_ppt = self.limits.fast_ppt_default.unwrap_or(MIDDLE_PPT); self.sysfs_hwmon.set(FAST_PPT_ATTRIBUTE, fast_ppt) .map_err(|e| SettingError { msg: format!( @@ -280,7 +255,7 @@ impl Gpu { }); } else if self.state.slow_ppt_set { self.state.slow_ppt_set = false; - let slow_ppt = self.limits.slow_ppt_default; + let slow_ppt = self.limits.slow_ppt_default.unwrap_or(MIDDLE_PPT); self.sysfs_hwmon.set(SLOW_PPT_ATTRIBUTE, slow_ppt) .map_err(|e| SettingError { msg: format!( @@ -304,41 +279,22 @@ impl Gpu { fn clamp_all(&mut self) { if let Some(fast_ppt) = &mut self.fast_ppt { - *fast_ppt = (*fast_ppt).clamp(self.limits.fast_ppt.min, self.limits.fast_ppt.max); + *fast_ppt = (*fast_ppt).clamp(self.limits.fast_ppt.and_then(|lim| lim.min).unwrap_or(MIN_FAST_PPT), self.limits.fast_ppt.and_then(|lim| lim.max).unwrap_or(MAX_FAST_PPT)); } if let Some(slow_ppt) = &mut self.slow_ppt { - *slow_ppt = (*slow_ppt).clamp(self.limits.slow_ppt.min, self.limits.slow_ppt.max); + *slow_ppt = (*slow_ppt).clamp(self.limits.slow_ppt.and_then(|lim| lim.min).unwrap_or(MIN_SLOW_PPT), self.limits.slow_ppt.and_then(|lim| lim.max).unwrap_or(MAX_SLOW_PPT)); } if let Some(clock_limits) = &mut self.clock_limits { if let Some(min) = clock_limits.min { clock_limits.min = - Some(min.clamp(self.limits.clock_min.min, self.limits.clock_min.max)); + Some(min.clamp(self.limits.clock_min.and_then(|lim| lim.min).unwrap_or(MIN_CLOCK), self.limits.clock_min.and_then(|lim| lim.max).unwrap_or(MAX_CLOCK))); } if let Some(max) = clock_limits.max { clock_limits.max = - Some(max.clamp(self.limits.clock_max.min, self.limits.clock_max.max)); + Some(max.clamp(self.limits.clock_max.and_then(|lim| lim.min).unwrap_or(MIN_CLOCK), self.limits.clock_max.and_then(|lim| lim.max).unwrap_or(MAX_CLOCK))); } } } - - pub fn system_default() -> Self { - let (oc_limits, is_default) = OverclockLimits::load_or_default(); - Self { - fast_ppt: None, - slow_ppt: None, - clock_limits: None, - slow_memory: false, - limits: oc_limits.gpu, - state: crate::state::steam_deck::Gpu::default(), - driver_mode: if is_default { - crate::persist::DriverJson::SteamDeck - } else { - crate::persist::DriverJson::SteamDeckAdvance - }, - sysfs_card: Self::find_card_sysfs(None::<&'static str>), - sysfs_hwmon: Self::find_hwmon_sysfs(None::<&'static str>), - } - } } impl Into for Gpu { @@ -354,6 +310,46 @@ impl Into for Gpu { } } +impl ProviderBuilder for Gpu { + fn from_json_and_limits(persistent: GpuJson, version: u64, limits: GenericGpuLimit) -> Self { + match version { + 0 => Self { + fast_ppt: persistent.fast_ppt, + slow_ppt: persistent.slow_ppt, + clock_limits: persistent.clock_limits.map(|x| min_max_from_json(x, version)), + slow_memory: persistent.slow_memory, + limits: limits, + state: crate::state::steam_deck::Gpu::default(), + sysfs_card: Self::find_card_sysfs(persistent.root.clone()), + sysfs_hwmon: Self::find_hwmon_sysfs(persistent.root), + }, + _ => Self { + fast_ppt: persistent.fast_ppt, + slow_ppt: persistent.slow_ppt, + clock_limits: persistent.clock_limits.map(|x| min_max_from_json(x, version)), + slow_memory: persistent.slow_memory, + limits: limits, + state: crate::state::steam_deck::Gpu::default(), + sysfs_card: Self::find_card_sysfs(persistent.root.clone()), + sysfs_hwmon: Self::find_hwmon_sysfs(persistent.root), + }, + } + } + + fn from_limits(limits: GenericGpuLimit) -> Self { + Self { + fast_ppt: None, + slow_ppt: None, + clock_limits: None, + slow_memory: false, + limits: limits, + state: crate::state::steam_deck::Gpu::default(), + sysfs_card: Self::find_card_sysfs(None::<&'static str>), + sysfs_hwmon: Self::find_hwmon_sysfs(None::<&'static str>), + } + } +} + impl OnSet for Gpu { fn on_set(&mut self) -> Result<(), Vec> { self.clamp_all(); @@ -375,26 +371,26 @@ impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { crate::api::GpuLimits { fast_ppt_limits: Some(RangeLimit { - min: self.limits.fast_ppt.min / self.limits.ppt_divisor, - max: self.limits.fast_ppt.max / self.limits.ppt_divisor, + min: super::util::range_min_or_fallback(&self.limits.fast_ppt, MIN_FAST_PPT) / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + max: super::util::range_max_or_fallback(&self.limits.fast_ppt, MAX_FAST_PPT) / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), }), slow_ppt_limits: Some(RangeLimit { - min: self.limits.slow_ppt.min / self.limits.ppt_divisor, - max: self.limits.slow_ppt.max / self.limits.ppt_divisor, + min: super::util::range_min_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), + max: super::util::range_max_or_fallback(&self.limits.slow_ppt, MIN_SLOW_PPT) / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR), }), - ppt_step: self.limits.ppt_step, + ppt_step: self.limits.ppt_step.unwrap_or(1), tdp_limits: None, tdp_boost_limits: None, tdp_step: 42, clock_min_limits: Some(RangeLimit { - min: self.limits.clock_min.min, - max: self.limits.clock_min.max, + min: super::util::range_min_or_fallback(&self.limits.clock_min, MIN_CLOCK), + max: super::util::range_max_or_fallback(&self.limits.clock_min, MAX_CLOCK), }), clock_max_limits: Some(RangeLimit { - min: self.limits.clock_max.min, - max: self.limits.clock_max.max, + min: super::util::range_min_or_fallback(&self.limits.clock_max, MIN_CLOCK), + max: super::util::range_max_or_fallback(&self.limits.clock_max, MAX_CLOCK), }), - clock_step: self.limits.clock_step, + clock_step: self.limits.clock_step.unwrap_or(100), memory_control_capable: true, } } @@ -404,14 +400,14 @@ impl TGpu for Gpu { } fn ppt(&mut self, fast: Option, slow: Option) { - self.fast_ppt = fast.map(|x| x * self.limits.ppt_divisor); - self.slow_ppt = slow.map(|x| x * self.limits.ppt_divisor); + self.fast_ppt = fast.map(|x| x * self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR)); + self.slow_ppt = slow.map(|x| x * self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR)); } fn get_ppt(&self) -> (Option, Option) { ( - self.fast_ppt.map(|x| x / self.limits.ppt_divisor), - self.slow_ppt.map(|x| x / self.limits.ppt_divisor), + self.fast_ppt.map(|x| x / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR)), + self.slow_ppt.map(|x| x / self.limits.ppt_divisor.unwrap_or(PPT_DIVISOR)), ) } @@ -428,6 +424,6 @@ impl TGpu for Gpu { } fn provider(&self) -> crate::persist::DriverJson { - self.driver_mode.clone() + crate::persist::DriverJson::SteamDeck } } diff --git a/backend/src/settings/steam_deck/mod.rs b/backend/src/settings/steam_deck/mod.rs index 268fa90..676d7fe 100644 --- a/backend/src/settings/steam_deck/mod.rs +++ b/backend/src/settings/steam_deck/mod.rs @@ -1,7 +1,6 @@ mod battery; mod cpu; mod gpu; -mod oc_limits; mod power_dpm_force; mod util; @@ -11,3 +10,11 @@ pub use gpu::Gpu; pub(self) use power_dpm_force::{POWER_DPM_FORCE_PERFORMANCE_LEVEL_MGMT, DPM_FORCE_LIMITS_ATTRIBUTE}; pub use util::flash_led; + +fn _impl_checker() { + fn impl_provider_builder, J, L>() {} + + impl_provider_builder::(); + impl_provider_builder::, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::(); +} diff --git a/backend/src/settings/steam_deck/oc_limits.rs b/backend/src/settings/steam_deck/oc_limits.rs deleted file mode 100644 index f229a0c..0000000 --- a/backend/src/settings/steam_deck/oc_limits.rs +++ /dev/null @@ -1,190 +0,0 @@ -use crate::api::RangeLimit as MinMax; -use serde::{Deserialize, Serialize}; - -const OC_LIMITS_FILEPATH: &str = "pt_oc.json"; - -#[derive(Serialize, Deserialize, Debug)] -pub(super) struct OverclockLimits { - pub battery: BatteryLimits, - pub cpus: CpusLimits, - pub gpu: GpuLimits, -} - -impl Default for OverclockLimits { - fn default() -> Self { - Self { - battery: BatteryLimits::default(), - cpus: CpusLimits::default(), - gpu: GpuLimits::default(), - } - } -} - -impl OverclockLimits { - /// (Self, is_default) - pub fn load_or_default() -> (Self, bool) { - let path = oc_limits_filepath(); - if path.exists() { - log::info!("Steam Deck limits file {} found", path.display()); - let mut file = match std::fs::File::open(&path) { - Ok(f) => f, - Err(e) => { - log::warn!( - "Steam Deck limits file {} err: {} (using default fallback)", - path.display(), - e - ); - return (Self::default(), true); - } - }; - match serde_json::from_reader(&mut file) { - Ok(result) => { - log::debug!( - "Steam Deck limits file {} successfully loaded", - path.display() - ); - (result, false) - } - Err(e) => { - log::warn!( - "Steam Deck limits file {} json err: {} (using default fallback)", - path.display(), - e - ); - (Self::default(), true) - } - } - } else { - log::info!( - "Steam Deck limits file {} not found (using default fallback)", - path.display() - ); - (Self::default(), true) - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub(super) struct BatteryLimits { - pub charge_rate: MinMax, - pub extra_readouts: bool, -} - -impl Default for BatteryLimits { - fn default() -> Self { - Self { - charge_rate: MinMax { - min: 250, - max: 2500, - }, - extra_readouts: false, - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub(super) struct CpusLimits { - pub cpus: Vec, - pub global_governors: bool, -} - -impl Default for CpusLimits { - fn default() -> Self { - Self { - cpus: [(); 8].iter().map(|_| CpuLimits::default()).collect(), - global_governors: true, - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub(super) struct CpuLimits { - pub clock_min: MinMax, - pub clock_max: MinMax, - pub clock_step: u64, - pub skip_resume_reclock: bool, -} - -impl Default for CpuLimits { - fn default() -> Self { - Self { - clock_min: MinMax { - min: 1400, - max: 3500, - }, - clock_max: MinMax { - min: 400, - max: 3500, - }, - clock_step: 100, - skip_resume_reclock: false, - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub(super) struct GpuLimits { - pub fast_ppt: MinMax, - pub fast_ppt_default: u64, - pub slow_ppt: MinMax, - pub slow_ppt_default: u64, - pub ppt_divisor: u64, - pub ppt_step: u64, - pub clock_min: MinMax, - pub clock_max: MinMax, - pub clock_step: u64, - pub skip_resume_reclock: bool, -} - -impl Default for GpuLimits { - fn default() -> Self { - Self { - fast_ppt: MinMax { - min: 1000000, - max: 30_000_000, - }, - fast_ppt_default: 15_000_000, - slow_ppt: MinMax { - min: 1000000, - max: 29_000_000, - }, - slow_ppt_default: 15_000_000, - ppt_divisor: 1_000_000, - ppt_step: 1, - clock_min: MinMax { - min: 400, - max: 1600, - }, - clock_max: MinMax { - min: 400, - max: 1600, - }, - clock_step: 100, - skip_resume_reclock: false, - } - } -} - -fn oc_limits_filepath() -> std::path::PathBuf { - crate::utility::settings_dir().join(OC_LIMITS_FILEPATH) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[cfg(not(feature = "dev_stuff"))] // this can fail due to reading from incompletely-written file otherwise - #[test] - fn load_pt_oc() { - let mut file = std::fs::File::open("../pt_oc.json").unwrap(); - let settings: OverclockLimits = serde_json::from_reader(&mut file).unwrap(); - assert!(settings.cpus.cpus.len() == 8); - } - - #[cfg(feature = "dev_stuff")] - #[test] - fn emit_default_pt_oc() { - let mut file = std::fs::File::create("../pt_oc.json").unwrap(); - serde_json::to_writer_pretty(&mut file, &OverclockLimits::default()).unwrap(); - } -} diff --git a/backend/src/settings/steam_deck/util.rs b/backend/src/settings/steam_deck/util.rs index 52eb3e9..029e2f3 100644 --- a/backend/src/settings/steam_deck/util.rs +++ b/backend/src/settings/steam_deck/util.rs @@ -12,6 +12,14 @@ pub const JUPITER_HWMON_NAME: &'static str = "jupiter"; pub const STEAMDECK_HWMON_NAME: &'static str = "steamdeck_hwmon"; pub const GPU_HWMON_NAME: &'static str = "amdgpu"; +pub fn range_min_or_fallback(range: &Option>, fallback: I) -> I { + range.and_then(|lim| lim.min).unwrap_or(fallback) +} + +pub fn range_max_or_fallback(range: &Option>, fallback: I) -> I { + range.and_then(|lim| lim.max).unwrap_or(fallback) +} + pub fn card_also_has(card: &dyn sysfuss::SysEntity, extensions: &'static [&'static str]) -> bool { extensions.iter() .all(|ext| card.as_ref().join(ext).exists()) diff --git a/backend/src/settings/traits.rs b/backend/src/settings/traits.rs index 4e96882..990984d 100644 --- a/backend/src/settings/traits.rs +++ b/backend/src/settings/traits.rs @@ -40,6 +40,12 @@ pub trait OnPowerEvent { } } +pub trait ProviderBuilder { + fn from_json_and_limits(persistent: J, version: u64, limits: L) -> Self; + + fn from_limits(limits: L) -> Self; +} + pub trait TGpu: OnSet + OnResume + OnPowerEvent + Debug + Send { fn limits(&self) -> crate::api::GpuLimits; diff --git a/backend/src/settings/unknown/battery.rs b/backend/src/settings/unknown/battery.rs index ab76959..6c1a2e3 100644 --- a/backend/src/settings/unknown/battery.rs +++ b/backend/src/settings/unknown/battery.rs @@ -1,12 +1,21 @@ use std::convert::Into; +use limits_core::json_v2::GenericBatteryLimit; + use crate::persist::BatteryJson; -use crate::settings::TBattery; +use crate::settings::{TBattery, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError}; #[derive(Debug, Clone)] pub struct Battery; +impl Battery { + #[inline] + fn system_default() -> Self { + Battery + } +} + impl Into for Battery { #[inline] fn into(self) -> BatteryJson { @@ -19,6 +28,16 @@ impl Into for Battery { } } +impl ProviderBuilder for Battery { + fn from_json_and_limits(_persistent: BatteryJson, _version: u64, _limits: GenericBatteryLimit) -> Self { + Battery::system_default() + } + + fn from_limits(_limits: GenericBatteryLimit) -> Self { + Battery::system_default() + } +} + impl OnSet for Battery { fn on_set(&mut self) -> Result<(), Vec> { Ok(()) diff --git a/backend/src/settings/unknown/cpu.rs b/backend/src/settings/unknown/cpu.rs index c3985cf..25130ca 100644 --- a/backend/src/settings/unknown/cpu.rs +++ b/backend/src/settings/unknown/cpu.rs @@ -1,9 +1,11 @@ use std::convert::Into; +use limits_core::json_v2::GenericCpusLimit; + use crate::persist::CpuJson; use crate::settings::MinMax; use crate::settings::{OnResume, OnSet, SettingError}; -use crate::settings::{TCpu, TCpus}; +use crate::settings::{TCpu, TCpus, ProviderBuilder}; const CPU_PRESENT_PATH: &str = "/sys/devices/system/cpu/present"; const CPU_SMT_PATH: &str = "/sys/devices/system/cpu/smt/control"; @@ -111,7 +113,7 @@ impl Cpus { } } - #[inline] + /*#[inline] pub fn from_json(mut other: Vec, version: u64) -> Self { let (_, can_smt) = Self::system_smt_capabilities(); let mut result = Vec::with_capacity(other.len()); @@ -140,6 +142,42 @@ impl Cpus { smt: smt_guess, smt_capable: can_smt, } + }*/ +} + +impl ProviderBuilder, GenericCpusLimit> for Cpus { + fn from_json_and_limits(mut persistent: Vec, version: u64, _limits: GenericCpusLimit) -> Self { + let (_, can_smt) = Self::system_smt_capabilities(); + let mut result = Vec::with_capacity(persistent.len()); + let max_cpus = Self::cpu_count(); + let smt_guess = crate::settings::util::guess_smt(&persistent) && can_smt; + for (i, cpu) in persistent.drain(..).enumerate() { + // prevent having more CPUs than available + if let Some(max_cpus) = max_cpus { + if i == max_cpus { + break; + } + } + let new_cpu = Cpu::from_json(cpu, version, i); + result.push(new_cpu); + } + if let Some(max_cpus) = max_cpus { + if result.len() != max_cpus { + let mut sys_cpus = Cpus::system_default(); + for i in result.len()..sys_cpus.cpus.len() { + result.push(sys_cpus.cpus.remove(i)); + } + } + } + Self { + cpus: result, + smt: smt_guess, + smt_capable: can_smt, + } + } + + fn from_limits(_limits: GenericCpusLimit) -> Self { + Self::system_default() } } @@ -153,7 +191,7 @@ impl TCpus for Cpus { } } - fn json(&self) -> Vec { + fn json(&self) -> Vec { self.cpus.iter().map(|x| x.to_owned().into()).collect() } diff --git a/backend/src/settings/unknown/gpu.rs b/backend/src/settings/unknown/gpu.rs index 0b4b73e..4e911d4 100644 --- a/backend/src/settings/unknown/gpu.rs +++ b/backend/src/settings/unknown/gpu.rs @@ -1,8 +1,10 @@ use std::convert::Into; +use limits_core::json_v2::GenericGpuLimit; + use crate::persist::GpuJson; use crate::settings::MinMax; -use crate::settings::TGpu; +use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError}; #[derive(Debug, Clone)] @@ -11,16 +13,21 @@ pub struct Gpu { } impl Gpu { - #[inline] - pub fn from_json(_other: GpuJson, _version: u64) -> Self { - Self { slow_memory: false } - } - pub fn system_default() -> Self { Self { slow_memory: false } } } +impl ProviderBuilder for Gpu { + fn from_json_and_limits(_persistent: GpuJson, _version: u64, _limits: GenericGpuLimit) -> Self { + Self::system_default() + } + + fn from_limits(_limits: GenericGpuLimit) -> Self { + Self::system_default() + } +} + impl Into for Gpu { #[inline] fn into(self) -> GpuJson { diff --git a/backend/src/settings/unknown/mod.rs b/backend/src/settings/unknown/mod.rs index 2039cae..bd0f419 100644 --- a/backend/src/settings/unknown/mod.rs +++ b/backend/src/settings/unknown/mod.rs @@ -5,3 +5,11 @@ mod gpu; pub use battery::Battery; pub use cpu::{Cpu, Cpus}; pub use gpu::Gpu; + +fn _impl_checker() { + fn impl_provider_builder, J, L>() {} + + impl_provider_builder::(); + impl_provider_builder::, limits_core::json_v2::GenericCpusLimit>(); + impl_provider_builder::(); +} From 3aa9680baedf850d7d576d0df28ce50cf2124ebe Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 18 Nov 2023 15:17:56 -0500 Subject: [PATCH 08/56] Add multi-profile per-game functionality for #82, change to JSON to RON format --- backend/Cargo.lock | 26 ++++- backend/Cargo.toml | 1 + backend/src/api/general.rs | 24 ++++- backend/src/api/handler.rs | 26 ++--- backend/src/consts.rs | 1 + backend/src/main.rs | 12 ++- backend/src/persist/error.rs | 35 ++++++- backend/src/persist/file.rs | 62 +++++++++++ backend/src/persist/general.rs | 50 +-------- backend/src/persist/mod.rs | 6 +- backend/src/settings/detect/auto_detect.rs | 13 ++- backend/src/settings/detect/limits_worker.rs | 6 +- backend/src/settings/driver.rs | 10 +- backend/src/settings/general.rs | 102 +++++++++---------- backend/src/settings/traits.rs | 10 +- backend/src/utility.rs | 17 ++-- src/backend.ts | 9 +- src/index.tsx | 2 +- 18 files changed, 253 insertions(+), 159 deletions(-) create mode 100644 backend/src/persist/file.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 04fb53e..bea6ffb 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -153,7 +153,7 @@ version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "lazy_static", @@ -175,6 +175,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -599,7 +608,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64 0.13.1", - "bitflags", + "bitflags 1.3.2", "bytes", "headers-core", "http", @@ -1059,6 +1068,7 @@ dependencies = [ "limits_core", "log", "regex", + "ron", "serde", "serde_json", "simplelog", @@ -1161,6 +1171,18 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.2", + "bitflags 2.4.1", + "serde", + "serde_derive", +] + [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 382f3de..3fa7e57 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -15,6 +15,7 @@ readme = "../README.md" usdpl-back = { version = "0.10.1", features = ["blocking"] }#, path = "../../usdpl-rs/usdpl-back"} serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +ron = "0.8" sysfuss = { version = "0.2", features = ["derive"] }#,path = "../../sysfs-nav"} # async diff --git a/backend/src/api/general.rs b/backend/src/api/general.rs index 9cda1c2..73cb706 100644 --- a/backend/src/api/general.rs +++ b/backend/src/api/general.rs @@ -55,18 +55,34 @@ pub fn load_settings( sender: Sender, ) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety - let setter = move |path: u64, name: String| { + let setter = move |id: u64, name: String, variant: u64, variant_name: Option| { sender .lock() .unwrap() - .send(ApiMessage::LoadSettings(path, name)) + .send(ApiMessage::LoadSettings(id, name, variant, variant_name.unwrap_or_else(|| crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned()))) .expect("load_settings send failed") }; move |params_in: super::ApiParameterType| { if let Some(Primitive::String(id)) = params_in.get(0) { if let Some(Primitive::String(name)) = params_in.get(1) { - setter(id.parse().unwrap_or_default(), name.to_owned()); - vec![true.into()] + if let Some(Primitive::F64(variant_id)) = params_in.get(2) { + if let Some(Primitive::String(variant_name)) = params_in.get(3) { + setter(id.parse().unwrap_or_default(), + name.to_owned(), + *variant_id as _, + Some(variant_name.to_owned())); + vec![true.into()] + } else { + setter(id.parse().unwrap_or_default(), + name.to_owned(), + *variant_id as _, + None); + vec![true.into()] + } + } else { + log::warn!("load_settings missing variant id parameter"); + vec!["load_settings missing variant id parameter".into()] + } } else { log::warn!("load_settings missing name parameter"); vec!["load_settings missing name parameter".into()] diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index 60316c9..56b2177 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -5,7 +5,6 @@ use crate::persist::SettingsJson; use crate::settings::{ MinMax, OnPowerEvent, OnResume, OnSet, PowerMode, Settings, TBattery, TCpus, TGeneral, TGpu, }; -use crate::utility::unwrap_maybe_fatal; type Callback = Box; @@ -23,7 +22,7 @@ pub enum ApiMessage { OnChargeChange(f64), // battery fill amount: 0 = empty, 1 = full PowerVibeCheck, WaitForEmptyQueue(Callback<()>), - LoadSettings(u64, String), // (path, name) + LoadSettings(u64, String, u64, String), // (path, name, variant, variant name) LoadMainSettings, LoadSystemSettings, GetLimits(Callback), @@ -287,21 +286,14 @@ impl ApiMessageHandler { log::debug!("api_worker is saving..."); let is_persistent = *settings.general.persistent(); let save_path = - crate::utility::settings_dir().join(settings.general.get_path().clone()); + crate::utility::settings_dir().join(settings.general.get_path()); if is_persistent { let settings_clone = settings.json(); let save_json: SettingsJson = settings_clone.into(); - unwrap_maybe_fatal(save_json.save(&save_path), "Failed to save settings"); - if let Some(event) = &settings.general.on_event().on_save { - if !event.is_empty() { - unwrap_maybe_fatal( - std::process::Command::new("/bin/bash") - .args(&["-c", event]) - .spawn(), - "Failed to start on_save event command", - ); - } + if let Err(e) = crate::persist::FileJson::update_variant_or_create(&save_path, save_json, settings.general.get_name().to_owned()) { + log::error!("Failed to create/update settings file {}: {}", save_path.display(), e); } + //unwrap_maybe_fatal(save_json.save(&save_path), "Failed to save settings"); log::debug!("Saved settings to {}", save_path.display()); if let Err(e) = crate::utility::chown_settings_dir() { log::error!("Failed to change config dir permissions: {}", e); @@ -375,9 +367,9 @@ impl ApiMessageHandler { self.on_empty.push(callback); false } - ApiMessage::LoadSettings(id, name) => { + ApiMessage::LoadSettings(id, name, variant_id, variant_name) => { let path = format!("{}.json", id); - match settings.load_file(path.into(), name, false) { + match settings.load_file(path.into(), name, variant_id, variant_name, false) { Ok(success) => log::info!("Loaded settings file? {}", success), Err(e) => log::warn!("Load file err: {}", e), } @@ -387,6 +379,8 @@ impl ApiMessageHandler { match settings.load_file( crate::consts::DEFAULT_SETTINGS_FILE.into(), crate::consts::DEFAULT_SETTINGS_NAME.to_owned(), + 0, + crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(), true, ) { Ok(success) => log::info!("Loaded main settings file? {}", success), @@ -395,7 +389,7 @@ impl ApiMessageHandler { true } ApiMessage::LoadSystemSettings => { - settings.load_system_default(settings.general.get_name().to_owned()); + settings.load_system_default(settings.general.get_name().to_owned(), settings.general.get_variant_id(), settings.general.get_variant_name().to_owned()); true } ApiMessage::GetLimits(cb) => { diff --git a/backend/src/consts.rs b/backend/src/consts.rs index 7b6eb5b..ec650fe 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -5,6 +5,7 @@ pub const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION"); pub const DEFAULT_SETTINGS_FILE: &str = "default_settings.json"; pub const DEFAULT_SETTINGS_NAME: &str = "Main"; +pub const DEFAULT_SETTINGS_VARIANT_NAME: &str = "Primary"; pub const LIMITS_FILE: &str = "limits_cache.json"; pub const LIMITS_OVERRIDE_FILE: &str = "limits_override.json"; diff --git a/backend/src/main.rs b/backend/src/main.rs index 6eb6d99..0c94237 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -75,12 +75,20 @@ fn main() -> Result<(), ()> { let _limits_handle = crate::settings::limits_worker_spawn(); let mut loaded_settings = - persist::SettingsJson::open(utility::settings_dir().join(DEFAULT_SETTINGS_FILE)) - .map(|settings| settings::Settings::from_json(settings, DEFAULT_SETTINGS_FILE.into())) + persist::FileJson::open(utility::settings_dir().join(DEFAULT_SETTINGS_FILE)) + .map(|mut file| file.variants.remove("0") + .map(|settings| settings::Settings::from_json(DEFAULT_SETTINGS_NAME.into(), settings, DEFAULT_SETTINGS_FILE.into())) + .unwrap_or_else(|| settings::Settings::system_default( + DEFAULT_SETTINGS_FILE.into(), + DEFAULT_SETTINGS_NAME.into(), + 0, + DEFAULT_SETTINGS_VARIANT_NAME.into()))) .unwrap_or_else(|_| { settings::Settings::system_default( DEFAULT_SETTINGS_FILE.into(), DEFAULT_SETTINGS_NAME.into(), + 0, + DEFAULT_SETTINGS_VARIANT_NAME.into(), ) }); diff --git a/backend/src/persist/error.rs b/backend/src/persist/error.rs index 2dcb6fa..502d199 100644 --- a/backend/src/persist/error.rs +++ b/backend/src/persist/error.rs @@ -1,10 +1,10 @@ #[derive(Debug)] -pub enum JsonError { - Serde(serde_json::Error), +pub enum SerdeError { + Serde(RonError), Io(std::io::Error), } -impl std::fmt::Display for JsonError { +impl std::fmt::Display for SerdeError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Serde(e) => (e as &dyn std::fmt::Display).fmt(f), @@ -12,3 +12,32 @@ impl std::fmt::Display for JsonError { } } } + +impl std::error::Error for SerdeError {} + +#[derive(Debug)] +pub enum RonError { + General(ron::error::Error), + Spanned(ron::error::SpannedError), +} + +impl std::fmt::Display for RonError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::General(e) => (e as &dyn std::fmt::Display).fmt(f), + Self::Spanned(e) => (e as &dyn std::fmt::Display).fmt(f), + } + } +} + +impl From for RonError { + fn from(value: ron::error::Error) -> Self { + Self::General(value) + } +} + +impl From for RonError { + fn from(value: ron::error::SpannedError) -> Self { + Self::Spanned(value) + } +} diff --git a/backend/src/persist/file.rs b/backend/src/persist/file.rs new file mode 100644 index 0000000..be575f4 --- /dev/null +++ b/backend/src/persist/file.rs @@ -0,0 +1,62 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::SerdeError; +use super::SettingsJson; + +#[derive(Serialize, Deserialize)] +pub struct FileJson { + pub version: u64, + pub name: String, + pub variants: HashMap, +} + +impl FileJson { + pub fn save>(&self, path: P) -> Result<(), SerdeError> { + let path = path.as_ref(); + + if !self.variants.is_empty() { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(SerdeError::Io)?; + } + let mut file = std::fs::File::create(path).map_err(SerdeError::Io)?; + ron::ser::to_writer_pretty(&mut file, &self, crate::utility::ron_pretty_config()).map_err(|e| SerdeError::Serde(e.into())) + } else { + if path.exists() { + // remove settings file when persistence is turned off, to prevent it from be loaded next time. + std::fs::remove_file(path).map_err(SerdeError::Io) + } else { + Ok(()) + } + } + } + + pub fn open>(path: P) -> Result { + let mut file = std::fs::File::open(path).map_err(SerdeError::Io)?; + ron::de::from_reader(&mut file).map_err(|e| SerdeError::Serde(e.into())) + } + + pub fn update_variant_or_create>(path: P, setting: SettingsJson, given_name: String) -> Result<(), SerdeError> { + if !setting.persistent { + return Ok(()) + } + let path = path.as_ref(); + + let file = if path.exists() { + let mut file = Self::open(path)?; + file.variants.insert(setting.variant.to_string(), setting); + file + } else { + let mut setting_variants = HashMap::with_capacity(1); + setting_variants.insert(setting.variant.to_string(), setting); + Self { + version: 0, + name: given_name, + variants: setting_variants, + } + }; + + file.save(path) + } +} diff --git a/backend/src/persist/general.rs b/backend/src/persist/general.rs index 58497ec..9809403 100644 --- a/backend/src/persist/general.rs +++ b/backend/src/persist/general.rs @@ -2,38 +2,18 @@ use std::default::Default; use serde::{Deserialize, Serialize}; -use super::JsonError; use super::{BatteryJson, CpuJson, DriverJson, GpuJson}; -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct OnEventJson { - pub on_save: Option, - pub on_load: Option, - pub on_set: Option, - pub on_resume: Option, -} - -impl Default for OnEventJson { - fn default() -> Self { - Self { - on_save: None, - on_load: None, - on_set: None, - on_resume: None, - } - } -} - #[derive(Serialize, Deserialize)] pub struct SettingsJson { pub version: u64, pub name: String, + pub variant: u64, pub persistent: bool, pub cpus: Vec, pub gpu: GpuJson, pub battery: BatteryJson, pub provider: Option, - pub events: Option, } impl Default for SettingsJson { @@ -41,42 +21,16 @@ impl Default for SettingsJson { Self { version: 0, name: crate::consts::DEFAULT_SETTINGS_NAME.to_owned(), + variant: 0, persistent: false, cpus: Vec::with_capacity(8), gpu: GpuJson::default(), battery: BatteryJson::default(), provider: None, - events: None, } } } -impl SettingsJson { - pub fn save>(&self, path: P) -> Result<(), JsonError> { - let path = path.as_ref(); - - if self.persistent { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(JsonError::Io)?; - } - let mut file = std::fs::File::create(path).map_err(JsonError::Io)?; - serde_json::to_writer_pretty(&mut file, &self).map_err(JsonError::Serde) - } else { - if path.exists() { - // remove settings file when persistence is turned off, to prevent it from be loaded next time. - std::fs::remove_file(path).map_err(JsonError::Io) - } else { - Ok(()) - } - } - } - - pub fn open>(path: P) -> Result { - let mut file = std::fs::File::open(path).map_err(JsonError::Io)?; - serde_json::from_reader(&mut file).map_err(JsonError::Serde) - } -} - #[derive(Serialize, Deserialize, Clone)] pub struct MinMaxJson { pub max: Option, diff --git a/backend/src/persist/mod.rs b/backend/src/persist/mod.rs index 17dfa35..4c9a31b 100644 --- a/backend/src/persist/mod.rs +++ b/backend/src/persist/mod.rs @@ -2,13 +2,15 @@ mod battery; mod cpu; mod driver; mod error; +mod file; mod general; mod gpu; pub use battery::{BatteryEventJson, BatteryJson}; pub use cpu::CpuJson; pub use driver::DriverJson; -pub use general::{MinMaxJson, OnEventJson, SettingsJson}; +pub use file::FileJson; +pub use general::{MinMaxJson, SettingsJson}; pub use gpu::GpuJson; -pub use error::JsonError; +pub use error::{SerdeError, RonError}; diff --git a/backend/src/settings/detect/auto_detect.rs b/backend/src/settings/detect/auto_detect.rs index 1d46c44..e7b8ded 100644 --- a/backend/src/settings/detect/auto_detect.rs +++ b/backend/src/settings/detect/auto_detect.rs @@ -10,7 +10,7 @@ use crate::settings::{Driver, General, TBattery, TCpus, TGeneral, TGpu, Provider fn get_limits() -> limits_core::json_v2::Base { let limits_path = super::utility::limits_path(); match File::open(&limits_path) { - Ok(f) => match serde_json::from_reader(f) { + Ok(f) => match ron::de::from_reader(f) { Ok(lim) => lim, Err(e) => { log::warn!( @@ -35,7 +35,7 @@ fn get_limits() -> limits_core::json_v2::Base { fn get_limits_overrides() -> Option { let limits_override_path = super::utility::limits_override_path(); match File::open(&limits_override_path) { - Ok(f) => match serde_json::from_reader(f) { + Ok(f) => match ron::de::from_reader(f) { Ok(lim) => Some(lim), Err(e) => { log::warn!( @@ -63,6 +63,8 @@ pub fn auto_detect_provider() -> DriverJson { None, crate::utility::settings_dir().join("autodetect.json"), "".to_owned(), + 0, + crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(), ) .battery .provider(); @@ -72,16 +74,19 @@ pub fn auto_detect_provider() -> DriverJson { /// Device detection logic pub fn auto_detect0( - settings_opt: Option, + settings_opt: Option<&SettingsJson>, json_path: std::path::PathBuf, name: String, + variant_id: u64, + variant_name: String, ) -> Driver { let mut general_driver = Box::new(General { persistent: false, path: json_path, name, + variant_id, + variant_name, driver: DriverJson::AutoDetect, - events: Default::default(), }); let cpu_info: String = usdpl_back::api::files::read_single("/proc/cpuinfo").unwrap_or_default(); diff --git a/backend/src/settings/detect/limits_worker.rs b/backend/src/settings/detect/limits_worker.rs index c3dbd0e..ad10f56 100644 --- a/backend/src/settings/detect/limits_worker.rs +++ b/backend/src/settings/detect/limits_worker.rs @@ -15,7 +15,7 @@ pub fn spawn() -> JoinHandle<()> { // try to load limits from file, fallback to built-in default let base = if limits_path.exists() { match std::fs::File::open(&limits_path) { - Ok(f) => match serde_json::from_reader(f) { + Ok(f) => match ron::de::from_reader(f) { Ok(b) => b, Err(e) => { log::error!("Cannot parse {}: {}", limits_path.display(), e); @@ -72,7 +72,7 @@ pub fn get_limits_cached() -> Base { let limits_path = super::utility::limits_path(); if limits_path.is_file() { match std::fs::File::open(&limits_path) { - Ok(f) => match serde_json::from_reader(f) { + Ok(f) => match ron::de::from_reader(f) { Ok(b) => b, Err(e) => { log::error!("Cannot parse {}: {}", limits_path.display(), e); @@ -93,7 +93,7 @@ pub fn get_limits_cached() -> Base { fn save_base(new_base: &Base, path: impl AsRef) { let limits_path = path.as_ref(); match std::fs::File::create(&limits_path) { - Ok(f) => match serde_json::to_writer_pretty(f, &new_base) { + Ok(f) => match ron::ser::to_writer_pretty(f, &new_base, crate::utility::ron_pretty_config()) { Ok(_) => log::info!("Successfully saved new limits to {}", limits_path.display()), Err(e) => log::error!( "Failed to save limits json to file `{}`: {}", diff --git a/backend/src/settings/driver.rs b/backend/src/settings/driver.rs index 90a3b7f..1aa005e 100644 --- a/backend/src/settings/driver.rs +++ b/backend/src/settings/driver.rs @@ -10,15 +10,17 @@ pub struct Driver { impl Driver { pub fn init( - settings: SettingsJson, + name: String, + settings: &SettingsJson, json_path: std::path::PathBuf, ) -> Self { let name_bup = settings.name.clone(); - auto_detect0(Some(settings), json_path, name_bup) + let id_bup = settings.variant; + auto_detect0(Some(settings), json_path, name, id_bup, name_bup) } - pub fn system_default(json_path: std::path::PathBuf, name: String) -> Self { - auto_detect0(None, json_path, name) + pub fn system_default(json_path: std::path::PathBuf, name: String, variant_id: u64, variant_name: String) -> Self { + auto_detect0(None, json_path, name, variant_id, variant_name) } } diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index a4b7844..a2f729f 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; //use super::{Battery, Cpus, Gpu}; use super::{OnResume, OnSet, SettingError}; use super::{TBattery, TCpus, TGeneral, TGpu}; -use crate::persist::SettingsJson; +use crate::persist::{SettingsJson, FileJson}; //use crate::utility::unwrap_lock; const LATEST_VERSION: u64 = 0; @@ -33,44 +33,19 @@ pub struct General { pub persistent: bool, pub path: PathBuf, pub name: String, + pub variant_id: u64, + pub variant_name: String, pub driver: crate::persist::DriverJson, - pub events: crate::persist::OnEventJson, } impl OnSet for General { fn on_set(&mut self) -> Result<(), Vec> { - if let Some(event) = &self.events.on_set { - if !event.is_empty() { - std::process::Command::new("/bin/bash") - .args(&["-c", event]) - .spawn() - .map_err(|e| { - vec![SettingError { - msg: format!("on_set event command error: {}", e), - setting: SettingVariant::General, - }] - })?; - } - } Ok(()) } } impl OnResume for General { fn on_resume(&self) -> Result<(), Vec> { - if let Some(event) = &self.events.on_resume { - if !event.is_empty() { - std::process::Command::new("/bin/bash") - .args(&["-c", event]) - .spawn() - .map_err(|e| { - vec![SettingError { - msg: format!("on_resume event command error: {}", e), - setting: SettingVariant::General, - }] - })?; - } - } Ok(()) } } @@ -106,12 +81,24 @@ impl TGeneral for General { self.name = name; } - fn provider(&self) -> crate::persist::DriverJson { - self.driver.clone() + fn get_variant_id(&self) -> u64 { + self.variant_id } - fn on_event(&self) -> &crate::persist::OnEventJson { - &self.events + fn variant_id(&mut self, id: u64) { + self.variant_id = id; + } + + fn get_variant_name(&self) -> &'_ str { + &self.variant_name + } + + fn variant_name(&mut self, name: String) { + self.variant_name = name; + } + + fn provider(&self) -> crate::persist::DriverJson { + self.driver.clone() } } @@ -155,8 +142,8 @@ impl OnSet for Settings { impl Settings { #[inline] - pub fn from_json(other: SettingsJson, json_path: PathBuf) -> Self { - let x = super::Driver::init(other, json_path.clone()); + pub fn from_json(name: String, other: SettingsJson, json_path: PathBuf) -> Self { + let x = super::Driver::init(name, &other, json_path.clone()); log::info!( "Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), @@ -172,8 +159,8 @@ impl Settings { } } - pub fn system_default(json_path: PathBuf, name: String) -> Self { - let driver = super::Driver::system_default(json_path, name); + pub fn system_default(json_path: PathBuf, name: String, variant_id: u64, variant_name: String) -> Self { + let driver = super::Driver::system_default(json_path, name, variant_id, variant_name); Self { general: driver.general, cpus: driver.cpus, @@ -182,26 +169,40 @@ impl Settings { } } - pub fn load_system_default(&mut self, name: String) { - let driver = super::Driver::system_default(self.general.get_path().to_owned(), name); + pub fn load_system_default(&mut self, name: String, variant_id: u64, variant_name: String) { + let driver = super::Driver::system_default(self.general.get_path().to_owned(), name, variant_id, variant_name); self.cpus = driver.cpus; self.gpu = driver.gpu; self.battery = driver.battery; self.general = driver.general; } + pub fn get_variant<'a>(settings_file: &'a FileJson, variant_id: u64, variant_name: String) -> Result<&'a SettingsJson, SettingError> { + if let Some(variant) = settings_file.variants.get(&variant_id.to_string()) { + Ok(variant) + } else { + Err(SettingError { + msg: format!("Cannot get non-existent variant `{}` (id:{})", variant_name, variant_id), + setting: SettingVariant::General, + }) + } + } + pub fn load_file( &mut self, filename: PathBuf, name: String, + variant: u64, + variant_name: String, system_defaults: bool, ) -> Result { let json_path = crate::utility::settings_dir().join(&filename); if json_path.exists() { - let settings_json = SettingsJson::open(&json_path).map_err(|e| SettingError { - msg: e.to_string(), + let file_json = FileJson::open(&json_path).map_err(|e| SettingError { + msg: format!("Failed to open settings {}: {}", json_path.display(), e), setting: SettingVariant::General, })?; + let settings_json = Self::get_variant(&file_json, variant, variant_name)?; if !settings_json.persistent { log::warn!( "Loaded persistent config `{}` ({}) with persistent=false", @@ -211,7 +212,7 @@ impl Settings { *self.general.persistent() = false; self.general.name(name); } else { - let x = super::Driver::init(settings_json, json_path.clone()); + let x = super::Driver::init(name, settings_json, json_path.clone()); log::info!("Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), x.cpus.provider(), x.gpu.provider(), x.battery.provider()); self.general = x.general; self.cpus = x.cpus; @@ -220,24 +221,15 @@ impl Settings { } } else { if system_defaults { - self.load_system_default(name); + self.load_system_default(name, variant, variant_name); } else { self.general.name(name); + self.general.variant_name(variant_name); } *self.general.persistent() = false; } self.general.path(filename); - if let Some(event) = &self.general.on_event().on_load { - if !event.is_empty() { - std::process::Command::new("/bin/bash") - .args(&["-c", event]) - .spawn() - .map_err(|e| SettingError { - msg: format!("on_save event command error: {}", e), - setting: SettingVariant::General, - })?; - } - } + self.general.variant_id(variant); Ok(*self.general.persistent()) } @@ -275,13 +267,13 @@ impl Settings { pub fn json(&self) -> SettingsJson { SettingsJson { version: LATEST_VERSION, - name: self.general.get_name().to_owned(), + name: self.general.get_variant_name().to_owned(), + variant: self.general.get_variant_id(), persistent: self.general.get_persistent(), cpus: self.cpus.json(), gpu: self.gpu.json(), battery: self.battery.json(), provider: Some(self.general.provider()), - events: Some(self.general.on_event().clone()), } } } diff --git a/backend/src/settings/traits.rs b/backend/src/settings/traits.rs index 990984d..8d93c55 100644 --- a/backend/src/settings/traits.rs +++ b/backend/src/settings/traits.rs @@ -109,9 +109,15 @@ pub trait TGeneral: OnSet + OnResume + OnPowerEvent + Debug + Send { fn name(&mut self, name: String); - fn provider(&self) -> crate::persist::DriverJson; + fn get_variant_id(&self) -> u64; - fn on_event(&self) -> &'_ crate::persist::OnEventJson; + fn variant_id(&mut self, id: u64); + + fn get_variant_name(&self) -> &'_ str; + + fn variant_name(&mut self, name: String); + + fn provider(&self) -> crate::persist::DriverJson; } pub trait TBattery: OnSet + OnResume + OnPowerEvent + Debug + Send { diff --git a/backend/src/utility.rs b/backend/src/utility.rs index 742cd32..62d6909 100644 --- a/backend/src/utility.rs +++ b/backend/src/utility.rs @@ -1,19 +1,8 @@ -use std::fmt::Display; //use std::sync::{LockResult, MutexGuard}; //use std::fs::{Permissions, metadata}; use std::io::{Read, Write}; use std::os::unix::fs::PermissionsExt; -pub fn unwrap_maybe_fatal(result: Result, message: &str) -> T { - match result { - Ok(x) => x, - Err(e) => { - log::error!("{}: {}", message, e); - panic!("{}: {}", message, e); - } - } -} - /*pub fn unwrap_lock<'a, T: Sized>( result: LockResult>, lock_name: &str, @@ -27,6 +16,12 @@ pub fn unwrap_maybe_fatal(result: Result, message: & } }*/ +pub fn ron_pretty_config() -> ron::ser::PrettyConfig { + ron::ser::PrettyConfig::default() + .struct_names(true) + .compact_arrays(true) +} + pub fn settings_dir() -> std::path::PathBuf { usdpl_back::api::dirs::home() .unwrap_or_else(|| "/tmp/".into()) diff --git a/src/backend.ts b/src/backend.ts index 07fe9d8..1d8d1f0 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -247,8 +247,13 @@ export async function getGeneralPersistent(): Promise { return (await call_backend("GENERAL_get_persistent", []))[0]; } -export async function loadGeneralSettings(id: string, name: string): Promise { - return (await call_backend("GENERAL_load_settings", [id, name]))[0]; +export async function loadGeneralSettings(id: string, name: string, variant_id: number, variant_name: string | undefined): Promise { + if (variant_name) { + return (await call_backend("GENERAL_load_settings", [id, name, variant_id, variant_name]))[0]; + } else { + return (await call_backend("GENERAL_load_settings", [id, name, variant_id]))[0]; + } + } export async function loadGeneralDefaultSettings(): Promise { diff --git a/src/index.tsx b/src/index.tsx index df1407d..2412a10 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -191,7 +191,7 @@ const reload = function() { backend.log(backend.LogLevel.Info, "RegisterForGameActionStart callback(" + actionType + ", " + id + ")"); // don't use gameInfo.appid, haha backend.resolve( - backend.loadGeneralSettings(id.toString(), gameInfo.display_name), + backend.loadGeneralSettings(id.toString(), gameInfo.display_name, 0, undefined), (ok: boolean) => {backend.log(backend.LogLevel.Debug, "Loading settings ok? " + ok)} ); }); From 396a52da5edaabfd5a24a7c22e38242d6f0ba258 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 18 Nov 2023 15:31:45 -0500 Subject: [PATCH 09/56] Use decky-provided settings directory --- backend/Cargo.toml | 2 +- backend/src/utility.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 3fa7e57..7fd5273 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -12,7 +12,7 @@ readme = "../README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -usdpl-back = { version = "0.10.1", features = ["blocking"] }#, path = "../../usdpl-rs/usdpl-back"} +usdpl-back = { version = "0.10.1", features = ["blocking", "decky"] }#, path = "../../usdpl-rs/usdpl-back"} serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" ron = "0.8" diff --git a/backend/src/utility.rs b/backend/src/utility.rs index 62d6909..00dae0b 100644 --- a/backend/src/utility.rs +++ b/backend/src/utility.rs @@ -22,12 +22,19 @@ pub fn ron_pretty_config() -> ron::ser::PrettyConfig { .compact_arrays(true) } -pub fn settings_dir() -> std::path::PathBuf { +#[allow(dead_code)] +pub fn settings_dir_old() -> std::path::PathBuf { usdpl_back::api::dirs::home() .unwrap_or_else(|| "/tmp/".into()) .join(".config/powertools/") } +pub fn settings_dir() -> std::path::PathBuf { + usdpl_back::api::decky::settings_dir() + .unwrap_or_else(|_| "/tmp/".to_owned()) + .into() +} + pub fn chown_settings_dir() -> std::io::Result<()> { let dir = settings_dir(); #[cfg(feature = "decky")] From e0ddee9e074c3f82be7e0a3de4d633ac7494d6b8 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 18 Nov 2023 15:55:17 -0500 Subject: [PATCH 10/56] Update .json to .ron in various places, fix tests --- backend/src/api/handler.rs | 2 +- backend/src/consts.rs | 6 +- backend/src/settings/detect/utility.rs | 2 +- backend/src/settings/mod.rs | 2 +- backend/src/settings/util.rs | 1 + backend/src/utility.rs | 23 ++++ limits_override.ron | 142 +++++++++++++++++++++++++ 7 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 limits_override.ron diff --git a/backend/src/api/handler.rs b/backend/src/api/handler.rs index 56b2177..69234c6 100644 --- a/backend/src/api/handler.rs +++ b/backend/src/api/handler.rs @@ -368,7 +368,7 @@ impl ApiMessageHandler { false } ApiMessage::LoadSettings(id, name, variant_id, variant_name) => { - let path = format!("{}.json", id); + let path = format!("{}.ron", id); match settings.load_file(path.into(), name, variant_id, variant_name, false) { Ok(success) => log::info!("Loaded settings file? {}", success), Err(e) => log::warn!("Load file err: {}", e), diff --git a/backend/src/consts.rs b/backend/src/consts.rs index ec650fe..531ce20 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -3,12 +3,12 @@ pub const PORT: u16 = 44443; pub const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME"); pub const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION"); -pub const DEFAULT_SETTINGS_FILE: &str = "default_settings.json"; +pub const DEFAULT_SETTINGS_FILE: &str = "default_settings.ron"; pub const DEFAULT_SETTINGS_NAME: &str = "Main"; pub const DEFAULT_SETTINGS_VARIANT_NAME: &str = "Primary"; -pub const LIMITS_FILE: &str = "limits_cache.json"; -pub const LIMITS_OVERRIDE_FILE: &str = "limits_override.json"; +pub const LIMITS_FILE: &str = "limits_cache.ron"; +pub const LIMITS_OVERRIDE_FILE: &str = "limits_override.ron"; pub const MESSAGE_SEEN_ID_FILE: &str = "seen_message.bin"; diff --git a/backend/src/settings/detect/utility.rs b/backend/src/settings/detect/utility.rs index 7d42be0..7f14b5f 100644 --- a/backend/src/settings/detect/utility.rs +++ b/backend/src/settings/detect/utility.rs @@ -12,7 +12,7 @@ pub fn limits_override_path() -> std::path::PathBuf { pub fn get_dev_messages() -> Vec { let limits_path = limits_path(); if let Ok(file) = std::fs::File::open(&limits_path) { - if let Ok(base) = serde_json::from_reader::<_, Base>(file) { + if let Ok(base) = ron::de::from_reader::<_, Base>(file) { base.messages } else { vec![] diff --git a/backend/src/settings/mod.rs b/backend/src/settings/mod.rs index 5aa3483..5bcb3f4 100644 --- a/backend/src/settings/mod.rs +++ b/backend/src/settings/mod.rs @@ -23,7 +23,7 @@ pub use traits::{OnPowerEvent, OnResume, OnSet, PowerMode, TBattery, TCpu, TCpus mod tests { #[test] fn system_defaults_test() { - let settings = super::Settings::system_default("idc".into(), "Cool name".into()); + let settings = super::Settings::system_default("idc".into(), "Cool name".into(), 0, "Variant 0".into()); println!("Loaded system settings: {:?}", settings); } } diff --git a/backend/src/settings/util.rs b/backend/src/settings/util.rs index cede8f6..6d4e8ed 100644 --- a/backend/src/settings/util.rs +++ b/backend/src/settings/util.rs @@ -58,6 +58,7 @@ mod test { fn cpu_with_online(status: bool) -> CpuJson { CpuJson { + root: None, online: status, clock_limits: None, governor: "schedutil".to_owned(), diff --git a/backend/src/utility.rs b/backend/src/utility.rs index 00dae0b..595f17a 100644 --- a/backend/src/utility.rs +++ b/backend/src/utility.rs @@ -103,3 +103,26 @@ pub fn read_version_file() -> String { } } } +#[cfg(test)] +mod generate { + #[test] + fn generate_default_limits_override() { + let limits = limits_core::json_v2::Limits { + cpu: limits_core::json_v2::Limit { + provider: limits_core::json_v2::CpuLimitType::SteamDeck, + limits: limits_core::json_v2::GenericCpusLimit::default_for(limits_core::json_v2::CpuLimitType::SteamDeck), + }, + gpu: limits_core::json_v2::Limit { + provider: limits_core::json_v2::GpuLimitType::SteamDeck, + limits: limits_core::json_v2::GenericGpuLimit::default_for(limits_core::json_v2::GpuLimitType::SteamDeck), + }, + battery: limits_core::json_v2::Limit { + provider: limits_core::json_v2::BatteryLimitType::SteamDeck, + limits: limits_core::json_v2::GenericBatteryLimit::default_for(limits_core::json_v2::BatteryLimitType::SteamDeck), + }, + }; + let output_file = std::fs::File::create("../limits_override.ron").unwrap(); + ron::ser::to_writer_pretty(output_file, &limits, crate::utility::ron_pretty_config()).unwrap(); + } +} + diff --git a/limits_override.ron b/limits_override.ron new file mode 100644 index 0000000..f5cb160 --- /dev/null +++ b/limits_override.ron @@ -0,0 +1,142 @@ +Limits( + cpu: Limit( + provider: SteamDeck, + limits: GenericCpusLimit( + cpus: [GenericCpuLimit( + clock_min: Some(RangeLimit( + min: Some(1400), + max: Some(3500), + )), + clock_max: Some(RangeLimit( + min: Some(400), + max: Some(3500), + )), + clock_step: Some(100), + skip_resume_reclock: false, + ), GenericCpuLimit( + clock_min: Some(RangeLimit( + min: Some(1400), + max: Some(3500), + )), + clock_max: Some(RangeLimit( + min: Some(400), + max: Some(3500), + )), + clock_step: Some(100), + skip_resume_reclock: false, + ), GenericCpuLimit( + clock_min: Some(RangeLimit( + min: Some(1400), + max: Some(3500), + )), + clock_max: Some(RangeLimit( + min: Some(400), + max: Some(3500), + )), + clock_step: Some(100), + skip_resume_reclock: false, + ), GenericCpuLimit( + clock_min: Some(RangeLimit( + min: Some(1400), + max: Some(3500), + )), + clock_max: Some(RangeLimit( + min: Some(400), + max: Some(3500), + )), + clock_step: Some(100), + skip_resume_reclock: false, + ), GenericCpuLimit( + clock_min: Some(RangeLimit( + min: Some(1400), + max: Some(3500), + )), + clock_max: Some(RangeLimit( + min: Some(400), + max: Some(3500), + )), + clock_step: Some(100), + skip_resume_reclock: false, + ), GenericCpuLimit( + clock_min: Some(RangeLimit( + min: Some(1400), + max: Some(3500), + )), + clock_max: Some(RangeLimit( + min: Some(400), + max: Some(3500), + )), + clock_step: Some(100), + skip_resume_reclock: false, + ), GenericCpuLimit( + clock_min: Some(RangeLimit( + min: Some(1400), + max: Some(3500), + )), + clock_max: Some(RangeLimit( + min: Some(400), + max: Some(3500), + )), + clock_step: Some(100), + skip_resume_reclock: false, + ), GenericCpuLimit( + clock_min: Some(RangeLimit( + min: Some(1400), + max: Some(3500), + )), + clock_max: Some(RangeLimit( + min: Some(400), + max: Some(3500), + )), + clock_step: Some(100), + skip_resume_reclock: false, + )], + global_governors: true, + ), + ), + gpu: Limit( + provider: SteamDeck, + limits: GenericGpuLimit( + fast_ppt: Some(RangeLimit( + min: Some(1000000), + max: Some(30000000), + )), + fast_ppt_default: Some(15000000), + slow_ppt: Some(RangeLimit( + min: Some(1000000), + max: Some(29000000), + )), + slow_ppt_default: Some(15000000), + ppt_divisor: Some(1000000), + ppt_step: Some(1), + tdp: None, + tdp_boost: None, + tdp_step: None, + clock_min: Some(RangeLimit( + min: Some(400), + max: Some(1600), + )), + clock_max: Some(RangeLimit( + min: Some(400), + max: Some(1600), + )), + clock_step: Some(100), + skip_resume_reclock: false, + ), + ), + battery: Limit( + provider: SteamDeck, + limits: GenericBatteryLimit( + charge_rate: Some(RangeLimit( + min: Some(250), + max: Some(2500), + )), + charge_modes: ["normal", "discharge", "idle"], + charge_limit: Some(RangeLimit( + min: Some(10.0), + max: Some(90.0), + )), + extra_readouts: false, + ), + ), +) \ No newline at end of file From 04a94c902f176e60d1c7bdde390ab8033c55863d Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 26 Nov 2023 09:50:13 -0500 Subject: [PATCH 11/56] Update limit enum names used in ron --- backend/limits_core/src/json_v2/battery_limit.rs | 2 ++ backend/limits_core/src/json_v2/cpu_limit.rs | 2 ++ backend/limits_core/src/json_v2/gpu_limit.rs | 2 ++ limits_override.ron | 6 +++--- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/limits_core/src/json_v2/battery_limit.rs b/backend/limits_core/src/json_v2/battery_limit.rs index f064fd9..4986283 100644 --- a/backend/limits_core/src/json_v2/battery_limit.rs +++ b/backend/limits_core/src/json_v2/battery_limit.rs @@ -4,7 +4,9 @@ use super::RangeLimit; #[derive(Serialize, Deserialize, Debug, Clone)] //#[serde(tag = "target")] pub enum BatteryLimitType { + #[serde(rename = "GabeBoy", alias = "SteamDeck")] SteamDeck, + #[serde(rename = "GabeBoyAdvance", alias = "SteamDeckAdvance")] SteamDeckAdvance, Generic, Unknown, diff --git a/backend/limits_core/src/json_v2/cpu_limit.rs b/backend/limits_core/src/json_v2/cpu_limit.rs index 84f7ea0..b2621d7 100644 --- a/backend/limits_core/src/json_v2/cpu_limit.rs +++ b/backend/limits_core/src/json_v2/cpu_limit.rs @@ -5,7 +5,9 @@ use super::RangeLimit; #[derive(Serialize, Deserialize, Debug, Clone)] //#[serde(tag = "target")] pub enum CpuLimitType { + #[serde(rename = "GabeBoy", alias = "SteamDeck")] SteamDeck, + #[serde(rename = "GabeBoyAdvance", alias = "SteamDeckAdvance")] SteamDeckAdvance, Generic, GenericAMD, diff --git a/backend/limits_core/src/json_v2/gpu_limit.rs b/backend/limits_core/src/json_v2/gpu_limit.rs index 39e09c1..32e47f6 100644 --- a/backend/limits_core/src/json_v2/gpu_limit.rs +++ b/backend/limits_core/src/json_v2/gpu_limit.rs @@ -4,7 +4,9 @@ use super::RangeLimit; #[derive(Serialize, Deserialize, Debug, Clone)] //#[serde(tag = "target")] pub enum GpuLimitType { + #[serde(rename = "GabeBoy", alias = "SteamDeck")] SteamDeck, + #[serde(rename = "GabeBoyAdvance", alias = "SteamDeckAdvance")] SteamDeckAdvance, Generic, GenericAMD, diff --git a/limits_override.ron b/limits_override.ron index f5cb160..2d74572 100644 --- a/limits_override.ron +++ b/limits_override.ron @@ -1,6 +1,6 @@ Limits( cpu: Limit( - provider: SteamDeck, + provider: GabeBoy, limits: GenericCpusLimit( cpus: [GenericCpuLimit( clock_min: Some(RangeLimit( @@ -95,7 +95,7 @@ Limits( ), ), gpu: Limit( - provider: SteamDeck, + provider: GabeBoy, limits: GenericGpuLimit( fast_ppt: Some(RangeLimit( min: Some(1000000), @@ -125,7 +125,7 @@ Limits( ), ), battery: Limit( - provider: SteamDeck, + provider: GabeBoy, limits: GenericBatteryLimit( charge_rate: Some(RangeLimit( min: Some(250), From d481f1314409c99a0ed48b006a8e9b53f6caa8a5 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 18 Nov 2023 17:16:58 -0500 Subject: [PATCH 12/56] Make settings variant map use u64 instead of strings since no longer restricted by JSON --- backend/src/main.rs | 2 +- backend/src/persist/file.rs | 6 +- backend/src/persist/general.rs | 2 +- backend/src/settings/general.rs | 2 +- backend/src/utility.rs | 34 +++++++++- default_settings.ron | 108 ++++++++++++++++++++++++++++++++ 6 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 default_settings.ron diff --git a/backend/src/main.rs b/backend/src/main.rs index 0c94237..1c34fd4 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -76,7 +76,7 @@ fn main() -> Result<(), ()> { let mut loaded_settings = persist::FileJson::open(utility::settings_dir().join(DEFAULT_SETTINGS_FILE)) - .map(|mut file| file.variants.remove("0") + .map(|mut file| file.variants.remove(&0) .map(|settings| settings::Settings::from_json(DEFAULT_SETTINGS_NAME.into(), settings, DEFAULT_SETTINGS_FILE.into())) .unwrap_or_else(|| settings::Settings::system_default( DEFAULT_SETTINGS_FILE.into(), diff --git a/backend/src/persist/file.rs b/backend/src/persist/file.rs index be575f4..2750996 100644 --- a/backend/src/persist/file.rs +++ b/backend/src/persist/file.rs @@ -9,7 +9,7 @@ use super::SettingsJson; pub struct FileJson { pub version: u64, pub name: String, - pub variants: HashMap, + pub variants: HashMap, } impl FileJson { @@ -45,11 +45,11 @@ impl FileJson { let file = if path.exists() { let mut file = Self::open(path)?; - file.variants.insert(setting.variant.to_string(), setting); + file.variants.insert(setting.variant, setting); file } else { let mut setting_variants = HashMap::with_capacity(1); - setting_variants.insert(setting.variant.to_string(), setting); + setting_variants.insert(setting.variant, setting); Self { version: 0, name: given_name, diff --git a/backend/src/persist/general.rs b/backend/src/persist/general.rs index 9809403..d3f7ab4 100644 --- a/backend/src/persist/general.rs +++ b/backend/src/persist/general.rs @@ -20,7 +20,7 @@ impl Default for SettingsJson { fn default() -> Self { Self { version: 0, - name: crate::consts::DEFAULT_SETTINGS_NAME.to_owned(), + name: crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(), variant: 0, persistent: false, cpus: Vec::with_capacity(8), diff --git a/backend/src/settings/general.rs b/backend/src/settings/general.rs index a2f729f..b1d5cf8 100644 --- a/backend/src/settings/general.rs +++ b/backend/src/settings/general.rs @@ -178,7 +178,7 @@ impl Settings { } pub fn get_variant<'a>(settings_file: &'a FileJson, variant_id: u64, variant_name: String) -> Result<&'a SettingsJson, SettingError> { - if let Some(variant) = settings_file.variants.get(&variant_id.to_string()) { + if let Some(variant) = settings_file.variants.get(&variant_id) { Ok(variant) } else { Err(SettingError { diff --git a/backend/src/utility.rs b/backend/src/utility.rs index 595f17a..123ce3a 100644 --- a/backend/src/utility.rs +++ b/backend/src/utility.rs @@ -121,8 +121,40 @@ mod generate { limits: limits_core::json_v2::GenericBatteryLimit::default_for(limits_core::json_v2::BatteryLimitType::SteamDeck), }, }; - let output_file = std::fs::File::create("../limits_override.ron").unwrap(); + let output_file = std::fs::File::create(format!("../{}", crate::consts::LIMITS_OVERRIDE_FILE)).unwrap(); ron::ser::to_writer_pretty(output_file, &limits, crate::utility::ron_pretty_config()).unwrap(); } + + #[test] + fn generate_default_minimal_save_file() { + let mut mini_variants = std::collections::HashMap::with_capacity(2); + mini_variants.insert(0, crate::persist::SettingsJson { + version: 0, + name: crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(), + variant: 0, + persistent: false, + cpus: vec![crate::persist::CpuJson::default(); 8], + gpu: crate::persist::GpuJson::default(), + battery: crate::persist::BatteryJson::default(), + provider: None, + }); + mini_variants.insert(42, crate::persist::SettingsJson { + version: 0, + name: "FortySecondary".to_owned(), + variant: 42, + persistent: false, + cpus: vec![crate::persist::CpuJson::default(); 8], + gpu: crate::persist::GpuJson::default(), + battery: crate::persist::BatteryJson::default(), + provider: None, + }); + let savefile = crate::persist::FileJson { + version: 0, + name: crate::consts::DEFAULT_SETTINGS_NAME.to_owned(), + variants: mini_variants, + }; + let output_file = std::fs::File::create(format!("../{}", crate::consts::DEFAULT_SETTINGS_FILE)).unwrap(); + ron::ser::to_writer_pretty(output_file, &savefile, crate::utility::ron_pretty_config()).unwrap(); + } } diff --git a/default_settings.ron b/default_settings.ron new file mode 100644 index 0000000..fbc356a --- /dev/null +++ b/default_settings.ron @@ -0,0 +1,108 @@ +FileJson( + version: 0, + name: "Main", + variants: { + 0: SettingsJson( + version: 0, + name: "Primary", + variant: 0, + persistent: false, + cpus: [CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + )], + gpu: GpuJson( + fast_ppt: None, + slow_ppt: None, + clock_limits: None, + slow_memory: false, + ), + battery: BatteryJson( + charge_rate: None, + charge_mode: None, + events: [], +), + provider: None, + ), + 42: SettingsJson( + version: 0, + name: "FortySecondary", + variant: 42, + persistent: false, + cpus: [CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + ), CpuJson( + online: true, + clock_limits: None, + governor: "schedutil", + )], + gpu: GpuJson( + fast_ppt: None, + slow_ppt: None, + clock_limits: None, + slow_memory: false, + ), + battery: BatteryJson( + charge_rate: None, + charge_mode: None, + events: [], +), + provider: None, + ), + }, +) \ No newline at end of file From 0b8e3deb9280f2a7af3db1c7ae11f2b970e6af42 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 26 Nov 2023 17:27:05 -0500 Subject: [PATCH 13/56] Fix reset value for max charge level --- backend/src/settings/steam_deck/battery.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index 07ac1a9..87f2d87 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -597,7 +597,7 @@ impl OnPowerEvent for Battery { // only true when charge_limit_set is false and self.state.charge_limit_set is true self.state.charge_limit_set = false; if attr_exists { - self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_LEVEL_ATTR, 100) + self.sysfs_hwmon.set(MAX_BATTERY_CHARGE_LEVEL_ATTR, 0) .unwrap_or_else(|e| errors.push( SettingError { msg: format!("Failed to reset (write to) {:?}: {}", MAX_BATTERY_CHARGE_LEVEL_ATTR, e), From 8f219a3255be61da64c28bea963ccf3210f884d9 Mon Sep 17 00:00:00 2001 From: drokath Date: Wed, 29 Nov 2023 01:47:25 +0000 Subject: [PATCH 14/56] Improve AMD 6800 handheld detection (#132) Related to #131 tested on Aokzoe A1 Co-authored-by: Vincent Guilbert Reviewed-on: https://git.ngni.us/NG-SD-Plugins/PowerTools/pulls/132 Co-authored-by: drokath Co-committed-by: drokath --- backend/limits_core/src/json/base.rs | 4 ++-- backend/limits_core/src/json_v2/base.rs | 4 ++-- backend/limits_srv/pt_limits.json | 2 +- backend/limits_srv/pt_limits_v2.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/limits_core/src/json/base.rs b/backend/limits_core/src/json/base.rs index c08372a..7abf32d 100644 --- a/backend/limits_core/src/json/base.rs +++ b/backend/limits_core/src/json/base.rs @@ -1,5 +1,5 @@ -use std::default::Default; use serde::{Deserialize, Serialize}; +use std::default::Default; /// Base JSON limits information #[derive(Serialize, Deserialize, Debug, Clone)] @@ -131,7 +131,7 @@ impl Default for Base { name: "AMD R7 6800U".to_owned(), conditions: super::Conditions { dmi: None, - cpuinfo: Some("model name\t+: AMD Ryzen 7 6800U\n".to_owned()), + cpuinfo: Some("model name\t+: AMD Ryzen 7 6800U( with Radeon Graphics)?\n".to_owned()), os: None, command: None, file_exists: None, diff --git a/backend/limits_core/src/json_v2/base.rs b/backend/limits_core/src/json_v2/base.rs index 81643d9..60cac16 100644 --- a/backend/limits_core/src/json_v2/base.rs +++ b/backend/limits_core/src/json_v2/base.rs @@ -1,5 +1,5 @@ -use std::default::Default; use serde::{Deserialize, Serialize}; +use std::default::Default; /// Base JSON limits information #[derive(Serialize, Deserialize, Debug, Clone)] @@ -191,7 +191,7 @@ impl Default for Base { name: "AMD R7 6800U".to_owned(), conditions: super::Conditions { dmi: None, - cpuinfo: Some("model name\t+: AMD Ryzen 7 6800U\n".to_owned()), + cpuinfo: Some("model name\t+: AMD Ryzen 7 6800U( with Radeon Graphics)?\n".to_owned()), os: None, command: None, file_exists: None, diff --git a/backend/limits_srv/pt_limits.json b/backend/limits_srv/pt_limits.json index eea7711..198e5ee 100644 --- a/backend/limits_srv/pt_limits.json +++ b/backend/limits_srv/pt_limits.json @@ -214,7 +214,7 @@ "name": "AMD R7 6800U", "conditions": { "dmi": null, - "cpuinfo": "model name\t+: AMD Ryzen 7 6800U\n", + "cpuinfo": "model name\t+: AMD Ryzen 7 6800U( with Radeon Graphics)?\n", "os": null, "command": null, "file_exists": null diff --git a/backend/limits_srv/pt_limits_v2.json b/backend/limits_srv/pt_limits_v2.json index fc515c6..3556db8 100644 --- a/backend/limits_srv/pt_limits_v2.json +++ b/backend/limits_srv/pt_limits_v2.json @@ -895,7 +895,7 @@ "name": "AMD R7 6800U", "conditions": { "dmi": null, - "cpuinfo": "model name\t+: AMD Ryzen 7 6800U\n", + "cpuinfo": "model name\t+: AMD Ryzen 7 6800U( with Radeon Graphics)?\n", "os": null, "command": null, "file_exists": null From a2d5103f12892e2730de1dca952a12c83fb5ba3b Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 10 Dec 2023 23:15:31 -0500 Subject: [PATCH 15/56] Improve generic support, add Framework 13 AMD support for test platform (ryzenadj v0.14 update not included) --- backend/limits_core/src/json_v2/base.rs | 70 +++++- backend/limits_srv/pt_limits_v2.json | 305 +++++++++++++++++++++--- backend/src/main.rs | 4 +- backend/src/settings/generic/battery.rs | 90 +++++-- backend/src/settings/generic/cpu.rs | 9 +- backend/src/settings/generic/gpu.rs | 24 +- backend/src/settings/generic_amd/gpu.rs | 55 ++++- 7 files changed, 483 insertions(+), 74 deletions(-) diff --git a/backend/limits_core/src/json_v2/base.rs b/backend/limits_core/src/json_v2/base.rs index 60cac16..987ffda 100644 --- a/backend/limits_core/src/json_v2/base.rs +++ b/backend/limits_core/src/json_v2/base.rs @@ -90,9 +90,10 @@ impl Default for Base { gpu: super::GpuLimit { provider: super::GpuLimitType::GenericAMD, limits: super::GenericGpuLimit { - fast_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(25_000_000) }), - slow_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(25_000_000) }), - ppt_step: Some(1_000_000), + fast_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(25_000) }), + slow_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(25_000) }), + ppt_step: Some(1_000), + ppt_divisor: Some(1_000), clock_min: Some(super::RangeLimit { min: Some(400), max: Some(1100) }), clock_max: Some(super::RangeLimit { min: Some(400), max: Some(1100) }), clock_step: Some(100), @@ -131,9 +132,10 @@ impl Default for Base { gpu: super::GpuLimit { provider: super::GpuLimitType::GenericAMD, limits: super::GenericGpuLimit { - fast_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(25_000_000) }), - slow_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(25_000_000) }), - ppt_step: Some(1_000_000), + fast_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(25_000) }), + slow_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(25_000) }), + ppt_step: Some(1_000), + ppt_divisor: Some(1_000), clock_min: Some(super::RangeLimit { min: Some(400), max: Some(1600) }), clock_max: Some(super::RangeLimit { min: Some(400), max: Some(1600) }), clock_step: Some(100), @@ -172,9 +174,10 @@ impl Default for Base { gpu: super::GpuLimit { provider: super::GpuLimitType::GenericAMD, limits: super::GenericGpuLimit { - fast_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(28_000_000) }), - slow_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(28_000_000) }), - ppt_step: Some(1_000_000), + fast_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(28_000) }), + slow_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(28_000) }), + ppt_step: Some(1_000), + ppt_divisor: Some(1_000), clock_min: Some(super::RangeLimit { min: Some(400), max: Some(2200) }), clock_max: Some(super::RangeLimit { min: Some(400), max: Some(2200) }), clock_step: Some(100), @@ -213,9 +216,10 @@ impl Default for Base { gpu: super::GpuLimit { provider: super::GpuLimitType::GenericAMD, limits: super::GenericGpuLimit { - fast_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(28_000_000) }), - slow_ppt: Some(super::RangeLimit { min: Some(1_000_000), max: Some(28_000_000) }), - ppt_step: Some(1_000_000), + fast_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(28_000) }), + slow_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(28_000) }), + ppt_step: Some(1_000), + ppt_divisor: Some(1_000), clock_min: Some(super::RangeLimit { min: Some(400), max: Some(2200) }), clock_max: Some(super::RangeLimit { min: Some(400), max: Some(2200) }), clock_step: Some(100), @@ -228,6 +232,48 @@ impl Default for Base { } } }, + super::Config { + name: "AMD R7 7840U".to_owned(), + conditions: super::Conditions { + dmi: None, + cpuinfo: Some("model name\\s+: AMD Ryzen 7 7840U( w\\/ Radeon 780M Graphics)?\n".to_owned()), + os: None, + command: None, + file_exists: None, + }, + limits: super::Limits { + cpu: super::CpuLimit { + provider: super::CpuLimitType::GenericAMD, + limits: super::GenericCpusLimit { + cpus: vec![ + super::GenericCpuLimit { + clock_min: Some(super::RangeLimit { min: Some(400), max: Some(5100) }), + clock_max: Some(super::RangeLimit { min: Some(400), max: Some(5100) }), + clock_step: Some(100), + skip_resume_reclock: false, + }; 16], // 8 cores with SMTx2 + global_governors: true, + } + }, + gpu: super::GpuLimit { + provider: super::GpuLimitType::GenericAMD, + limits: super::GenericGpuLimit { + fast_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(53_000) }), + slow_ppt: Some(super::RangeLimit { min: Some(1_000), max: Some(43_000) }), + ppt_step: Some(1_000), + ppt_divisor: Some(1_000), + clock_min: None, + clock_max: None, + clock_step: None, + ..Default::default() + } + }, + battery: super::Limit { + provider: super::BatteryLimitType::Generic, + limits: super::GenericBatteryLimit::default_for(super::BatteryLimitType::Generic), + } + } + }, super::Config { name: "Fallback".to_owned(), conditions: super::Conditions { diff --git a/backend/limits_srv/pt_limits_v2.json b/backend/limits_srv/pt_limits_v2.json index 3556db8..24dcd84 100644 --- a/backend/limits_srv/pt_limits_v2.json +++ b/backend/limits_srv/pt_limits_v2.json @@ -11,7 +11,7 @@ }, "limits": { "cpu": { - "provider": "SteamDeckAdvance", + "provider": "GabeBoyAdvance", "limits": { "cpus": [ { @@ -115,7 +115,7 @@ } }, "gpu": { - "provider": "SteamDeckAdvance", + "provider": "GabeBoyAdvance", "limits": { "fast_ppt": { "min": 1000000, @@ -145,7 +145,7 @@ } }, "battery": { - "provider": "SteamDeckAdvance", + "provider": "GabeBoyAdvance", "limits": { "charge_rate": { "min": 250, @@ -176,7 +176,7 @@ }, "limits": { "cpu": { - "provider": "SteamDeck", + "provider": "GabeBoy", "limits": { "cpus": [ { @@ -280,7 +280,7 @@ } }, "gpu": { - "provider": "SteamDeck", + "provider": "GabeBoy", "limits": { "fast_ppt": { "min": 1000000, @@ -310,7 +310,7 @@ } }, "battery": { - "provider": "SteamDeck", + "provider": "GabeBoy", "limits": { "charge_rate": { "min": 250, @@ -400,17 +400,17 @@ "provider": "GenericAMD", "limits": { "fast_ppt": { - "min": 1000000, - "max": 25000000 + "min": 1000, + "max": 25000 }, "fast_ppt_default": null, "slow_ppt": { - "min": 1000000, - "max": 25000000 + "min": 1000, + "max": 25000 }, "slow_ppt_default": null, - "ppt_divisor": null, - "ppt_step": 1000000, + "ppt_divisor": 1000, + "ppt_step": 1000, "tdp": null, "tdp_boost": null, "tdp_step": null, @@ -603,17 +603,17 @@ "provider": "GenericAMD", "limits": { "fast_ppt": { - "min": 1000000, - "max": 25000000 + "min": 1000, + "max": 25000 }, "fast_ppt_default": null, "slow_ppt": { - "min": 1000000, - "max": 25000000 + "min": 1000, + "max": 25000 }, "slow_ppt_default": null, - "ppt_divisor": null, - "ppt_step": 1000000, + "ppt_divisor": 1000, + "ppt_step": 1000, "tdp": null, "tdp_boost": null, "tdp_step": null, @@ -854,17 +854,17 @@ "provider": "GenericAMD", "limits": { "fast_ppt": { - "min": 1000000, - "max": 28000000 + "min": 1000, + "max": 28000 }, "fast_ppt_default": null, "slow_ppt": { - "min": 1000000, - "max": 28000000 + "min": 1000, + "max": 28000 }, "slow_ppt_default": null, - "ppt_divisor": null, - "ppt_step": 1000000, + "ppt_divisor": 1000, + "ppt_step": 1000, "tdp": null, "tdp_boost": null, "tdp_step": null, @@ -1105,17 +1105,17 @@ "provider": "GenericAMD", "limits": { "fast_ppt": { - "min": 1000000, - "max": 28000000 + "min": 1000, + "max": 28000 }, "fast_ppt_default": null, "slow_ppt": { - "min": 1000000, - "max": 28000000 + "min": 1000, + "max": 28000 }, "slow_ppt_default": null, - "ppt_divisor": null, - "ppt_step": 1000000, + "ppt_divisor": 1000, + "ppt_step": 1000, "tdp": null, "tdp_boost": null, "tdp_step": null, @@ -1142,6 +1142,251 @@ } } }, + { + "name": "AMD R7 7840U", + "conditions": { + "dmi": null, + "cpuinfo": "model name\\s+: AMD Ryzen 7 7840U( w\\/ Radeon 780M Graphics)?\n", + "os": null, + "command": null, + "file_exists": null + }, + "limits": { + "cpu": { + "provider": "GenericAMD", + "limits": { + "cpus": [ + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + }, + { + "clock_min": { + "min": 400, + "max": 5100 + }, + "clock_max": { + "min": 400, + "max": 5100 + }, + "clock_step": 100, + "skip_resume_reclock": false + } + ], + "global_governors": true + } + }, + "gpu": { + "provider": "GenericAMD", + "limits": { + "fast_ppt": { + "min": 1000, + "max": 53000 + }, + "fast_ppt_default": null, + "slow_ppt": { + "min": 1000, + "max": 43000 + }, + "slow_ppt_default": null, + "ppt_divisor": 1000, + "ppt_step": 1000, + "tdp": null, + "tdp_boost": null, + "tdp_step": null, + "clock_min": null, + "clock_max": null, + "clock_step": null, + "skip_resume_reclock": false + } + }, + "battery": { + "provider": "Generic", + "limits": { + "charge_rate": null, + "charge_modes": [], + "charge_limit": null, + "extra_readouts": false + } + } + } + }, { "name": "Fallback", "conditions": { diff --git a/backend/src/main.rs b/backend/src/main.rs index 1c34fd4..a07b4d4 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -25,6 +25,7 @@ fn main() -> Result<(), ()> { .join(PACKAGE_NAME.to_owned() + ".log"); #[cfg(not(debug_assertions))] let log_filepath = std::path::Path::new("/tmp").join(format!("{}.log", PACKAGE_NAME)); + println!("Logging to: {:?}", log_filepath); #[cfg(debug_assertions)] let old_log_filepath = usdpl_back::api::dirs::home() .unwrap_or_else(|| "/tmp/".into()) @@ -46,12 +47,11 @@ fn main() -> Result<(), ()> { LevelFilter::Info }, Default::default(), - std::fs::File::create(&log_filepath).unwrap(), + std::fs::File::create(&log_filepath).expect("Failed to create log file"), //std::fs::File::create("/home/deck/powertools-rs.log").unwrap(), ) .unwrap(); log::debug!("Logging to: {:?}.", log_filepath); - println!("Logging to: {:?}", log_filepath); log::info!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION); println!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION); log::info!( diff --git a/backend/src/settings/generic/battery.rs b/backend/src/settings/generic/battery.rs index 2c64075..af08b11 100644 --- a/backend/src/settings/generic/battery.rs +++ b/backend/src/settings/generic/battery.rs @@ -1,7 +1,7 @@ use std::convert::Into; use limits_core::json_v2::GenericBatteryLimit; -use sysfuss::SysEntity; +use sysfuss::{SysEntity, SysEntityAttributesExt}; use crate::persist::BatteryJson; use crate::settings::{TBattery, ProviderBuilder}; @@ -27,7 +27,7 @@ impl Into for Battery { } impl Battery { - fn read_f64>(path: P) -> Result { + /*fn read_f64>(path: P) -> Result { let path = path.as_ref(); match usdpl_back::api::files::read_single::<_, f64, _>(path) { Err(e) => Err(SettingError { @@ -38,13 +38,14 @@ impl Battery { // so convert this to mA for consistency Ok(val) => Ok(val / 1000.0), } - } + }*/ fn find_psu_sysfs(root: Option>) -> sysfuss::PowerSupplyPath { let root = crate::settings::util::root_or_default_sysfs(root); match root.power_supply(crate::settings::util::always_satisfied) { - Ok(mut iter) => { - iter.next() + Ok(iter) => { + iter.filter(|x| x.name().is_ok_and(|name| name.starts_with("BAT"))) + .next() .unwrap_or_else(|| { log::error!("Failed to find generic battery power_supply in sysfs (no results), using naive fallback"); root.power_supply_by_name("BAT0") @@ -56,6 +57,28 @@ impl Battery { } } } + + fn get_design_voltage(&self) -> Option { + match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::VoltageMax) { + Ok(x) => Some(x/1000000.0), + Err(e) => { + log::debug!("get_design_voltage voltage_max err: {}", e); + match sysfuss::SysEntityRawExt::attribute::<_, f64, _>(&self.sysfs, "voltage_min_design".to_owned()) { // Framework 13 AMD + Ok(x) => Some(x/1000000.0), + Err(e) => { + log::debug!("get_design_voltage voltage_min_design err: {}", e); + match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::VoltageMin) { + Ok(x) => Some(x/1000000.0), + Err(e) => { + log::debug!("get_design_voltage voltage_min err: {}", e); + None + } + } + } + } + } + } + } } impl ProviderBuilder for Battery { @@ -124,37 +147,55 @@ impl TBattery for Battery { } fn read_charge_full(&self) -> Option { - match Self::read_f64("/sys/class/power_supply/BAT0/energy_full") { - Ok(x) => Some(x), - Err(e) => { - log::warn!("read_charge_full err: {}", e.msg); - None + if let Some(battery_voltage) = self.get_design_voltage() { + match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::ChargeFull) { + Ok(x) => Some(x/1000000.0 * battery_voltage), + Err(e) => { + log::warn!("read_charge_full err: {}", e); + None + } } + } else { + None } } fn read_charge_now(&self) -> Option { - match Self::read_f64("/sys/class/power_supply/BAT0/energy_now") { - Ok(x) => Some(x), - Err(e) => { - log::warn!("read_charge_now err: {}", e.msg); - None + if let Some(battery_voltage) = self.get_design_voltage() { + match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::ChargeNow) { + Ok(x) => Some(x/1000000.0 * battery_voltage), + Err(e) => { + log::warn!("read_charge_now err: {}", e); + None + } } + } else { + None } } fn read_charge_design(&self) -> Option { - match Self::read_f64("/sys/class/power_supply/BAT0/energy_design") { - Ok(x) => Some(x), - Err(e) => { - log::warn!("read_charge_design err: {}", e.msg); - None + if let Some(battery_voltage) = self.get_design_voltage() { + match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::ChargeFullDesign) { + Ok(x) => Some(x/1000000.0 * battery_voltage), + Err(e) => { + log::warn!("read_charge_design err: {}", e); + None + } } + } else { + None } } fn read_current_now(&self) -> Option { - None + match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::CurrentNow) { + Ok(x) => Some(x/1000.0), // expects mA, reads uA + Err(e) => { + log::warn!("read_current_now err: {}", e); + None + } + } } fn read_charge_power(&self) -> Option { @@ -164,6 +205,13 @@ impl TBattery for Battery { fn charge_limit(&mut self, _limit: Option) {} fn get_charge_limit(&self) -> Option { + /*match self.sysfs.attribute::(sysfuss::PowerSupplyAttribute::ChargeControlLimit) { + Ok(x) => Some(x/1000.0), + Err(e) => { + log::warn!("read_charge_design err: {}", e); + None + } + }*/ None } diff --git a/backend/src/settings/generic/cpu.rs b/backend/src/settings/generic/cpu.rs index 40d2056..75d0d1e 100644 --- a/backend/src/settings/generic/cpu.rs +++ b/backend/src/settings/generic/cpu.rs @@ -218,6 +218,13 @@ pub struct Cpu { } }*/ +impl Cpu { + #[inline] + fn current_governor(index: usize) -> String { + usdpl_back::api::files::read_single(cpu_governor_path(index)).unwrap_or_else(|_| "schedutil".to_owned()) + } +} + impl AsRef for Cpu { #[inline] fn as_ref(&self) -> &Cpu { @@ -237,7 +244,7 @@ impl FromGenericCpuInfo for Cpu { fn from_limits(cpu_index: usize, limits: GenericCpuLimit) -> Self { Self { online: true, - governor: "schedutil".to_owned(), + governor: Self::current_governor(cpu_index), clock_limits: None, limits, index: cpu_index, diff --git a/backend/src/settings/generic/gpu.rs b/backend/src/settings/generic/gpu.rs index 0bee174..1da9a82 100644 --- a/backend/src/settings/generic/gpu.rs +++ b/backend/src/settings/generic/gpu.rs @@ -15,7 +15,7 @@ pub struct Gpu { pub fast_ppt: Option, pub slow_ppt: Option, pub clock_limits: Option>, - limits: GenericGpuLimit, + pub limits: GenericGpuLimit, sysfs: BasicEntityPath, } @@ -122,13 +122,23 @@ impl TGpu for Gpu { .limits .fast_ppt .clone() - .map(|x| RangeLimit::new(x.min.unwrap_or(0), x.max.unwrap_or(15_000_000))), + .map(|x| RangeLimit::new(x.min.unwrap_or(0), x.max.unwrap_or(15))) + .map(|mut x| if let Some(ppt_divisor) = self.limits.ppt_divisor { + x.min /= ppt_divisor; + x.max /= ppt_divisor; + x + } else {x}), slow_ppt_limits: self .limits .slow_ppt .clone() - .map(|x| RangeLimit::new(x.min.unwrap_or(0), x.max.unwrap_or(15_000_000))), - ppt_step: self.limits.ppt_step.unwrap_or(1_000_000), + .map(|x| RangeLimit::new(x.min.unwrap_or(0), x.max.unwrap_or(15))) + .map(|mut x| if let Some(ppt_divisor) = self.limits.ppt_divisor { + x.min /= ppt_divisor; + x.max /= ppt_divisor; + x + } else {x}), + ppt_step: self.limits.ppt_step.unwrap_or(1), tdp_limits: self .limits .tdp @@ -161,7 +171,8 @@ impl TGpu for Gpu { fn ppt(&mut self, fast: Option, slow: Option) { if let Some(fast_lims) = &self.limits.fast_ppt { - self.fast_ppt = fast.map(|x| { + self.fast_ppt = fast.map(|x| if let Some(ppt_divisor) = self.limits.ppt_divisor { x * ppt_divisor } else { x }) + .map(|x| { x.clamp( fast_lims.min.unwrap_or(0), fast_lims.max.unwrap_or(u64::MAX), @@ -169,7 +180,8 @@ impl TGpu for Gpu { }); } if let Some(slow_lims) = &self.limits.slow_ppt { - self.slow_ppt = slow.map(|x| { + self.slow_ppt = slow.map(|x| if let Some(ppt_divisor) = self.limits.ppt_divisor { x * ppt_divisor } else { x }) + .map(|x| { x.clamp( slow_lims.min.unwrap_or(0), slow_lims.max.unwrap_or(u64::MAX), diff --git a/backend/src/settings/generic_amd/gpu.rs b/backend/src/settings/generic_amd/gpu.rs index 9d0090b..6e8f833 100644 --- a/backend/src/settings/generic_amd/gpu.rs +++ b/backend/src/settings/generic_amd/gpu.rs @@ -7,9 +7,40 @@ use crate::settings::MinMax; use crate::settings::{TGpu, ProviderBuilder}; use crate::settings::{OnResume, OnSet, SettingError, SettingVariant}; +fn msg_or_err(output: &mut String, msg: &str, result: Result) { + use std::fmt::Write; + match result { + Ok(val) => writeln!(output, "{}: {}", msg, val).unwrap(), + Err(e) => writeln!(output, "{} failed: {}", msg, e).unwrap(), + } +} + +fn log_capabilities(ryzenadj: &RyzenAdj) { + log::info!("RyzenAdj v{}.{}.{}", libryzenadj::libryzenadj_sys::RYZENADJ_REVISION_VER, libryzenadj::libryzenadj_sys::RYZENADJ_MAJOR_VER, libryzenadj::libryzenadj_sys::RYZENADJ_MINIOR_VER); + if let Some(x) = ryzenadj.get_init_table_err() { + log::warn!("RyzenAdj table init error: {}", x); + } + let mut log_msg = String::new(); + msg_or_err(&mut log_msg, "bios version", ryzenadj.get_bios_if_ver()); + msg_or_err(&mut log_msg, "refresh", ryzenadj.refresh().map(|_| "success")); + msg_or_err(&mut log_msg, "CPU family", ryzenadj.get_cpu_family().map(|fam| { + let fam_dbg = format!("{:?}", fam); + format!("{} (#{})", fam_dbg, fam as i32) + })); + msg_or_err(&mut log_msg, "get_fast_value (PPT)", ryzenadj.get_fast_value()); + msg_or_err(&mut log_msg, "get_slow_value (PPT)", ryzenadj.get_slow_value()); + msg_or_err(&mut log_msg, "get_gfx_clk", ryzenadj.get_gfx_clk()); + msg_or_err(&mut log_msg, "get_gfx_volt", ryzenadj.get_gfx_volt()); + + log::info!("RyzenAdj GPU info:\n{}", log_msg); +} + fn ryzen_adj_or_log() -> Option> { match RyzenAdj::new() { - Ok(x) => Some(Mutex::new(x)), + Ok(x) => { + log_capabilities(&x); + Some(Mutex::new(x)) + }, Err(e) => { log::error!("RyzenAdj init error: {}", e); None @@ -256,9 +287,29 @@ impl OnSet for Gpu { impl crate::settings::OnPowerEvent for Gpu {} +fn bad_gpu_limits() -> crate::api::GpuLimits { + crate::api::GpuLimits { + fast_ppt_limits: None, + slow_ppt_limits: None, + ppt_step: 1, + tdp_limits: None, + tdp_boost_limits: None, + tdp_step: 1, + clock_min_limits: None, + clock_max_limits: None, + clock_step: 100, + memory_control_capable: false, + } +} + impl TGpu for Gpu { fn limits(&self) -> crate::api::GpuLimits { - self.generic.limits() + if self.implementor.is_some() { + // NOTE: since set functions may succeed when gets do not, there is no good way to (automatically) check whether things are working + self.generic.limits() + } else { + bad_gpu_limits() + } } fn json(&self) -> crate::persist::GpuJson { From 30ad80f0311bfdd6b289149e60378c7ff95480ae Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 11 Dec 2023 17:52:22 -0500 Subject: [PATCH 16/56] Add ppt divisor to get_ppt instead of just setter --- backend/src/settings/generic/gpu.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/settings/generic/gpu.rs b/backend/src/settings/generic/gpu.rs index 1da9a82..e538147 100644 --- a/backend/src/settings/generic/gpu.rs +++ b/backend/src/settings/generic/gpu.rs @@ -191,7 +191,8 @@ impl TGpu for Gpu { } fn get_ppt(&self) -> (Option, Option) { - (self.fast_ppt, self.slow_ppt) + (self.fast_ppt.map(|x| if let Some(ppt_divisor) = self.limits.ppt_divisor { x / ppt_divisor } else { x }), + self.slow_ppt.map(|x| if let Some(ppt_divisor) = self.limits.ppt_divisor { x / ppt_divisor } else { x })) } fn clock_limits(&mut self, limits: Option>) { From df71a619d963ac3a2a21e9afdcdba0201fa04e6b Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 16 Dec 2023 14:40:36 -0500 Subject: [PATCH 17/56] Update limits config to correctly detect OLED with newer firmware's CPU name --- backend/limits_core/src/json/base.rs | 4 ++-- backend/limits_core/src/json_v2/base.rs | 4 ++-- backend/limits_srv/pt_limits.json | 4 ++-- backend/limits_srv/pt_limits_v2.json | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/limits_core/src/json/base.rs b/backend/limits_core/src/json/base.rs index 7abf32d..fefab76 100644 --- a/backend/limits_core/src/json/base.rs +++ b/backend/limits_core/src/json/base.rs @@ -20,7 +20,7 @@ impl Default for Base { name: "Steam Deck Custom".to_owned(), conditions: super::Conditions { dmi: None, - cpuinfo: Some("model name\t: AMD Custom APU 0405\n".to_owned()), + cpuinfo: Some("model name\t: AMD Custom APU (0405)|(0932)\n".to_owned()), os: None, command: None, file_exists: Some("./pt_oc.json".into()), @@ -35,7 +35,7 @@ impl Default for Base { name: "Steam Deck".to_owned(), conditions: super::Conditions { dmi: None, - cpuinfo: Some("model name\t: AMD Custom APU 0405\n".to_owned()), + cpuinfo: Some("model name\t: AMD Custom APU (0405)|(0932)\n".to_owned()), os: None, command: None, file_exists: None, diff --git a/backend/limits_core/src/json_v2/base.rs b/backend/limits_core/src/json_v2/base.rs index 987ffda..23c0717 100644 --- a/backend/limits_core/src/json_v2/base.rs +++ b/backend/limits_core/src/json_v2/base.rs @@ -20,7 +20,7 @@ impl Default for Base { name: "Steam Deck Custom".to_owned(), conditions: super::Conditions { dmi: None, - cpuinfo: Some("model name\t: AMD Custom APU 0405\n".to_owned()), + cpuinfo: Some("model name\t: AMD Custom APU (0405)|(0932)\n".to_owned()), os: None, command: None, file_exists: Some("./limits_override.json".into()), @@ -44,7 +44,7 @@ impl Default for Base { name: "Steam Deck".to_owned(), conditions: super::Conditions { dmi: None, - cpuinfo: Some("model name\t: AMD Custom APU 0405\n".to_owned()), + cpuinfo: Some("model name\t: AMD Custom APU (0405)|(0932)\n".to_owned()), os: None, command: None, file_exists: None, diff --git a/backend/limits_srv/pt_limits.json b/backend/limits_srv/pt_limits.json index 198e5ee..2e9264b 100644 --- a/backend/limits_srv/pt_limits.json +++ b/backend/limits_srv/pt_limits.json @@ -4,7 +4,7 @@ "name": "Steam Deck Custom", "conditions": { "dmi": null, - "cpuinfo": "model name\t: AMD Custom APU 0405\n", + "cpuinfo": "model name\t: AMD Custom APU (0405)|(0932)\n", "os": null, "command": null, "file_exists": "./pt_oc.json" @@ -28,7 +28,7 @@ "name": "Steam Deck", "conditions": { "dmi": null, - "cpuinfo": "model name\t: AMD Custom APU 0405\n", + "cpuinfo": "model name\t: AMD Custom APU (0405)|(0932)\n", "os": null, "command": null, "file_exists": null diff --git a/backend/limits_srv/pt_limits_v2.json b/backend/limits_srv/pt_limits_v2.json index 24dcd84..0196b5d 100644 --- a/backend/limits_srv/pt_limits_v2.json +++ b/backend/limits_srv/pt_limits_v2.json @@ -4,7 +4,7 @@ "name": "Steam Deck Custom", "conditions": { "dmi": null, - "cpuinfo": "model name\t: AMD Custom APU 0405\n", + "cpuinfo": "model name\t: AMD Custom APU (0405)|(0932)\n", "os": null, "command": null, "file_exists": "./limits_override.json" @@ -169,7 +169,7 @@ "name": "Steam Deck", "conditions": { "dmi": null, - "cpuinfo": "model name\t: AMD Custom APU 0405\n", + "cpuinfo": "model name\t: AMD Custom APU (0405)|(0932)\n", "os": null, "command": null, "file_exists": null From 310af1b3ae76ac993a69fb950d6f4558525f1a43 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 17 Dec 2023 16:13:59 -0500 Subject: [PATCH 18/56] Fix ryzenadj build error with feature flag, try fix game start callbacks, start LED lib refactor --- backend/Cargo.lock | 9 +- backend/Cargo.toml | 2 + backend/limits_core/Cargo.lock | 24 --- backend/limits_core/Cargo.toml | 1 - backend/limits_srv/Cargo.lock | 1 - backend/sd_led/Cargo.lock | 16 ++ backend/sd_led/Cargo.toml | 10 ++ backend/sd_led/src/lib.rs | 193 +++++++++++++++++++++ backend/sd_led/src/raw_io.rs | 51 ++++++ backend/src/persist/gpu.rs | 4 + backend/src/settings/generic/gpu.rs | 16 ++ backend/src/settings/generic_amd/gpu.rs | 1 + backend/src/settings/steam_deck/battery.rs | 8 +- backend/src/settings/steam_deck/gpu.rs | 2 + backend/src/settings/steam_deck/util.rs | 108 +----------- backend/src/settings/unknown/gpu.rs | 2 + src/index.tsx | 35 ++-- 17 files changed, 337 insertions(+), 146 deletions(-) create mode 100644 backend/sd_led/Cargo.lock create mode 100644 backend/sd_led/Cargo.toml create mode 100644 backend/sd_led/src/lib.rs create mode 100644 backend/sd_led/src/raw_io.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index bea6ffb..408422c 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -820,7 +820,6 @@ name = "limits_core" version = "3.0.0" dependencies = [ "serde", - "serde_json", ] [[package]] @@ -1069,6 +1068,7 @@ dependencies = [ "log", "regex", "ron", + "sd_led", "serde", "serde_json", "simplelog", @@ -1216,6 +1216,13 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "sd_led" +version = "0.1.0" +dependencies = [ + "log", +] + [[package]] name = "serde" version = "1.0.183" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7fd5273..7b00fd3 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -30,6 +30,7 @@ simplelog = "0.12" limits_core = { version = "3", path = "./limits_core" } regex = "1" libryzenadj = { version = "0.12" } +sd_led = { version = "*", path = "./sd_led" } # ureq's tls feature does not like musl targets ureq = { version = "2", features = ["json", "gzip", "brotli", "charset"], default-features = false, optional = true } @@ -39,6 +40,7 @@ decky = ["usdpl-back/decky"] crankshaft = ["usdpl-back/crankshaft"] encrypt = ["usdpl-back/encrypt"] online = ["ureq"] +experimental = [] dev_stuff = [] [profile.release] diff --git a/backend/limits_core/Cargo.lock b/backend/limits_core/Cargo.lock index 1fa968e..169eb4d 100644 --- a/backend/limits_core/Cargo.lock +++ b/backend/limits_core/Cargo.lock @@ -2,18 +2,11 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "itoa" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" - [[package]] name = "limits_core" version = "3.0.0" dependencies = [ "serde", - "serde_json", ] [[package]] @@ -34,12 +27,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "ryu" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" - [[package]] name = "serde" version = "1.0.166" @@ -60,17 +47,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_json" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" -dependencies = [ - "itoa", - "ryu", - "serde", -] - [[package]] name = "syn" version = "2.0.23" diff --git a/backend/limits_core/Cargo.toml b/backend/limits_core/Cargo.toml index 8f6db2c..bc36c76 100644 --- a/backend/limits_core/Cargo.toml +++ b/backend/limits_core/Cargo.toml @@ -7,4 +7,3 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" diff --git a/backend/limits_srv/Cargo.lock b/backend/limits_srv/Cargo.lock index 9c3cf2d..ca8a790 100644 --- a/backend/limits_srv/Cargo.lock +++ b/backend/limits_srv/Cargo.lock @@ -436,7 +436,6 @@ name = "limits_core" version = "3.0.0" dependencies = [ "serde", - "serde_json", ] [[package]] diff --git a/backend/sd_led/Cargo.lock b/backend/sd_led/Cargo.lock new file mode 100644 index 0000000..0a12fba --- /dev/null +++ b/backend/sd_led/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "sd_led" +version = "0.1.0" +dependencies = [ + "log", +] diff --git a/backend/sd_led/Cargo.toml b/backend/sd_led/Cargo.toml new file mode 100644 index 0000000..0ee424a --- /dev/null +++ b/backend/sd_led/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sd_led" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# logging +log = "0.4" diff --git a/backend/sd_led/src/lib.rs b/backend/sd_led/src/lib.rs new file mode 100644 index 0000000..aae4209 --- /dev/null +++ b/backend/sd_led/src/lib.rs @@ -0,0 +1,193 @@ +//! Rough Rust port of some BatCtrl functionality +//! Original: /usr/share/jupiter_controller_fw_updater/RA_bootloader_updater/linux_host_tools/BatCtrl +//! I do not have access to the source code, so this is my own interpretation of what it does. +//! +//! But also Quanta is based in a place with some questionable copyright practices, so... +pub mod raw_io; + +use std::io::Error; + +pub fn set_led(red_unused: bool, green_aka_white: bool, blue_unused: bool) -> Result { + let payload: u8 = 0x80 + | (red_unused as u8 & 1) + | ((green_aka_white as u8 & 1) << 1) + | ((blue_unused as u8 & 1) << 2); + //log::info!("Payload: {:b}", payload); + raw_io::write2(Setting::LEDStatus as _, payload) +} + +pub fn set(setting: Setting, mode: u8) -> Result { + raw_io::write2(setting as u8, mode) +} + +#[derive(Copy, Clone)] +#[repr(u8)] +pub enum Setting { + CycleCount = 0x32, + ControlBoard = 0x6C, + Charge = 0xA6, + ChargeMode = 0x76, + LEDStatus = 199, + LEDBreathing = 0x63, + FanSpeed = 0x2c, // lower 3 bits seem to not do everything, every other bit increases speed -- 5 total steps, 0xf4 seems to do something similar too + // 0x40 write 0x08 makes LED red + green turn on + // 0x58 write 0x80 shuts off battery power (bms?) + // 0x63 makes blue (0x02) or white (0x01) LED breathing effect + // 0x7a write 0x01, 0x02, or 0x03 turns off display +} + +#[derive(Copy, Clone, Debug)] +#[repr(u8)] +pub enum ControlBoard { + Enable = 0xAA, + Disable = 0xAB, +} + +#[derive(Copy, Clone, Debug)] +#[repr(u8)] +pub enum ChargeMode { + Normal = 0, + Discharge = 0x42, + Idle = 0x45, +} + +#[derive(Copy, Clone)] +#[repr(u8)] +pub enum Charge { + Enable = 0, + Disable = 4, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[allow(dead_code)] + fn led_all_experiment_test() -> Result<(), Error> { + let original = raw_io::write_read(Setting::LEDStatus as _)?; + let sleep_dur = std::time::Duration::from_millis(1000); + for b in 0..0x7F { + let actual = 0x80 | b; + raw_io::write2(Setting::LEDStatus as _, actual)?; + println!("Wrote {actual:#b} to LED byte"); + std::thread::sleep(sleep_dur); + } + raw_io::write2(Setting::LEDStatus as _, original)?; + Ok(()) + } + + #[test] + #[allow(dead_code)] + fn led_singles_experiment_test() -> Result<(), Error> { + let original = raw_io::write_read(Setting::LEDStatus as _)?; + let sleep_dur = std::time::Duration::from_millis(1000); + let mut value = 1; + for _ in 0..std::mem::size_of::()*8 { + let actual = 0x80 | value; + raw_io::write2(Setting::LEDStatus as _, actual)?; + println!("Wrote {actual:#b} to LED byte"); + value = value << 1; + std::thread::sleep(sleep_dur); + } + raw_io::write2(Setting::LEDStatus as _, original)?; + Ok(()) + } + + #[test] + #[allow(dead_code)] + fn led_specify_experiment_test() -> Result<(), Error> { + let mut buffer = String::new(); + println!("LED number(s) to display?"); + std::io::stdin().read_line(&mut buffer)?; + + let mut resultant = 0; + let original = raw_io::write_read(Setting::LEDStatus as _)?; + for word in buffer.split(' ') { + let trimmed_word = word.trim(); + if !trimmed_word.is_empty() { + let value: u8 = trimmed_word.parse().expect("Invalid u8 number"); + let actual = 0x80 | value; + raw_io::wait_ready_for_write()?; + raw_io::write2(Setting::LEDStatus as _, actual)?; + println!("Wrote {actual:#b} to LED byte"); + resultant |= actual; + } + } + println!("Effectively wrote {resultant:#b} to LED byte"); + + println!("Press enter to return to normal"); + std::io::stdin().read_line(&mut buffer)?; + raw_io::write2(Setting::LEDStatus as _, original)?; + Ok(()) + } + + #[test] + #[allow(dead_code)] + fn breath_specify_experiment_test() -> Result<(), Error> { + let mut buffer = String::new(); + println!("LED number(s) to display?"); + std::io::stdin().read_line(&mut buffer)?; + + for word in buffer.split(' ') { + let trimmed_word = word.trim(); + if !trimmed_word.is_empty() { + let value: u8 = trimmed_word.parse().expect("Invalid u8 number"); + let actual = 0x20 | value; + raw_io::wait_ready_for_write()?; + raw_io::write2(0x63, actual)?; + println!("Wrote {actual:#b} to LED breathing byte"); + } + } + + println!("Press enter to return to normal"); + std::io::stdin().read_line(&mut buffer)?; + raw_io::write2(0x63, 0)?; + Ok(()) + } + + #[test] + #[allow(dead_code)] + fn unmapped_ports_experiment_test() -> Result<(), Error> { + let sleep_dur = std::time::Duration::from_millis(10000); + let value = 0xaa; + for addr in 0x63..0x64 { + //raw_io::wait_ready_for_read()?; + //let read = raw_io::write_read(addr)?; + raw_io::wait_ready_for_write()?; + raw_io::write2(addr, value)?; + println!("wrote {value:#b} for {addr:#x} port"); + std::thread::sleep(sleep_dur); + } + //raw_io::write2(Setting::LEDStatus as _, 0)?; + Ok(()) + } + + #[test] + #[allow(dead_code)] + fn write_specify_experiment_test() -> Result<(), Error> { + let mut buffer = String::new(); + println!("Register?"); + std::io::stdin().read_line(&mut buffer)?; + let register: u8 = buffer.trim().parse().expect("Invalid u8 number"); + buffer.clear(); + + println!("Value(s)?"); + std::io::stdin().read_line(&mut buffer)?; + + for word in buffer.split(' ') { + let trimmed_word = word.trim(); + if !trimmed_word.is_empty() { + let value: u8 = trimmed_word.parse().expect("Invalid u8 number"); + raw_io::wait_ready_for_write()?; + raw_io::write2(register, value)?; + println!("Wrote {value:#09b} to {register:#02x} register"); + } + } + + println!("Press enter to clear register"); + std::io::stdin().read_line(&mut buffer)?; + raw_io::write2(register, 0)?; + Ok(()) + } +} diff --git a/backend/sd_led/src/raw_io.rs b/backend/sd_led/src/raw_io.rs new file mode 100644 index 0000000..07d6568 --- /dev/null +++ b/backend/sd_led/src/raw_io.rs @@ -0,0 +1,51 @@ +use std::fs::OpenOptions; +use std::io::{Error, Read, Seek, SeekFrom, Write}; + +#[inline] +pub fn write2(p0: u8, p1: u8) -> Result { + write_to(0x6c, 0x81)?; + wait_ready_for_write()?; + let count0 = write_to(0x68, p0)?; + wait_ready_for_write()?; + let count1 = write_to(0x68, p1)?; + Ok(count0 + count1) +} + +#[inline] +pub fn write_read(p0: u8) -> Result { + write_to(0x6c, 0x81)?; + wait_ready_for_write()?; + write_to(0x68, p0)?; + wait_ready_for_read()?; + read_from(0x68) +} + +pub fn write_to(location: u64, value: u8) -> Result { + let mut file = OpenOptions::new().write(true).open("/dev/port")?; + file.seek(SeekFrom::Start(location))?; + file.write(&[value]) +} + +pub fn read_from(location: u64) -> Result { + let mut file = OpenOptions::new().read(true).open("/dev/port")?; + file.seek(SeekFrom::Start(location))?; + let mut buffer = [0]; + file.read(&mut buffer)?; + Ok(buffer[0]) +} + +pub fn wait_ready_for_write() -> Result<(), Error> { + let mut count = 0; + while count < 0x1ffff && (read_from(0x6c)? & 2) != 0 { + count += 1; + } + Ok(()) +} + +pub fn wait_ready_for_read() -> Result<(), Error> { + let mut count = 0; + while count < 0x1ffff && (read_from(0x6c)? & 1) == 0 { + count += 1; + } + Ok(()) +} diff --git a/backend/src/persist/gpu.rs b/backend/src/persist/gpu.rs index 7755847..a91f9de 100644 --- a/backend/src/persist/gpu.rs +++ b/backend/src/persist/gpu.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; pub struct GpuJson { pub fast_ppt: Option, pub slow_ppt: Option, + pub tdp: Option, + pub tdp_boost: Option, pub clock_limits: Option>, pub slow_memory: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -19,6 +21,8 @@ impl Default for GpuJson { Self { fast_ppt: None, slow_ppt: None, + tdp: None, + tdp_boost: None, clock_limits: None, slow_memory: false, root: None, diff --git a/backend/src/settings/generic/gpu.rs b/backend/src/settings/generic/gpu.rs index e538147..27c4322 100644 --- a/backend/src/settings/generic/gpu.rs +++ b/backend/src/settings/generic/gpu.rs @@ -14,6 +14,8 @@ pub struct Gpu { pub slow_memory: bool, pub fast_ppt: Option, pub slow_ppt: Option, + pub tdp: Option, + pub tdp_boost: Option, pub clock_limits: Option>, pub limits: GenericGpuLimit, sysfs: BasicEntityPath, @@ -70,6 +72,16 @@ impl ProviderBuilder for Gpu { } else { None }, + tdp: if limits.tdp.is_some() { + persistent.tdp + } else { + None + }, + tdp_boost: if limits.tdp_boost.is_some() { + persistent.tdp_boost + } else { + None + }, clock_limits: clock_lims, limits, sysfs: Self::find_card_sysfs(persistent.root) @@ -81,6 +93,8 @@ impl ProviderBuilder for Gpu { slow_memory: false, fast_ppt: None, slow_ppt: None, + tdp: None, + tdp_boost: None, clock_limits: None, limits, sysfs: Self::find_card_sysfs(None::<&'static str>), @@ -94,6 +108,8 @@ impl Into for Gpu { GpuJson { fast_ppt: self.fast_ppt, slow_ppt: self.slow_ppt, + tdp: self.tdp, + tdp_boost: self.tdp_boost, clock_limits: self.clock_limits.map(|x| x.into()), slow_memory: false, root: self.sysfs.root().and_then(|p| p.as_ref().to_str().map(|s| s.to_owned())) diff --git a/backend/src/settings/generic_amd/gpu.rs b/backend/src/settings/generic_amd/gpu.rs index 6e8f833..baf1508 100644 --- a/backend/src/settings/generic_amd/gpu.rs +++ b/backend/src/settings/generic_amd/gpu.rs @@ -17,6 +17,7 @@ fn msg_or_err(output: &mut String, m fn log_capabilities(ryzenadj: &RyzenAdj) { log::info!("RyzenAdj v{}.{}.{}", libryzenadj::libryzenadj_sys::RYZENADJ_REVISION_VER, libryzenadj::libryzenadj_sys::RYZENADJ_MAJOR_VER, libryzenadj::libryzenadj_sys::RYZENADJ_MINIOR_VER); + #[cfg(feature = "experimental")] if let Some(x) = ryzenadj.get_init_table_err() { log::warn!("RyzenAdj table init error: {}", x); } diff --git a/backend/src/settings/steam_deck/battery.rs b/backend/src/settings/steam_deck/battery.rs index 87f2d87..db152be 100644 --- a/backend/src/settings/steam_deck/battery.rs +++ b/backend/src/settings/steam_deck/battery.rs @@ -6,7 +6,7 @@ use sysfuss::capability::attributes; use limits_core::json_v2::GenericBatteryLimit; -use super::util::ChargeMode; +use sd_led::ChargeMode; use crate::api::RangeLimit; use crate::persist::{BatteryEventJson, BatteryJson}; use crate::settings::{TBattery, ProviderBuilder}; @@ -131,7 +131,7 @@ impl EventInstruction { fn set_charge_mode(&self) -> Result<(), SettingError> { if let Some(charge_mode) = self.charge_mode { - super::util::set(super::util::Setting::ChargeMode, charge_mode as _) + sd_led::set(sd_led::Setting::ChargeMode, charge_mode as _) .map_err(|e| SettingError { msg: format!("Failed to set charge mode: {}", e), setting: crate::settings::SettingVariant::Battery, @@ -329,7 +329,7 @@ impl Battery { fn set_charge_mode(&mut self) -> Result<(), SettingError> { if let Some(charge_mode) = self.charge_mode { self.state.charge_mode_set = true; - super::util::set(super::util::Setting::ChargeMode, charge_mode as _) + sd_led::set(sd_led::Setting::ChargeMode, charge_mode as _) .map_err(|e| SettingError { msg: format!("Failed to set charge mode: {}", e), setting: crate::settings::SettingVariant::Battery, @@ -337,7 +337,7 @@ impl Battery { .map(|_| ()) } else if self.state.charge_mode_set { self.state.charge_mode_set = false; - super::util::set(super::util::Setting::ChargeMode, ChargeMode::Normal as _) + sd_led::set(sd_led::Setting::ChargeMode, ChargeMode::Normal as _) .map_err(|e| SettingError { msg: format!("Failed to set charge mode: {}", e), setting: crate::settings::SettingVariant::Battery, diff --git a/backend/src/settings/steam_deck/gpu.rs b/backend/src/settings/steam_deck/gpu.rs index 96316f0..7b06454 100644 --- a/backend/src/settings/steam_deck/gpu.rs +++ b/backend/src/settings/steam_deck/gpu.rs @@ -303,6 +303,8 @@ impl Into for Gpu { GpuJson { fast_ppt: self.fast_ppt, slow_ppt: self.slow_ppt, + tdp: None, + tdp_boost: None, clock_limits: self.clock_limits.map(|x| x.into()), slow_memory: self.slow_memory, root: self.sysfs_card.root().or(self.sysfs_hwmon.root()).and_then(|p| p.as_ref().to_str().map(|r| r.to_owned())) diff --git a/backend/src/settings/steam_deck/util.rs b/backend/src/settings/steam_deck/util.rs index 029e2f3..4362bb2 100644 --- a/backend/src/settings/steam_deck/util.rs +++ b/backend/src/settings/steam_deck/util.rs @@ -1,13 +1,5 @@ -//! Rough Rust port of some BatCtrl functionality -//! Original: /usr/share/jupiter_controller_fw_updater/RA_bootloader_updater/linux_host_tools/BatCtrl -//! I do not have access to the source code, so this is my own interpretation of what it does. -//! -//! But also Quanta is based in a place with some questionable copyright practices, so... #![allow(dead_code)] -use std::fs::OpenOptions; -use std::io::{Error, Read, Seek, SeekFrom, Write}; - pub const JUPITER_HWMON_NAME: &'static str = "jupiter"; pub const STEAMDECK_HWMON_NAME: &'static str = "steamdeck_hwmon"; pub const GPU_HWMON_NAME: &'static str = "amdgpu"; @@ -25,63 +17,6 @@ pub fn card_also_has(card: &dyn sysfuss::SysEntity, extensions: &'static [&'stat .all(|ext| card.as_ref().join(ext).exists()) } -#[inline] -fn write2(p0: u8, p1: u8) -> Result { - write_to(0x6c, 0x81)?; - wait_ready_for_write()?; - let count0 = write_to(0x68, p0)?; - wait_ready_for_write()?; - let count1 = write_to(0x68, p1)?; - Ok(count0 + count1) -} - -fn write_read(p0: u8) -> Result { - write_to(0x6c, 0x81)?; - wait_ready_for_write()?; - write_to(0x68, p0)?; - wait_ready_for_read()?; - read_from(0x68) -} - -fn write_to(location: u64, value: u8) -> Result { - let mut file = OpenOptions::new().write(true).open("/dev/port")?; - file.seek(SeekFrom::Start(location))?; - file.write(&[value]) -} - -fn read_from(location: u64) -> Result { - let mut file = OpenOptions::new().read(true).open("/dev/port")?; - file.seek(SeekFrom::Start(location))?; - let mut buffer = [0]; - file.read(&mut buffer)?; - Ok(buffer[0]) -} - -fn wait_ready_for_write() -> Result<(), Error> { - let mut count = 0; - while count < 0x1ffff && (read_from(0x6c)? & 2) != 0 { - count += 1; - } - Ok(()) -} - -fn wait_ready_for_read() -> Result<(), Error> { - let mut count = 0; - while count < 0x1ffff && (read_from(0x6c)? & 1) == 0 { - count += 1; - } - Ok(()) -} - -pub fn set_led(red_unused: bool, green_aka_white: bool, blue_unused: bool) -> Result { - let payload: u8 = 0x80 - | (red_unused as u8 & 1) - | ((green_aka_white as u8 & 1) << 1) - | ((blue_unused as u8 & 1) << 2); - //log::info!("Payload: {:b}", payload); - write2(Setting::LEDStatus as _, payload) -} - const THINGS: &[u8] = &[ 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, @@ -92,55 +27,20 @@ const THINGS: &[u8] = &[ const TIME_UNIT: std::time::Duration = std::time::Duration::from_millis(200); pub fn flash_led() { - let old_led_state = write_read(Setting::LEDStatus as _) + let led_status = sd_led::Setting::LEDStatus; + let old_led_state = sd_led::raw_io::write_read(led_status as _) .map_err(|e| log::error!("Failed to read LED status: {}", e)); for &code in THINGS { let on = code != 0; - if let Err(e) = set_led(on, on, false) { + if let Err(e) = sd_led::set_led(on, on, false) { log::error!("Thing err: {}", e); } std::thread::sleep(TIME_UNIT); } if let Ok(old_led_state) = old_led_state { log::debug!("Restoring LED state to {:#02b}", old_led_state); - write2(Setting::LEDStatus as _, old_led_state) + sd_led::raw_io::write2(led_status as _, old_led_state) .map_err(|e| log::error!("Failed to restore LED status: {}", e)) .unwrap(); } } - -pub fn set(setting: Setting, mode: u8) -> Result { - write2(setting as u8, mode) -} - -#[derive(Copy, Clone)] -#[repr(u8)] -pub enum Setting { - CycleCount = 0x32, - ControlBoard = 0x6C, - Charge = 0xA6, - ChargeMode = 0x76, - LEDStatus = 199, -} - -#[derive(Copy, Clone, Debug)] -#[repr(u8)] -pub enum ControlBoard { - Enable = 0xAA, - Disable = 0xAB, -} - -#[derive(Copy, Clone, Debug)] -#[repr(u8)] -pub enum ChargeMode { - Normal = 0, - Discharge = 0x42, - Idle = 0x45, -} - -#[derive(Copy, Clone)] -#[repr(u8)] -pub enum Charge { - Enable = 0, - Disable = 4, -} diff --git a/backend/src/settings/unknown/gpu.rs b/backend/src/settings/unknown/gpu.rs index 4e911d4..f7b5b36 100644 --- a/backend/src/settings/unknown/gpu.rs +++ b/backend/src/settings/unknown/gpu.rs @@ -34,6 +34,8 @@ impl Into for Gpu { GpuJson { fast_ppt: None, slow_ppt: None, + tdp: None, + tdp_boost: None, clock_limits: None, slow_memory: false, root: None, diff --git a/src/index.tsx b/src/index.tsx index 2412a10..1ba2b15 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -163,12 +163,20 @@ const reload = function() { backend.resolve(backend.getMessages(null), (messages: backend.Message[]) => { set_value(MESSAGE_LIST, messages) }); }; -// init USDPL WASM and connection to back-end -(async function(){ - await backend.initBackend(); - usdplReady = true; - reload(); // technically this is only a load +const clearHooks = function() { + clearInterval(periodicHook!); + periodicHook = null; + lifetimeHook?.unregister(); + startHook?.unregister(); + endHook?.unregister(); + backend.log(backend.LogLevel.Debug, "Unregistered PowerTools callbacks, so long and thanks for all the fish."); +}; + +const registerCallbacks = function(autoclear: boolean) { + if (autoclear) { + clearHooks(); + } // register Steam callbacks //@ts-ignore lifetimeHook = SteamClient.GameSessions.RegisterForAppLifetimeNotifications((update) => { @@ -203,6 +211,15 @@ const reload = function() { }); backend.log(backend.LogLevel.Debug, "Registered PowerTools callbacks, hello!"); +}; + +// init USDPL WASM and connection to back-end +(async function(){ + await backend.initBackend(); + usdplReady = true; + reload(); // technically this is only a load + + registerCallbacks(true); })(); const periodicals = function() { @@ -334,19 +351,15 @@ export default definePlugin((serverApi: ServerAPI) => { if (now.getDate() == 1 && now.getMonth() == 3) { ico = ; } + registerCallbacks(false); return { title: