From 6335eb818ec3ae1e87b177a90ada94f844ddbc80 Mon Sep 17 00:00:00 2001 From: FliegendeWurst <2012gdwu+github@posteo.de> Date: Thu, 12 Oct 2023 14:42:54 +0200 Subject: [PATCH] Implement measurements view --- Cargo.lock | 54 ++--- Cargo.toml | 6 +- default.nix | 2 +- src/{main.rs => bin/ccs811.rs} | 0 src/bin/display_all.rs | 26 +- src/bin/draw/font_15x30.png | Bin 0 -> 781 bytes src/bin/draw/font_15x30.raw | Bin 0 -> 630 bytes src/bin/draw/measurements.rs | 422 +++++++++++++++++++++++++++++++++ src/bin/draw/mod.rs | 2 + src/bin/main_loop.rs | 114 ++++++++- src/bin/schedule/mod.rs | 19 +- src/lib.rs | 79 +++++- 12 files changed, 645 insertions(+), 79 deletions(-) rename src/{main.rs => bin/ccs811.rs} (100%) create mode 100644 src/bin/draw/font_15x30.png create mode 100644 src/bin/draw/font_15x30.raw create mode 100644 src/bin/draw/measurements.rs create mode 100644 src/bin/draw/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 67d4adb..952addd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,9 +493,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "embedded-graphics" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "750082c65094fbcc4baf9ba31583ce9a8bb7f52cadfb96f6164b1bc7f922f32b" +checksum = "0649998afacf6d575d126d83e68b78c0ab0e00ca2ac7e9b3db11b4cbe8274ef0" dependencies = [ "az", "byteorder", @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "embedded-graphics-core" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b1239db5f3eeb7e33e35bd10bd014e7b2537b17e071f726a09351431337cfa" +checksum = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044" dependencies = [ "az", "byteorder", @@ -526,10 +526,17 @@ dependencies = [ [[package]] name = "embedded-hal" -version = "1.0.0-alpha.5" +version = "1.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a554c04648665230499563ccdfd1fcd719ef2d5a0af54bdc81c5d877fb556db4" +checksum = "129b101ddfee640565f7c07b301a31d95aa21e5acef21a491c307139f5fa4c91" + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0760ec0a3bf76859d5e33f39542af103f157d5b2ecfb00ace56dd461472e3a" dependencies = [ + "embedded-hal 1.0.0-alpha.9", "nb 1.1.0", ] @@ -594,9 +601,9 @@ dependencies = [ [[package]] name = "float-cmp" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] @@ -1062,9 +1069,9 @@ dependencies = [ [[package]] name = "micromath" -version = "1.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4010833aea396656c2f91ee704d51a6f1329ec2ab56ffd00bfd56f7481ea94" +checksum = "39617bc909d64b068dcffd0e3e31679195b5576d0c83fadc52690268cc2b2b55" [[package]] name = "minimal-lexical" @@ -1502,7 +1509,7 @@ dependencies = [ "libc", "linux-embedded-hal", "rand_xoshiro", - "rppal 0.13.1", + "rppal", "rusqlite", "serde", "serde_derive", @@ -1581,26 +1588,13 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rppal" -version = "0.12.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb26758c881b4837b2f4aef569e4251f75388e36b37204e1804ef429c220121c" +checksum = "612e1a22e21f08a246657c6433fe52b773ae43d07c9ef88ccfc433cc8683caba" dependencies = [ "embedded-hal 0.2.7", - "lazy_static", - "libc", - "nb 0.1.3", - "void", -] - -[[package]] -name = "rppal" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c88c9c6248de4d337747b619d8f671055ef48a87dc21b97998833f189a0bbd4f" -dependencies = [ - "embedded-hal 0.2.7", - "embedded-hal 1.0.0-alpha.5", - "lazy_static", + "embedded-hal 1.0.0-alpha.9", + "embedded-hal-nb", "libc", "nb 0.1.3", "spin_sleep", @@ -1838,7 +1832,7 @@ dependencies = [ [[package]] name = "ssd1351" version = "0.3.0" -source = "git+https://github.com/FliegendeWurst/ssd1351-rust?rev=ce3194474355f4433ffe2a63484f263071a0d54e#ce3194474355f4433ffe2a63484f263071a0d54e" +source = "git+https://github.com/FliegendeWurst/ssd1351-rust?rev=3de5be50bd9a59391c669aec8357923a56d121f6#3de5be50bd9a59391c669aec8357923a56d121f6" dependencies = [ "chrono", "display-interface", @@ -1846,7 +1840,7 @@ dependencies = [ "embedded-graphics", "embedded-hal 0.2.7", "linux-embedded-hal", - "rppal 0.12.0", + "rppal", "simple-signal", ] diff --git a/Cargo.toml b/Cargo.toml index fabb02d..3ae7c41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -embedded-graphics = "0.7.1" +embedded-graphics = "0.8.1" linux-embedded-hal = "0.3.0" embedded-hal = "0.2.5" libc = "0.2.98" @@ -18,8 +18,8 @@ image = { version = "0.24.1", optional = true } serde_json = "1.0.79" serde_derive = "1.0.136" serde = "1.0.136" -rppal = { version = "0.13.1", features = ["hal"] } -ssd1351 = { git = "https://github.com/FliegendeWurst/ssd1351-rust", rev = "ce3194474355f4433ffe2a63484f263071a0d54e" } +rppal = { version = "0.14.1", features = ["hal"] } +ssd1351 = { git = "https://github.com/FliegendeWurst/ssd1351-rust", rev = "3de5be50bd9a59391c669aec8357923a56d121f6" } display-interface-spi = "0.4.1" ureq = { version = "2.4.0", default-features = false } winit = { version = "0.28.7", optional = true } diff --git a/default.nix b/default.nix index db6df6f..1f56d5e 100644 --- a/default.nix +++ b/default.nix @@ -9,7 +9,7 @@ rustPlatform.buildRustPackage { cargoLock = { lockFile = ./Cargo.lock; outputHashes = { - "ssd1351-0.3.0" = "sha256-D0gnYbZfSsG/K8BN5N8pmMGtGcqWt7/0gN3UXLRsOlc="; + "ssd1351-0.3.0" = "sha256-DD7+NhYwUwD/xC+7ZUNKdhcfsSCOQ9NVEy9lcS47Q5E="; # "gpio-am2302-rs-1.1.0" = "sha256-tyA/R80LtWIXoVEoxHhkmzy0IsMdMH1Oi3FTQ56XjyQ="; }; }; diff --git a/src/main.rs b/src/bin/ccs811.rs similarity index 100% rename from src/main.rs rename to src/bin/ccs811.rs diff --git a/src/bin/display_all.rs b/src/bin/display_all.rs index 36d20dc..c60c7e3 100644 --- a/src/bin/display_all.rs +++ b/src/bin/display_all.rs @@ -16,39 +16,15 @@ use embedded_graphics::{ Drawable, }; +use raspi_oled::Events; use rppal::{ gpio::Gpio, spi::{Bus, Mode, SlaveSelect, Spi}, }; use rusqlite::Connection; -use serde_derive::Deserialize; -//use ssd1306::{I2CDisplayInterface, Ssd1306, size::DisplaySize128x64, rotation::DisplayRotation, mode::DisplayConfig}; use time::{format_description, Date, OffsetDateTime, PrimitiveDateTime}; use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt, PrimitiveDateTimeExt}; -#[derive(Deserialize)] -struct Events { - events: Vec, - weekly: Vec, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct Event { - name: String, - start_time: String, - end_time: Option, -} - -#[derive(Deserialize)] -struct Weekly { - name: String, - day: i32, - hour: i32, - minute: i32, - duration: i32, -} - #[derive(Clone, Copy, PartialEq, Eq)] enum Status { Unknown, diff --git a/src/bin/draw/font_15x30.png b/src/bin/draw/font_15x30.png new file mode 100644 index 0000000000000000000000000000000000000000..7edadacdf47c9750db6ce7c4a067e06d7152904b GIT binary patch literal 781 zcmV+o1M>WdP)R@u|7c^-}Qd}Gb*Mfr|i&X~~XI&j!1wrrw#MQ+~(M3x9Us7lh z`&;7}8k85+sOF&_oqA zSV+*SlVT!E`za6qsN1;D7MDTeCDZ=_Z9!z~GB*e~bZvU7*vl z?eAmT?wkPrXW&Zj`Ri?9_LKBhPm3P`!`r~cbx%|FfXf|VmKh8_V@PhnQnhSTdi`PfzmvH00006P)t-s z00030|No`gpWOfe0Uk+2K~#9!&Cfkf12GT<;AdH48nknP)tVzvy4bMCp>=7AVvSOy zg9}kORjy%_BCR8eEYd2=*enGHKw_FFeHzUV{;G5!7(77HqOzGOauAik5JqTPw$m_k zGEr0lPw^6nKyPTfyh)-a4>-RcBNMnq+k2lyTQ{oou}_B3+@iJCxvERmK@{j$?foO- z`8kbMZPk(lWR)&e{V&z~6V1&8*L&wUR8`4r4|L@j*<_zr)zRo3m^&m+n;+#syB+AI zQK^$|psImBETL%ZJ-bzVs8dxPt;P4f1h4mrP&BOCIzldbBTAxoMY~KY73HF^_P!`5 zQFV+&uiUW=w21>Kj^`!GE`62nV6us#IxL?{9zkl>Qv;Sx(SJ~1Sh<6-Xk!;500000 LNkvXXu0mjfW=&wI literal 0 HcmV?d00001 diff --git a/src/bin/draw/font_15x30.raw b/src/bin/draw/font_15x30.raw new file mode 100644 index 0000000000000000000000000000000000000000..162da71ae53d97b637b83b48ea683c02602b1981 GIT binary patch literal 630 zcmc)Hu}%Xq3d>vc5JNikHHoGbVwn!n~Ug3M$ujUMDwKjc5-8+M6b%7 literal 0 HcmV?d00001 diff --git a/src/bin/draw/measurements.rs b/src/bin/draw/measurements.rs new file mode 100644 index 0000000..278d7ed --- /dev/null +++ b/src/bin/draw/measurements.rs @@ -0,0 +1,422 @@ +use std::{fs, ops::Sub, sync::atomic::AtomicBool, time::Duration}; + +use embedded_graphics::{ + image::ImageRaw, + mono_font::{ + ascii::{FONT_10X20, FONT_4X6, FONT_5X8, FONT_6X9, FONT_9X15}, + mapping::StrGlyphMapping, + DecorationDimensions, MonoFont, MonoTextStyleBuilder, + }, + pixelcolor::Rgb565, + prelude::*, + primitives::{Primitive, PrimitiveStyleBuilder, Rectangle}, + text::{renderer::CharacterStyle, Text}, + Drawable, +}; +use raspi_oled::Events; +use time::{format_description, Date, OffsetDateTime, PrimitiveDateTime}; + +use crate::{screensaver::Screensaver, Context, ContextDefault, Draw}; +use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt, PrimitiveDateTimeExt}; + +static CLOCK_FONT: MonoFont = MonoFont { + image: ImageRaw::new(include_bytes!("font_15x30.raw"), 165), + glyph_mapping: &StrGlyphMapping::new("0123456789:", 0), + character_size: Size::new(15, 30), + character_spacing: 0, + baseline: 22, + underline: DecorationDimensions::default_underline(30), + strikethrough: DecorationDimensions::default_strikethrough(30), +}; + +#[derive(Debug)] +pub struct Measurements { + drawn: AtomicBool, + mode: MeasurementsMode, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MeasurementsMode { + Default, + Temps, + Events, +} + +impl Default for Measurements { + fn default() -> Self { + Self { + drawn: AtomicBool::new(false), + mode: MeasurementsMode::Default, + } + } +} + +impl Measurements { + pub fn temps() -> Self { + Self { + drawn: AtomicBool::new(false), + mode: MeasurementsMode::Temps, + } + } + + pub fn events() -> Self { + Self { + drawn: AtomicBool::new(false), + mode: MeasurementsMode::Events, + } + } +} + +impl> Screensaver for Measurements { + fn id(&self) -> &'static str { + match self.mode { + MeasurementsMode::Default => "measurements", + MeasurementsMode::Temps => "measurements_temps", + MeasurementsMode::Events => "measurements_events", + } + } + + fn convert_draw(&self) -> Box> { + Box::new(Measurements { + drawn: AtomicBool::new(false), + mode: self.mode, + }) + } +} + +impl> Draw for Measurements { + fn draw_with_ctx(&self, ctx: &ContextDefault, disp: &mut D, rng: &mut crate::Rng) -> Result { + if self.drawn.load(std::sync::atomic::Ordering::Relaxed) { + return Ok(false); + } + let events = fs::read_to_string("events.json").expect("failed to read events.json"); + let events: Events = serde_json::from_str(&events).unwrap(); + let database = ctx.database(); + let database = database.borrow_mut(); + + let (rh, temp): (i64, i64) = database + .query_row( + "SELECT humidity, celsius FROM sensor_readings ORDER BY sensor_readings.time DESC LIMIT 1", + [], + |row| Ok((row.get(0).unwrap(), row.get(1).unwrap())), + ) + .unwrap(); + + let time = OffsetDateTime::now_utc().to_timezone(BERLIN); + + let mut query = database + .prepare("SELECT celsius FROM sensor_readings ORDER BY sensor_readings.time DESC LIMIT 288") + .unwrap(); + let mut temps: Vec = query + .query_map([], |r| Ok(r.get(0))) + .unwrap() + .map(Result::unwrap) + .map(Result::unwrap) + .collect(); + let mut global_min = 1000; + let mut global_max = 0; + let mut vals: Vec<(i32, i32)> = vec![]; + for hour in temps.chunks_mut(6) { + hour.sort(); + let mut min = hour[1]; + let mut max = hour[hour.len() - 2]; + //println!("min {} max {}", min, max); + // sanity check value + if max > 400 { + if vals.is_empty() { + max = min; + } else { + max = vals.last().unwrap().1; + } + min = min.min(max); + } + + global_min = min.min(global_min); + global_max = max.max(global_max); + vals.push((min, max)); + } + + let hour = time.hour(); + let minute = time.minute(); + + let text_style_clock = MonoTextStyleBuilder::new() + .font(&CLOCK_FONT) + .text_color(Rgb565::new(0xff, 0xff, 0xff)) + .build(); + let text_style2 = MonoTextStyleBuilder::new() + .font(&FONT_9X15) + .text_color(Rgb565::new(0xff, 0xff, 0xff)) + .build(); + let mut text_style_6x9 = MonoTextStyleBuilder::new() + .font(&FONT_6X9) + .text_color(Rgb565::new(0xff, 0xff, 0xff)) + .build(); + let text_style_4x6 = MonoTextStyleBuilder::new() + .font(&FONT_4X6) + .text_color(Rgb565::new(0xff, 0xff, 0xff)) + .build(); + let text_style4 = MonoTextStyleBuilder::new() + .font(&FONT_5X8) + .text_color(Rgb565::new(0xff, 0xff, 0xff)) + .build(); + let rect_style = PrimitiveStyleBuilder::new() + .fill_color(Rgb565::new(0xff, 0xff, 0xff)) + .build(); + + //let text = format!("{}.{}% {}.{}°C", rh / 10, rh % 10, temp / 10, temp % 10); + //Text::new(&text, Point::new(0, 10), text_style).draw(disp).unwrap(); + let hour = format!("{:02}", hour); + Text::new(&hour, Point::new(64 - 2, 6 + 20), text_style_clock).draw(disp)?; + let minute = format!("{:02}", minute); + Text::new(&minute, Point::new(64 + 30 + 4, 6 + 20), text_style_clock).draw(disp)?; + Rectangle::new((93, 14).into(), (4, 4).into()) + .into_styled(rect_style) + .draw(disp)?; + Rectangle::new((93, 22).into(), (4, 4).into()) + .into_styled(rect_style) + .draw(disp)?; + + let rh = format!("{:02}", rh / 10); + Text::new(&rh, Point::new(64 + 3, 64 - 4), text_style2).draw(disp)?; + Text::new("%", Point::new(64 + 3 + 18, 64 - 4), text_style_6x9).draw(disp)?; + let temp_int = format!("{:02}", temp / 10); + Text::new(&temp_int, Point::new(64 + 32 + 3, 64 - 4), text_style2).draw(disp)?; + Rectangle::new((64 + 32 + 3 + 18, 64 - 4).into(), (1, 1).into()) + .into_styled(rect_style) + .draw(disp)?; + let temp_fract = format!("{}", temp % 10); + Text::new(&temp_fract, Point::new(64 + 32 + 3 + 18 + 2, 64 - 4), text_style_6x9).draw(disp)?; + + for (x, y) in [ + (118, 49), + (119, 49), + (117, 50), + (117, 51), + (120, 50), + (120, 51), + (118, 52), + (119, 52), + (122, 50), + (122, 51), + (122, 52), + (123, 49), + (124, 49), + (123, 53), + (124, 53), + ] { + Rectangle::new((x, y).into(), (1, 1).into()) + .into_styled(rect_style) + .draw(disp)?; + } + + let x = 0; + let y = 0; + Rectangle::new((x + 2, y + 8).into(), (1, 24).into()) + .into_styled(rect_style) + .draw(disp)?; + Rectangle::new((x + 1, y + 16).into(), (1, 1).into()) + .into_styled(rect_style) + .draw(disp)?; + Rectangle::new((x + 1, y + 28).into(), (1, 1).into()) + .into_styled(rect_style) + .draw(disp)?; + let mut day = time.weekday().number_days_from_monday() as usize; + let days = [ + ("M", "o"), + ("D", "i"), + ("M", "i"), + ("D", "o"), + ("F", "r"), + ("S", "a"), + ("S", "o"), + ]; + for i in 0..5 { + Text::new(days[day].0, (x + 12 * i + 4, y + 6).into(), text_style_6x9).draw(disp)?; + Text::new(days[day].1, (x + 12 * i + 10, y + 6).into(), text_style4).draw(disp)?; + day += 1; + day %= days.len(); + } + let mut bits = vec![]; + // events + let mut all_events = vec![]; + for event in events.weekly { + let mut event_time = time.clone(); + while event_time.weekday().number_days_from_monday() as i32 != event.day { + event_time += Duration::from_secs(24 * 60 * 60); + } + all_events.push(( + event.day, + event.hour, + event.minute, + event.duration, + event.name, + event_time.to_julian_day(), + )); + } + let format = format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]").unwrap(); + for event in events.events { + let dt = PrimitiveDateTime::parse(&event.start_time, &format) + .unwrap() + .assume_timezone(BERLIN) + .unwrap(); + let julian_day = dt.to_julian_day(); + if dt < time { + continue; + } + let duration = if let Some(end_time) = event.end_time.as_ref() { + let dt2 = PrimitiveDateTime::parse(end_time, &format) + .unwrap() + .assume_timezone(BERLIN) + .unwrap(); + (dt2.sub(dt).as_seconds_f32() / 60.0) as i32 + } else { + 30 + }; + all_events.push(( + dt.weekday().number_days_from_monday() as _, + dt.hour() as _, + dt.minute() as _, + duration, + event.name, + julian_day, + )); + } + let today = time.date().to_julian_day(); + let weekday = time.weekday().number_days_from_monday() as i32; + all_events.sort_by_key(|x| (x.5, ((x.0 + 7) - weekday) % 7, x.1, x.2)); + //println!("{:?}", all_events); + let mut time_until_first = None; + let colors = vec![ + Rgb565::new(0xff >> 3, 0xff >> 2, 0x00 >> 3), + Rgb565::new(0xff >> 3, 0x00 >> 2, 0xff >> 3), + Rgb565::new(0x00 >> 3, 0xff >> 2, 0xff >> 3), + Rgb565::new(0xff >> 3, 0x00 >> 2, 0x00 >> 3), + Rgb565::new(0x00 >> 3, 0xff >> 2, 0x00 >> 3), + Rgb565::new(0x00 >> 3, 0x00 >> 2, 0xff >> 3), + Rgb565::new(0xff >> 3, 0xff >> 2, 0xff >> 3), + ]; + for i in 0..5 { + let day = (weekday + i) % 7; + for hour in 0..24 { + for minute in 0..60 { + if minute % 6 != 0 { + continue; + } + + if i == 0 && hour == time.hour() as i32 && minute == (time.minute() as i32 / 6) * 6 { + bits.push((i, hour, minute / 6, Some(Rgb565::new(0xff, 0x00, 0xff)))); + } + + for (event_idx, event) in all_events.iter().enumerate() { + if event.0 != day || event.5 < today || event.5 - today > 4 { + continue; + } + let event_start = event.1 * 60 + event.2; + let event_end = event_start + event.3; + let now = hour * 60 + minute; + let now2 = hour * 60 + minute + 6; + if now2 > event_start && now < event_end { + bits.push((i, hour, minute / 6, colors.get(event_idx).copied())); + } + if time_until_first.is_none() + && (i > 0 + || event.1 > time.hour() as i32 || (event.1 == time.hour() as i32 + && event.2 >= time.minute() as i32)) + { + time_until_first = Some( + ((i * 24 + event.1) * 60 + event.2) * 60 + - (time.hour() as i32 * 60 + time.minute() as i32) * 60, + ); + } + } + } + } + } + for (d, h, m, color) in bits { + // calculate position + let x = x + 4 + d * 12 + m; + let y = y + 8 + h; + disp.fill_solid( + &Rectangle::new((x, y).into(), (1, 1).into()), + color.unwrap_or(Rgb565::new(0xff, 0xff, 0x10)), + )?; + //Rectangle::new((x, y).into(), (1, 1).into()).into_styled(rect_style).draw(disp).unwrap(); + } + if self.mode == MeasurementsMode::Events { + for (i, event) in all_events.iter().take(7).enumerate() { + let text = if event.4.len() > 19 { + &event.4[0..event.4.floor_char_boundary(19)] + } else { + &event.4 + }; + let day = event.0 as usize; + let y = y + 64 + 9 * i as i32 + 5; + if event.5 > today && event.5 - today > 7 { + let dt = Date::from_julian_day(event.5).unwrap(); + Text::new( + &format!("{}.{}.", dt.day(), dt.month() as u8), + (0, y).into(), + text_style_4x6, + ) + .draw(disp)?; + } else { + text_style_6x9.set_text_color(Some(Rgb565::new(0xff, 0xff, 0xff))); + Text::new(days[day].0, (x, y).into(), text_style_6x9).draw(disp)?; + Text::new(days[day].1, (x + 6, y).into(), text_style4).draw(disp)?; + } + text_style_6x9.set_text_color(Some(colors[i])); + Text::new(text, (x + 14, y).into(), text_style_6x9).draw(disp)?; + } + } else if self.mode == MeasurementsMode::Temps { + let diff = global_max - global_min; + let x = 0; + let y = 64; + let scaley = 63; + let scalex = 2; + vals.reverse(); + for (i, (a, b)) in vals.into_iter().enumerate() { + let x = x + i as i32 * scalex; + let y1 = y + (global_max - b) * scaley / diff; + let y2 = y + (global_max - a) * scaley / diff; + let height = y2 - y1 + 1; + let rect = Rectangle::new((x, y1).into(), (scalex as u32, height as u32).into()); + disp.fill_solid(&rect, Rgb565::new(0xff, 0xff, 0xff))?; + } + Text::new( + &format!("{}", global_max as f32 / 10.0), + (100, 64 + 10).into(), + text_style_6x9, + ) + .draw(disp)?; + Text::new( + &format!("{}", global_min as f32 / 10.0), + (100, 64 + 50).into(), + text_style_6x9, + ) + .draw(disp)?; + } + if let Some(secs) = time_until_first { + let days = secs / (24 * 60 * 60); + let hours = secs / (60 * 60) % 24; + let minutes = secs / 60 % 60; + let text = if days > 0 { + String::new() + } else if hours > 0 { + format!("{}h{}m", hours, minutes) + } else if minutes > 0 { + format!("{}m", minutes) + } else { + "?".into() + }; + Text::new(&text, (x + 2, y + 60).into(), text_style2).draw(disp)?; + } + + self.drawn.store(true, std::sync::atomic::Ordering::Relaxed); + + Ok(true) + } + + fn draw(&self, disp: &mut D, rng: &mut crate::Rng) -> Result::Error> { + panic!("draw without ctx"); + } +} diff --git a/src/bin/draw/mod.rs b/src/bin/draw/mod.rs new file mode 100644 index 0000000..e2cb2f9 --- /dev/null +++ b/src/bin/draw/mod.rs @@ -0,0 +1,2 @@ +mod measurements; +pub use measurements::Measurements; diff --git a/src/bin/main_loop.rs b/src/bin/main_loop.rs index 591e898..753afe0 100644 --- a/src/bin/main_loop.rs +++ b/src/bin/main_loop.rs @@ -1,21 +1,24 @@ -#![feature(array_windows)] +#![feature(array_windows, round_char_boundary)] use std::{ cell::RefCell, + rc::Rc, thread, time::{Duration, Instant}, }; use action::Action; use display_interface_spi::SPIInterfaceNoCS; -use embedded_graphics::{pixelcolor::Rgb565, prelude::DrawTarget, Drawable}; +use embedded_graphics::{pixelcolor::Rgb565, prelude::DrawTarget}; use gpiocdev::line::{Bias, EdgeDetection, Value}; use rand_xoshiro::{rand_core::SeedableRng, Xoroshiro128StarStar}; +use raspi_oled::{disable_pwm, enable_pwm, PWM_ON}; use rppal::{ gpio::{Gpio, OutputPin}, hal::Delay, spi::{Bus, Mode, SlaveSelect, Spi}, }; +use rusqlite::Connection; use schedule::Schedule; use screensaver::{Screensaver, TimeDisplay}; use ssd1351::display::display::Ssd1351; @@ -23,6 +26,7 @@ use time::OffsetDateTime; use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt}; mod action; +mod draw; mod schedule; mod screensaver; @@ -43,18 +47,29 @@ pub trait Context { fn do_action(&self, action: Action); fn active_count(&self) -> usize; + + fn database(&self) -> Rc>; + + fn enable_pwm(&self); } -struct ContextDefault> { +pub struct ContextDefault> { screensavers: Vec>>, scheduled: Vec>, active: RefCell>>>, + database: Rc>, } impl> ContextDefault { fn new() -> Self { + let mut screensavers = screensaver::screensavers(); + screensavers.push(Box::new(draw::Measurements::default())); + screensavers.push(Box::new(draw::Measurements::temps())); + screensavers.push(Box::new(draw::Measurements::events())); + let database = Connection::open("sensors.db").expect("failed to open database"); ContextDefault { - screensavers: screensaver::screensavers(), + database: Rc::new(RefCell::new(database)), + screensavers, scheduled: schedule::reminders(), active: RefCell::new(vec![Box::new(TimeDisplay::new())]), } @@ -72,10 +87,11 @@ impl> ContextDefault { } let a = active.last().unwrap(); if !a.expired() { - return a.draw(disp, rng).unwrap_or(true); + return a.draw_with_ctx(self, disp, rng).unwrap_or(true); } drop(active); self.active.borrow_mut().pop(); + disable_pwm().unwrap(); self.loop_iter(disp, rng) } @@ -107,6 +123,14 @@ impl> Context for ContextDefault { fn active_count(&self) -> usize { self.active.borrow().len() } + + fn database(&self) -> Rc> { + self.database.clone() + } + + fn enable_pwm(&self) { + enable_pwm().unwrap(); + } } #[cfg(not(feature = "pc"))] @@ -149,7 +173,7 @@ fn pc_main() { let mut buffer_dirty = true; let mut ctx = ContextDefault::new(); - ctx.do_action(Action::Screensaver("plate")); + ctx.do_action(Action::Screensaver("measurements")); let mut rng = Xoroshiro128StarStar::seed_from_u64(17381); event_loop.run(move |event, _, control_flow| { @@ -209,6 +233,7 @@ fn pc_main() { } buffer.present().unwrap(); buffer_dirty = false; + let _ = disp.buffer.save(format!("/tmp/iter{}.png", iters)); } }, _ => (), @@ -217,6 +242,9 @@ fn pc_main() { } pub trait Draw> { + fn draw_with_ctx(&self, ctx: &ContextDefault, disp: &mut D, rng: &mut Rng) -> Result { + self.draw(disp, rng) + } fn draw(&self, disp: &mut D, rng: &mut Rng) -> Result; fn expired(&self) -> bool { false @@ -237,7 +265,35 @@ fn rpi_main() { disp.reset(&mut rst, &mut Delay).unwrap(); disp.turn_on().unwrap(); + // Init PWM handling + let pwm = thread::spawn(handle_pwm); + main_loop(disp); + + let _ = pwm.join(); +} + +fn handle_pwm() { + let pwm = gpiocdev::Request::builder() + .on_chip("/dev/gpiochip0") + .with_line(12) + .as_output(Value::Inactive) + .request() + .unwrap(); + loop { + thread::sleep(Duration::from_millis(500)); + let on = PWM_ON.load(std::sync::atomic::Ordering::Relaxed); + if !on { + let _ = pwm.set_value(12, Value::Inactive); + continue; + } + for _ in 0..100 { + let _ = pwm.set_value(12, Value::Active); + thread::sleep(Duration::from_millis(1)); + let _ = pwm.set_value(12, Value::Inactive); + thread::sleep(Duration::from_millis(1)); + } + } } fn main_loop(mut disp: Oled) { @@ -278,29 +334,63 @@ fn main_loop(mut disp: Oled) { let e = lines.read_edge_event().unwrap(); last_button = Instant::now(); match e.offset { - 19 => { + 5 => { menu.push(1); }, 6 => { menu.push(2); }, - 5 => { + 19 => { menu.push(3); }, _ => { println!("unknown offset: {}", e.offset); }, } + let mut pop_last = false; + let mut clear = false; match &*menu { - [2] => { - let _ = ctx.pop_action_and_clear(&mut disp); + [1] => { + ctx.do_action(Action::Screensaver("measurements")); }, - [3] => { + [1, 2] => { + let _ = ctx.pop_action_and_clear(&mut disp); + ctx.do_action(Action::Screensaver("measurements_temps")); + pop_last = true; + }, + [1, 3] => { + let _ = ctx.pop_action_and_clear(&mut disp); + ctx.do_action(Action::Screensaver("measurements_events")); + pop_last = true; + }, + [2] => { + if ctx.active_count() > 1 { + let _ = ctx.pop_action_and_clear(&mut disp); + disable_pwm().unwrap(); + let _ = disp.flush(); + } + }, + [3] => {}, + [3, 1] => { + enable_pwm().unwrap(); + pop_last = true; + }, + [3, 2] => { + disable_pwm().unwrap(); + pop_last = true; + }, + [3, 3] => { ctx.do_action(Action::Screensaver("rpi")); + clear = true; }, _ => {}, } - //println!("menu: {menu:?}"); + if pop_last { + menu.pop(); + } + if clear { + menu.clear(); + } } // clean up stale menu selection if !menu.is_empty() && Instant::now().duration_since(last_button).as_secs() >= 10 { diff --git a/src/bin/schedule/mod.rs b/src/bin/schedule/mod.rs index d2413b8..cf82597 100644 --- a/src/bin/schedule/mod.rs +++ b/src/bin/schedule/mod.rs @@ -20,6 +20,7 @@ pub struct Reminder { hour: u8, minute: u8, action: Action, + should_beep: bool, } impl Schedule for Reminder { @@ -28,19 +29,27 @@ impl Schedule for Reminder { } fn execute(&self, ctx: &dyn Context, _time: OffsetDateTime) { + if self.should_beep { + ctx.enable_pwm(); + } ctx.do_action(self.action); } } impl Reminder { - const fn new(hour: u8, minute: u8, action: Action) -> Self { - Reminder { hour, minute, action } + const fn new(hour: u8, minute: u8, action: Action, should_beep: bool) -> Self { + Reminder { + hour, + minute, + action, + should_beep, + } } } -static DUOLINGO: Reminder = Reminder::new(11, 30, Action::Screensaver("duolingo")); -static DUOLINGO_NIGHT: Reminder = Reminder::new(23, 30, Action::Screensaver("duolingo")); -static FOOD: Reminder = Reminder::new(13, 15, Action::Screensaver("plate")); +static DUOLINGO: Reminder = Reminder::new(11, 40, Action::Screensaver("duolingo"), false); +static DUOLINGO_NIGHT: Reminder = Reminder::new(23, 40, Action::Screensaver("duolingo"), false); +static FOOD: Reminder = Reminder::new(13, 15, Action::Screensaver("plate"), false); pub fn reminders() -> Vec> { vec![Box::new(DUOLINGO), Box::new(DUOLINGO_NIGHT), Box::new(FOOD)] diff --git a/src/lib.rs b/src/lib.rs index 6289c23..8c85535 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + sync::atomic::AtomicBool, thread::sleep, time::{self, Duration}, }; @@ -8,7 +9,7 @@ use embedded_graphics::{ pixelcolor::Rgb565, prelude::{OriginDimensions, RgbColor, Size}, }; -use gpiocdev::line::{Bias, EdgeDetection, EdgeKind, Value}; +use gpiocdev::line::{Bias, EdgeKind, Value}; #[cfg(feature = "pc")] use image::{ImageBuffer, Rgb}; @@ -65,21 +66,34 @@ fn read_events(timeout: std::time::Duration) -> Result, Sen .on_chip("/dev/gpiochip0") .with_line(26) .as_input() - .with_edge_detection(EdgeDetection::BothEdges) + //.with_edge_detection(EdgeDetection::BothEdges) + //.with_debounce_period(Duration::ZERO) + .with_kernel_event_buffer_size(1024) .with_bias(Bias::PullDown) .request()?; let start = time::Instant::now(); + let mut last_value = Value::Active; let mut events = Vec::with_capacity(81); while start.elapsed() < timeout && events.len() < 81 { + let new_value = input.value(26)?; + if new_value != last_value { + match new_value { + Value::Inactive => events.push((start.elapsed().as_micros() as u64, EdgeKind::Falling)), + Value::Active => events.push((start.elapsed().as_micros() as u64, EdgeKind::Rising)), + } + last_value = new_value; + } + /* if input.wait_edge_event(timeout)? { let event = input.read_edge_event()?; events.push((start.elapsed().as_micros() as u64, event.kind)); } + */ } if events.len() < 81 { - println!("error: only got {} events", events.len()); + println!("error: only got {} events: {:?}", events.len(), events); return Err(SensorError::Timeout); } Ok(events) @@ -174,10 +188,15 @@ pub fn am2302_reading() -> Result<(u16, u16), SensorError> { out.set_value(26, Value::Active)?; sleep(Duration::from_millis(500)); set_max_priority(); + out.set_value(26, Value::Inactive)?; + sleep(Duration::from_millis(4)); + drop(out); + /* // set low for 20 ms out.set_value(26, Value::Inactive)?; sleep(Duration::from_millis(3)); drop(out); + */ let events = read_events(Duration::from_secs(1)); println!("{:?} {:?}", events, events.as_ref().map(|x| x.len())); @@ -201,3 +220,57 @@ fn set_normal_priority() { libc::sched_setscheduler(0, libc::SCHED_OTHER, (&sched_para) as *const libc::sched_param); } } + +pub fn disable_pwm() -> Result<(), rppal::pwm::Error> { + /* + let pwm = Pwm::new(rppal::pwm::Channel::Pwm0)?; + if pwm.is_enabled()? { + pwm.disable()?; + } + */ + PWM_ON.store(false, std::sync::atomic::Ordering::Relaxed); + Ok(()) +} + +pub fn enable_pwm() -> Result<(), rppal::pwm::Error> { + PWM_ON.store(true, std::sync::atomic::Ordering::Relaxed); + /* + let mut pwm = Pwm::with_period( + rppal::pwm::Channel::Pwm0, + Duration::from_micros(500), + Duration::from_micros(250), + rppal::pwm::Polarity::Normal, + true, + )?; + assert!(pwm.is_enabled()?); + pwm.set_reset_on_drop(false); + */ + Ok(()) +} + +pub static PWM_ON: AtomicBool = AtomicBool::new(false); + +use serde_derive::Deserialize; + +#[derive(Deserialize)] +pub struct Events { + pub events: Vec, + pub weekly: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Event { + pub name: String, + pub start_time: String, + pub end_time: Option, +} + +#[derive(Deserialize)] +pub struct Weekly { + pub name: String, + pub day: i32, + pub hour: i32, + pub minute: i32, + pub duration: i32, +}