From 191a8006a04603a74f18775dcefed0457eee16f0 Mon Sep 17 00:00:00 2001 From: Albin Chaboissier Date: Sun, 30 Nov 2025 22:13:32 +0100 Subject: [PATCH] graphs --- Cargo.lock | 1 + Cargo.toml | 3 +- assets/co2-icon.bmp | Bin 4278 -> 4278 bytes assets/co2-icon.svg | 4 +- assets/humidity-icon.bmp | Bin 4278 -> 4278 bytes assets/humidity-icon.svg | 6 +- assets/indic-falling.bmp | Bin 538 -> 538 bytes assets/indic-falling.svg | 4 +- assets/indic-rising.bmp | Bin 538 -> 538 bytes assets/indic-rising.svg | 4 +- assets/indic-steady.bmp | Bin 538 -> 538 bytes assets/indic-steady.svg | 4 +- assets/temperature-icon.bmp | Bin 4278 -> 4278 bytes assets/temperature-icon.svg | 6 +- assets/voc-icon.bmp | Bin 4278 -> 4278 bytes assets/voc-icon.svg | 4 +- src/colors.rs | 5 +- src/graph.rs | 255 ++++++++++++++++++++++++++++++++++++ src/main.rs | 52 +++++++- src/views/detail.rs | 57 ++++---- src/views/icon.rs | 5 +- src/views/menu.rs | 8 +- 22 files changed, 362 insertions(+), 56 deletions(-) create mode 100644 src/graph.rs diff --git a/Cargo.lock b/Cargo.lock index c2864ef..a738340 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,6 +229,7 @@ dependencies = [ "embedded-sprites", "heapless 0.9.2", "profont", + "rand", "tinybmp", ] diff --git a/Cargo.toml b/Cargo.toml index 59e9e4f..7b50a33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,11 @@ version = "0.1.0" edition = "2024" [dependencies] -#buoyant = "0.6.0-alpha.0" buoyant = "0.5.3" -#buoyant = {path = "buoyant" } embedded-graphics = "0.8.1" embedded-graphics-simulator = "0.8.0" embedded-sprites = "0.2.0" heapless = "0.9.2" profont = "0.7.0" +rand = "0.9.2" tinybmp = "0.6.0" diff --git a/assets/co2-icon.bmp b/assets/co2-icon.bmp index a74e2133d7232371e0944aa8aef6a37eb703df12..e69df2df6a9bc3385f496c967c6a31297b12f2cb 100644 GIT binary patch literal 4278 zcmeH}F>Dht6o#MAHi$+-kx-T(6=jJk6{L!aE)hdAG)o!E(50Df2p!52hAua8LaLCe zN@bv#)1e*8f`l?dmM(OOgxDC`B@A6!2{Q5Cb1s*Q6Xn_j!4O-Q*zeBYz4z?j=fbm3 zN#ODpe?7b&buIx(kl22R_rKgV7j{kg1@PlGz=KY=P<;Ms_w&`egB@N>9u`s)kyD=Uax(mLX!4{45N zCRSdKm=Q#4{*Q6wyhH6KZDEme!S)5q9(kzK$v{&Qr6NP`$?qLzBrQe*PQ(djXb{V` z8G2p$kz2>izK9ijWME$>4=J3Z{RsVf>DK4f=M_=aXIZ@4`n@l2KJfGsB9AFx?*H6GTaiL#4I;%WN_is X2~WCpRguwe#f_lTLDLNUPZ{8h#`L$K)Y_4y9__>tN=XnqW zkKwu3YPC|&AADQh@(*{+>sYVY9cX*gDn7jwNn36*g6y2|r?}=j(L!`&pD<(mFcAI`vc1Z@R7@}PV@)+nHX4c=G-IaYV)cWE& z)M%A86MAX-$%yXCjzn$Djbq`IXDxlvhXdIG+xJ)r+HUOm*;PrbQz!>Oud=Ko8+F7? zIumWB_JJ&mJvHXj_LSwGY|AUiy!to>uZxqeMDlcYm6Z4UbQ>%WAzpKq`j{JO$Q>Qu z*Rh)_yWS_VE3GnPxavv#`T$fgQG?MKjqOh6UkHAe#RbgQncGh&t24a{-^`9Qjv(72 zF@$dxVJ9NwQdza0u&Sc0`UJ#z?7Dgr*`7>I?9x8P_#`B!X4GhR>=Dilq}~a8Fvo?B zE7;%#R%Hct#L-)1Ykx2bqEQrtvMw-6(X#`Xk$ zY)@Xu;c9$l7HNT%$ML!-TdOpCm-{#DcpdE4?X!o8+|l`} ezw6n;-dAQ(^@#9Bm7LLFe?y0TOkF)>i-GBID+8QMs!NNtzSY5zfUGeYWR zfi=y_bW(SuCJPcHHkXV2`E19Bn#jqs{oePUKR-Wv|J~OP{rjAX-_N4GS~aE)Z3+3M znE&y(YNA_x{t(?fAbPgy?!P_wboTxC?)z^$Pj|lj;y8|ah&B;22ip17BFZV{f=l=ooN`V*zo~cjxrow4)qNDa{7?Lx zt{_$&RmoR8@y2JH+z!C4J#;yo{~irmAWIJe4&^>|BD>j7gF^3EkY!dRE9w%OiOi|_ zQ2$FCwJ>%09tmEP85)X$%ofd9EfKQchFQ9((t6W<>3egy_`JpEdle zfpduCR!umiTR=Z|?&uKoj+}bv=m_E&;^exw1nlGWxL-I1D_6FRv#$k6xkf_xMK znUx#7xORSF=!Km&Jf`NXPO@(A{{XS@c= zmT$r?+q`poG10qq lvb#-M%;>pyx5zSx`d+*{HuR;*sv3Gj>vmzXt=jr5{{m4MK-2&L literal 4278 zcmb7`J#X7E5QashXrT^8grb9%6a+-uL%TF{mn?CdWN7Ek*&=^r?b@YFfY@=6KhngJ zva~;$=xOe<*J9zkfEq_0k*W1TS1lz$@*?v<# z|M8-_%5L@f7X-hb1;Ojb?%w{#v-9tN2On;CUhRDOBX)OpU!I(t^vdh$_4Rf5{`t?X z!Kd?gX1cyVp`Egw$o8+2qd1Kx(KMPwX*`NmhdGICWA|kmFT-3E@Xf_COyge53QcS! zClRO8HF7bFn)rTp8fT!DQf)gEY3#G9o2}$Lvh&+o^T=&~7rQ5~!Zsd0zY1Npsk4<_ zddYt4FT=X_I{U(54ILFjsb?!Wl;bjTP`B>QB5rriK8pFQWq9C-wFAf79Wk#WTgf|6 zbT1|~HCA29blfq!AbZS4b0wBrwT{{NF({{OB{NXnq}i-81@B~_oU%utVg=(2i{F)8 zfu0djPT4cia7adRBJHF1FNl9An<}^q>HOw4wd9J-vuq`4^v%io&E$mT2fK$XyAN!C z2CA=g3VyP?!Lkp4-PPK9C5!>PVxoo->hYdYF@Gp0GAaBr3?oeh1}!iS>fMEoCS2 z5|l5=`x%w|j#UD^glvrQLL9f`6x=n1u=_@9KsHyN30I9Z+ItdD84DI$oXzd$s<9QD z*X&$STf1~8+#cxE4LiR_kwDmacykoVh`mnn3stGi^d_d zbS~^pH{`l6t0%n1uHf9%W2b>aoI2ylrCam)dGKakJyp$SzB=M#D|rjqr4WmU6QyGN zs}Q&ARjoCumXDpteP+`_Xnp*BvRdTX75r>nPcT<#jWjk2k&6-YvL*X`X-44r0J*x? zeC}$}tyjRji}W$u<3^FWx3Lq6Cn8IqwvgLwQYqm6(|ljt8O{3Mu3Mky7m(LA*;YkL v!*&<8o^eL#-N@Z^S87aM>|%{-MpxZ>w9cwsE3_WJ2|JOlY+Gq`{RY8*F9fa} diff --git a/assets/humidity-icon.svg b/assets/humidity-icon.svg index 6ac5f37..d5733b9 100644 --- a/assets/humidity-icon.svg +++ b/assets/humidity-icon.svg @@ -26,8 +26,8 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="9.5273334" - inkscape:cx="19.83766" - inkscape:cy="22.724092" + inkscape:cx="19.890141" + inkscape:cy="22.829053" inkscape:window-width="1916" inkscape:window-height="1032" inkscape:window-x="0" @@ -35,7 +35,7 @@ inkscape:window-maximized="0" inkscape:current-layer="svg1" /> _t*;r4jg5uV($ds`m~H0FnVhy<*O@h9`i)@PK{N|82bt}_;DF2pvLzT! z0HFkm=mdrvKsW(O%mFL{619Mfu7HcJfQnf#TwthBa8Phyr~tAcDjO6v7E3$^!bK8_ z8Vn5}m5K@=J`4gyfnYJ54-#J_z`&ru08;^@6|jn->s<`?iz5e27l?M`0NK$b;K;EU zY|mn#NE6Tw1_qG2CV@nbL?9og4%tMAX)u)#Ga;sd#26SB0d+y#3*m!Eh|3ia0$`RS H2SOhJ)V*)S literal 538 zcmZ?rm11H5gDxOh1H@cFECR%g3<3_t*;r4jg5uV($ds`m~H0FnVhy<*O@h9`i)@PK{N|82gr8hfIvkLMF$ug zN-Hp&U^wBZfGo-|gW(3l4TT8^F+~TE2nZtdGpvA#F|2^71gg5ASmEg4=%82uWPxo` zXmHdxEb$l!7fU#50C^ynXn^=I=%@h_I}GQ8#DQu#5; _t*;r4jg5uV($ds`m~H0FnVhy<*O@h9`i)@PK{N|82Z`;-;mCo+1u_*F z9!oryP(Tq}BmfdyB!CcfO+XVMDw_lxIUp(* z14Wv^W+F^On20b9BnGk@;xZ5q1{JW1F)%EKs{pBlxJHQ+rVB(Xae`c^&f>&*7_9cN zgcGMaIFOi`&agdaW?^Dsdd_@??J+PuKwuTC4m$*{VugqR!;}do#te;BkPIhS_t*;r4jg5uV($ds`m~H0FnVhy<*O@h9`i)@PK{N|82gr8hfWRbyBmo#3 zLML)OmUt|IDz+FX@>pUql4z0uNCX5C`VT`z9!ngCssyQ8B+vwei-0VM%0)mMAu108 zMHYc=N0^2%5n&og3}iRNWgs35CSnzH _t*;r4jg5uV($ds`m~H0FnVhy<*O@h9`i)@PK{N|82Z`;-;mCo+1u_*F z9!oryP(Tq}BmfdyB!Ccfg?41R!c)qClR&0tFZkMlVpnCWcKf&|fHaC;;tX lU_f;X#D7R`RzUa;({Bt6*uns0CMZ;p!wP$tg7hQ84gk+0i!uNJ literal 538 zcmZ?rm11H5gDxOh1H@cFECR%g3<3_t*;r4jg5uV($ds`m~H0FnVhy<*O@h9`i)@PK{N|82gr8hfWRbyBmo#3 zLML)OmUt|IDz+FX@>pUql4z0uNCX5C`VT`z9!ngCssyQeq`;)fr1?kzrgD*lqb0)@ zhp!G_8MZhY0(l^p7%IF4iGZNOTSr4jjz$TH8kp#V&5aTdHDEj#{ZIp&7`k4DEsY>s xEYa<7NYu^1c*$YJHk0SVJYXqbb23}hpRJpjj9obdnv diff --git a/assets/indic-steady.svg b/assets/indic-steady.svg index 4ac30f3..db12000 100644 --- a/assets/indic-steady.svg +++ b/assets/indic-steady.svg @@ -44,7 +44,7 @@ id="layer1" transform="matrix(0.10139507,0,0,0.10139507,4.4930246,0.14783758)"> }*(LYBTC>3nBf!UyS4=pE-f=}DiSp1$|v!}s6ng08Rd`x%~B z?%5M{aSy&G-~1Z2bS;*#e17dTOlaA`_XOsj=#*#?F%?169Lb$lHxOGXv!q z5}6zo8D$q6xNaqTV^nl+a{0orKhCVGHvoGdn8AC21r-B%{`zo?MwAy zINN0Q2C6*?uqCpDyho0(<#rcVwJf=$Ib=z;d@^wZYRo{kGRw?Gd2+m^#-tsw*AC0N@t)Y` z>KJ8m6h|vdC7qo*#$GeP`32obmY$+a^-w0R@O#&b@$?yu@6h~=Jsh}5%;T1!SmSxK z8#?b*PieHXu)IyDdW!4d7vr%1Z2E0%6;)^L*iV3Cr7|(KDG{pdZak zn|!*6uErb%q}vZOHKyshOt+j}bAGXH-5KC;;;G1dR9HudwV7*R+iM#hm*dMv0`xMEsD?b7j{3rr7OjLrIle|=tq%vq`jsMZM90Dko_ vE=pE*H|^s8qhu2MKL_VAN+#QGGq(%whY@nMZCQglZluP*%Xh2ZfBWkX?|`}& literal 4278 zcmds)J!soN6vr=CX=#TNC|y(Ah!R0NxS2e3un?3C9o)^(B|-!|hE6eENnZn=S@P79>E?&j&4`bI)7<&~}^$(xEoXvmlw}0-v-1{Ev92^|HI6gjp&9BE7 z7Z*?8Kl`QZe?EIh!^QJavclih+P^CQ7u#woeP`~iSXd3cm2>5v8qM;W%j(2xn&av^ zQ$l@YG|KTe*^>?nGTdk;7EC&2=xMfpLfqN1G90vxrqTS?t`BF+s6BMM#py}*q+JaK zg9RCa=AT6C2?vKo=`q{Tu5Sd{TUo8V(bggLt6E-vwk>AYhme`b?vXw9Ra;8vNf!^(wARp{i&c((MOQAelA+Pn>?={L46{;ZTZnC5iZP1R2sH03BAaROKxi3# zTt@{m9I+iK4}1?AiDko@!1GUq4ED4P+m^P2Z2Cxy*T*y-YpS1%y3!MU{Zef3Fy8Ho z@hn7yyGNo{U85GwTS_Nb>uf1y)w~sWvn544?~xJX$@t2|ZLao7-$n!RjyT~_lFC>t zKtxUC8dzu)icDgwHE5jVvs&;yA-{bU#i}-w>w%||HMT!v_8{`=*uQj{$6*6b&|p&* zQdZbbMNiCDYw)$|nmjvcUp4Y_9yiDexf)K-mSn3Myu7-GtVuT3AU(T~uEi`VHedI( z^n)zhl=|^jQLGk_I>U}E>Ys{vlX6%3!m35BnANvvfwzDSpNB6$G+V8?UH1qwJUkm* zaR+<0)JOWLJ}h>c*sx_vCox&A!MBkKl6fro&F1?(*`;z7&6%_%wTWj!C+AAPY(4?$ zZg##8S}p&i(s#R?Rw$~IZGK`j;u8T2{FCHb%dwq>(o=eVm!i{XP=3F)vfQ08Ls2$j zo(Pm15xLd%Y++s^Tk!oUIw{>9^U3w4ebB^Ss^PR!mnRBCbLL`h`-;XDl^go@R_53o z^OLwdt03dkc}cAe+Jepe`?1r00v6AW+($*RT9O@5y`0=>Hs52poOA-9y~SDF%{Iq8 zOQj0*?aW1MY%T01ET|9NE?FCAGr`Z78}e+w)0xYPPR;R`w3p|x)BX^TNx#LqnY5W7 z_c@E?|G7=Mt(HA?=6U7%h6T<-^w`tXPOm9FF~+>4T)$1<)cz39O}QU}NessR0?oTi A{r~^~ diff --git a/assets/temperature-icon.svg b/assets/temperature-icon.svg index 2ada07b..57b2e69 100644 --- a/assets/temperature-icon.svg +++ b/assets/temperature-icon.svg @@ -26,8 +26,8 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="10.216728" - inkscape:cx="24.861188" - inkscape:cy="27.748609" + inkscape:cx="24.910128" + inkscape:cy="27.74861" inkscape:window-width="1916" inkscape:window-height="1032" inkscape:window-x="0" @@ -49,7 +49,7 @@ div[style*="background-color: rgb(135, 135, 135)"] { inkscape:label="Layer 1" transform="matrix(2.0390253,0,0,2.0390253,-3.3768322,-2.3378069)"> lw0m>idgJ2*R0)8+sJYZ*FkcV=e+=Ghy4}H}L z+#x3_*ZP{#*w|P&EiFwAh}mY&oXKg+b)8uwrr!vr9YnJrb4J<2E(E%Q-dB?XIh-It zaNH7ASEd(c&)6l%> piwI2Y>ha66*;)HlzLC7IJ${r3O000+Zy3GIp literal 4278 zcmZ?r-6p^Q23lw0m>idgJ2*R0)8+sJYZ*FkcV=e+=Ghy4}H}L z+#x3_*ZP{#*w|P&EiFwAh}mY&oXKg+b)8uwrr!vr9YnJrbAW6WMOwj8voS-K_FeFw+Ym3yT9yBr%xmYAoT68P}AE zpoTXkCSmFwH5*r$td4@kFoM>Emc?C2X$Mw5dtyl&Af2$h3b6+g-q`9(-)xAA_tl8J zxDJ{>`;hV{LN@XovGwIpFdJL>2MgO(NM#=^FCv$Ru(A(CL&`r`td6E{SV$n~;Sna8 zUa0LP4kw5YJ<-HbRblcV=?OJV(Cf=wEO`?-AL3#YQ(q1hvw6_^53qcAItsOaK!~RU V?mwW0HzI9PgEeY4W+>7&OaL189LfLy diff --git a/assets/voc-icon.svg b/assets/voc-icon.svg index 8aec3ee..1c73917 100644 --- a/assets/voc-icon.svg +++ b/assets/voc-icon.svg @@ -24,7 +24,7 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="12.529602" - inkscape:cx="23.624054" + inkscape:cx="23.66396" inkscape:cy="30.687327" inkscape:window-width="1916" inkscape:window-height="1032" @@ -33,7 +33,7 @@ inkscape:window-maximized="0" inkscape:current-layer="svg1" /> f32 { + ((x - x_min) / (x_max - x_min)) * (y_max - y_min) + y_min +} + +const DEFAULT_LUT: [Rgb565; 5] = [ + Rgb565::new(0, 16, 11), + Rgb565::new(11, 20, 17), + Rgb565::new(23, 20, 18), + Rgb565::new(31, 24, 12), + Rgb565::new(31, 41, 0), +]; + +fn rgb565_interpolate(a: Rgb565, b: Rgb565, x: f32) -> Rgb565 { + Rgb565::new( + (a.r() as f32 * (1. - x)) as u8 + (b.r() as f32 * x) as u8, + (a.g() as f32 * (1. - x)) as u8 + (b.g() as f32 * x) as u8, + (a.b() as f32 * (1. - x)) as u8 + (b.b() as f32 * x) as u8, + ) +} + +fn color_lut(mut x: f32, colors: &[Rgb565]) -> Rgb565 { + if x == 1. { + return colors[colors.len() - 1]; + } + + if x == 0. { + return colors[0]; + } + + x *= (colors.len() - 1) as f32; + + let index = x.floor(); + let interp = x - index; + + rgb565_interpolate(colors[index as usize], colors[index as usize + 1], interp) +} + +pub fn graph_data + GetPixel>( + data: &[f32], + target: &mut T, +) { + let min = data.iter().copied().reduce(f32::min).unwrap_or(0.); + let max = data.iter().copied().reduce(f32::max).unwrap_or(0.); + let size = Size::new( + target.bounding_box().size.width, + target.bounding_box().size.height, + ); + + // Draw data as WHITE line + let mut start = Point::new( + 0, + map_float(data[0], min, max, size.height as f32, 0.) as i32, + ); + for (i, x) in data.iter().skip(1).enumerate() { + let point = Point::new( + map_float(i as f32, 0., data.len() as f32 - 1., 0., size.width as f32) as i32, + map_float(*x, min, max, size.height as f32, 0.) as i32, + ); + let _ = Line::new(start, point) + .into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 2)) + .draw(target); + start = point; + } + + for x in 0..size.width { + // Start coloring from up to bottom + let mut met_curve = false; + + for y in 0..size.height { + let position = Point::new(x as i32, y as i32); + let pixel = target.pixel(position).unwrap(); + let height_factor = map_float(y as f32, 0., size.height as f32, 1., 0.); + let height_color = color_lut(height_factor, &DEFAULT_LUT); + + if pixel == Rgb565::WHITE { + let _ = Pixel(position, height_color).draw(target); + met_curve = true; + } else if met_curve && (x as i32 - y as i32) % 7 == 0 { + let _ = Pixel( + position, + rgb565_interpolate(height_color, Rgb565::BLACK, 1. - height_factor), + ) + .draw(target); + } + } + } +} + +pub fn min_indicator + GetPixel>( + data: &[f32], + target: &mut T, +) { + let size = target.bounding_box().size; + let (min_index, min) = data + .iter() + .copied() + .enumerate() + .reduce(|a, b| if a.1 < b.1 { a } else { b }) + .unwrap_or((0, 0.)); + + let min_x = map_float( + min_index as f32, + 0., + data.len() as f32, + 0., + size.width as f32, + ) as i32; + + // let _ = Line::new(Point::new(min_x, 0), Point::new(min_x, size.height as i32)) + // .into_styled(PrimitiveStyle::with_stroke(Rgb565::RED, 1)) + // .draw(target); + for y in 0..size.height { + if (y / 2) % 2 == 0 { + let position = Point::new(min_x, y as i32); + let _ = Pixel( + position, + rgb565_interpolate(Rgb565::RED, Rgb565::BLACK, 0.6), + ) + .draw(target); + + // let position = Point::new(min_x + 1, y as i32); + // let _ = Pixel( + // position, + // rgb565_interpolate(Rgb565::RED, Rgb565::BLACK, 0.6), + // ) + // .draw(target); + } + } + + let minimum_text = "Minimum"; + let font = &PROFONT_10_POINT; + + let text_start = if min_x < (size.width / 2) as i32 { + 5 + } else { + -((minimum_text.len() + 1) as i32 * font.character_size.width as i32 + 3) + }; + + let style = MonoTextStyle::new( + &PROFONT_10_POINT, + rgb565_interpolate(Rgb565::WHITE, Rgb565::BLACK, 0.8), + ); + let _ = Text::new( + minimum_text, + Point::new(min_x + text_start as i32, 10), + style, + ) + .draw(target); + + let value = format!(16; "{:.1}", min).unwrap(); + let _ = Text::new( + value.as_str(), + Point::new( + min_x + text_start as i32, + 10 + font.character_size.height as i32, + ), + style, + ) + .draw(target); +} + +pub fn max_indicator + GetPixel>( + data: &[f32], + target: &mut T, +) { + let size = target.bounding_box().size; + let (max_index, max) = data + .iter() + .copied() + .enumerate() + .reduce(|a, b| if a.1 > b.1 { a } else { b }) + .unwrap_or((0, 0.)); + + let max_x = map_float( + max_index as f32, + 0., + data.len() as f32, + 0., + size.width as f32, + ) as i32; + + for y in 0..size.height { + if (y / 2) % 2 == 0 { + let position = Point::new(max_x, y as i32); + let _ = Pixel( + position, + rgb565_interpolate(Rgb565::GREEN, Rgb565::BLACK, 0.6), + ) + .draw(target); + + // let position = Point::new(max_x + 1, y as i32); + // let _ = Pixel( + // position, + // rgb565_interpolate(Rgb565::GREEN, Rgb565::BLACK, 0.6), + // ) + // .draw(target); + } + } + + let maximum_text = "Maximum"; + let font = &PROFONT_10_POINT; + + let text_start = if max_x < (size.width / 2) as i32 { + 5 + } else { + -((maximum_text.len() + 1) as i32 * font.character_size.width as i32 + 3) + }; + + let style = MonoTextStyle::new( + &PROFONT_10_POINT, + rgb565_interpolate(Rgb565::WHITE, Rgb565::BLACK, 0.8), + ); + let _ = Text::new( + maximum_text, + Point::new( + max_x + text_start as i32, + size.height as i32 - font.character_size.height as i32 * 2, + ), + style, + ) + .draw(target); + + let value = format!(16; "{:.1}", max).unwrap(); + let _ = Text::new( + value.as_str(), + Point::new( + max_x + text_start as i32, + size.height as i32 - font.character_size.height as i32, + ), + style, + ) + .draw(target); +} diff --git a/src/main.rs b/src/main.rs index 6833498..7a5fb47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ //#![feature(unsafe_cell_access)] mod colors; +mod graph; mod images; mod views; @@ -8,7 +9,11 @@ use std::cell::UnsafeCell; use std::mem::MaybeUninit; use buoyant::view::prelude::*; +use embedded_graphics::framebuffer::Framebuffer; +use embedded_graphics::framebuffer::buffer_size; +use embedded_graphics::image::ImageRaw; use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::pixelcolor::raw::LittleEndian; use embedded_graphics::prelude::*; use embedded_graphics_simulator::OutputSettings; use embedded_graphics_simulator::SimulatorDisplay; @@ -16,6 +21,10 @@ use embedded_graphics_simulator::Window; use heapless::format; use tinybmp::Bmp; +use crate::graph::graph_data; +use crate::graph::max_indicator; +use crate::graph::min_indicator; + const BACKGROUND_COLOR: Rgb565 = Rgb565::BLACK; const DEFAULT_COLOR: Rgb565 = Rgb565::WHITE; @@ -23,22 +32,50 @@ fn main() { images::prepare_images(); let mut window = Window::new("Hello World", &OutputSettings::default()); - let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(320, 240)); + let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(240, 240)); display.clear(BACKGROUND_COLOR); - // views::menu::menu_view() - // .as_drawable(display.size(), DEFAULT_COLOR) - // .draw(&mut display) - // .unwrap(); - - views::detail::detailed_view(MenuIndicatorType::Temperature(38.3), Tendency::Steady) + views::menu::menu_view() .as_drawable(display.size(), DEFAULT_COLOR) .draw(&mut display) .unwrap(); + // views::detail::detailed_view(MenuIndicatorType::Temperature(38.3), Tendency::Steady) + // .as_drawable(display.size(), DEFAULT_COLOR) + // .draw(&mut display) + // .unwrap(); + + let mut dummy_data = [0.; 100]; + for i in 0..dummy_data.len() { + dummy_data[i] = rand::random::(); + } + + let smoothed = dummy_data + .windows(30) + .map(|x| x.iter().sum::() / 10.) + .collect::>(); + + let mut fb = Framebuffer::< + Rgb565, + _, + LittleEndian, + 240, + { 240 - 80 }, + { buffer_size::(240, 240) }, + >::new(); + + // graph_data(&smoothed, &mut fb); + // min_indicator(&smoothed, &mut fb); + //max_indicator(&smoothed, &mut fb); + + let img_raw = ImageRaw::::new(fb.data(), 240); + let image = embedded_graphics::image::Image::new(&img_raw, Point::new(0, 60)); + //image.draw(&mut display).unwrap(); + window.show_static(&display); } +#[derive(Clone, Copy)] pub enum MenuIndicatorType { Temperature(f32), Humidity(f32), @@ -77,6 +114,7 @@ impl MenuIndicatorType { } } +#[derive(Clone, Copy)] pub enum Tendency { Rising, Steady, diff --git a/src/views/detail.rs b/src/views/detail.rs index 0bcf0ff..69fdbaf 100644 --- a/src/views/detail.rs +++ b/src/views/detail.rs @@ -6,12 +6,18 @@ use buoyant::view::Text; use buoyant::view::VStack; use buoyant::view::View; use buoyant::view::ViewExt; +use buoyant::view::ZStack; +use buoyant::view::shape::Rectangle; +use buoyant::view::shape::ShapeExt; use embedded_graphics::pixelcolor::Rgb565; use profont::PROFONT_18_POINT; use profont::PROFONT_24_POINT; use crate::MenuIndicatorType; use crate::Tendency; +use crate::colors::BACKGROUND_COLOR; +use crate::colors::FRAME_BACKGROUD_COLOR; +use crate::colors::FRAME_STROKE; use crate::colors::FRAME_STROKE_COLOR; use crate::colors::MAIN_TEXT_COLOR; use crate::colors::SUB_TEXT_COLOR; @@ -20,33 +26,36 @@ use crate::views::icon::icon_box_view; pub fn detailed_view(indicator: MenuIndicatorType, tendency: Tendency) -> impl View { VStack::new(( - // Header - HStack::new(( - icon_box_view(FRAME_STROKE_COLOR, indicator.get_corresponding_icon()), - Spacer::default().flex_frame().with_max_width(10), - tendency_indicator(tendency), - Text::new(indicator.get_value_str(), &PROFONT_24_POINT) - .foreground_color(MAIN_TEXT_COLOR), - Text::new(indicator.get_corresponding_unit_string(), &PROFONT_18_POINT) - .foreground_color(SUB_TEXT_COLOR) + ZStack::new(( + header(indicator, tendency), + Rectangle + .corner_radius(10) + .stroked(FRAME_STROKE) + .foreground_color(BACKGROUND_COLOR) .flex_frame() - .with_infinite_max_height() - .with_vertical_alignment(VerticalAlignment::Bottom) - .with_max_height(25), - Spacer::default(), - Text::new("Temperature", &PROFONT_18_POINT) - .foreground_color(SUB_TEXT_COLOR) - .flex_frame() - .with_infinite_max_height() - .with_vertical_alignment(VerticalAlignment::Bottom) - .with_max_height(25), - Spacer::default().flex_frame().with_max_width(10), + .with_max_height(53), )), // Window - Spacer::default() - .flex_frame() - .with_infinite_max_width() - .with_infinite_max_height(), + Spacer::default(), )) + .with_spacing(2) .with_alignment(HorizontalAlignment::Leading) } + +pub fn header(indicator: MenuIndicatorType, tendency: Tendency) -> impl View { + // Header + HStack::new(( + icon_box_view(FRAME_STROKE_COLOR, indicator.get_corresponding_icon()), + Spacer::default().flex_frame().with_max_width(10), + Spacer::default(), + tendency_indicator(tendency), + Text::new(indicator.get_value_str(), &PROFONT_24_POINT).foreground_color(MAIN_TEXT_COLOR), + Text::new(indicator.get_corresponding_unit_string(), &PROFONT_18_POINT) + .foreground_color(SUB_TEXT_COLOR) + .flex_frame() + .with_infinite_max_height() + .with_vertical_alignment(VerticalAlignment::Bottom) + .with_max_height(25), + Spacer::default(), + )) +} diff --git a/src/views/icon.rs b/src/views/icon.rs index 8d0f5c1..6fc616b 100644 --- a/src/views/icon.rs +++ b/src/views/icon.rs @@ -4,12 +4,15 @@ use buoyant::view::ZStack; use buoyant::view::shape::Rectangle; use embedded_graphics::pixelcolor::Rgb565; +use crate::BACKGROUND_COLOR; use crate::get_image; use crate::images::StaticImage; pub fn icon_box_view(box_color: Rgb565, icon: &'static StaticImage) -> impl View { ZStack::new(( - Rectangle.corner_radius(10).foreground_color(box_color), + Rectangle + .corner_radius(10) + .foreground_color(BACKGROUND_COLOR), buoyant::view::Image::new(get_image!(icon)), )) .flex_frame() diff --git a/src/views/menu.rs b/src/views/menu.rs index 23d7a7a..c30469a 100644 --- a/src/views/menu.rs +++ b/src/views/menu.rs @@ -23,19 +23,19 @@ pub fn menu_view() -> impl View { main_menu_indicator(MenuIndicatorType::Temperature(31.5), Tendency::Falling), main_menu_indicator(MenuIndicatorType::Humidity(36.2), Tendency::Steady), )) - .with_spacing(5), + .with_spacing(2), HStack::new(( main_menu_indicator(MenuIndicatorType::Co2(1329), Tendency::Rising), main_menu_indicator(MenuIndicatorType::Voc(29), Tendency::Falling), )) - .with_spacing(5), + .with_spacing(2), )) - .with_spacing(5) + .with_spacing(2) } fn main_menu_indicator(indicator_type: MenuIndicatorType, tendency: Tendency) -> impl View { Rectangle - .corner_radius(10) + .corner_radius(5) .stroked(FRAME_STROKE) .foreground_color(FRAME_STROKE_COLOR) .background(Alignment::Center, || {