From 633872a3a06df712db0efd87d0c2eabb8da5a4cd Mon Sep 17 00:00:00 2001 From: ewired <37567272+ewired@users.noreply.github.com> Date: Sat, 24 May 2025 21:35:44 -0400 Subject: [PATCH 1/5] Cargo dependencies for TRMNL client as fork of upstream --- Cargo.lock | 158 +++++++++++++++++++++++++++++++++++++---- crates/core/Cargo.toml | 2 + 2 files changed, 147 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19c65089..c1cad5b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -151,12 +151,24 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.8.0" @@ -236,6 +248,35 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -339,6 +380,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -613,7 +663,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] @@ -797,6 +847,18 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", +] + [[package]] name = "importer" version = "0.9.44" @@ -901,6 +963,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lockfree-object-pool" version = "0.1.6" @@ -909,9 +977,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lzma-rs" @@ -1059,6 +1127,7 @@ dependencies = [ "flate2", "fxhash", "globset", + "image", "indexmap", "kl-hyphenate", "lazy_static", @@ -1078,6 +1147,7 @@ dependencies = [ "titlecase", "toml", "unicode-normalization", + "ureq", "walkdir", "xi-unicode", "zip", @@ -1288,7 +1358,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.26.11", "windows-registry", ] @@ -1321,10 +1391,11 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustls" -version = "0.23.18" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -1344,18 +1415,19 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -1602,10 +1674,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] @@ -1614,6 +1688,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1772,6 +1856,39 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea" +dependencies = [ + "base64", + "cookie_store", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "ureq-proto", + "utf-8", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ureq-proto" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.4" @@ -1783,6 +1900,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -1921,9 +2044,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.7" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.0", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" dependencies = [ "rustls-pki-types", ] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 25a48e61..fd650360 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -39,3 +39,5 @@ rand_core = "0.6.4" rand_xoshiro = "0.6.0" percent-encoding = "2.3.1" chrono = { version = "0.4.38", features = ["serde", "clock"], default-features = false } +image = { version = "0.25.6", features = ["bmp", "png"], default-features = false } +ureq = { version = "3.0.11", features = ["json"] } From 744b0e3ac8af6c97be268904ad064f8254f6a380 Mon Sep 17 00:00:00 2001 From: ewired <37567272+ewired@users.noreply.github.com> Date: Sat, 24 May 2025 20:11:53 -0400 Subject: [PATCH 2/5] Introduce RTC wake alarm manager abstraction --- crates/core/src/context.rs | 8 ++- crates/core/src/lib.rs | 2 + crates/core/src/rtc.rs | 124 ++++++++++++++++++++++++++++++++++++- crates/plato/src/app.rs | 46 +++++++------- 4 files changed, 149 insertions(+), 31 deletions(-) diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index b6400049..3bdda581 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -19,7 +19,7 @@ use crate::geom::Rectangle; use crate::device::CURRENT_DEVICE; use crate::library::Library; use crate::font::Fonts; -use crate::rtc::Rtc; +use crate::rtc::{Rtc, AlarmManager}; const KEYBOARD_LAYOUTS_DIRNAME: &str = "keyboard-layouts"; const DICTIONARIES_DIRNAME: &str = "dictionaries"; @@ -28,7 +28,7 @@ const INPUT_HISTORY_SIZE: usize = 32; pub struct Context { pub fb: Box, - pub rtc: Option, + pub alarm_manager: Option, pub display: Display, pub settings: Settings, pub library: Library, @@ -55,7 +55,9 @@ impl Context { let dims = fb.dims(); let rotation = CURRENT_DEVICE.transformed_rotation(fb.rotation()); let rng = Xoroshiro128Plus::seed_from_u64(Local::now().timestamp_subsec_nanos() as u64); - Context { fb, rtc, display: Display { dims, rotation }, + + let alarm_manager = rtc.map(AlarmManager::new); + Context { fb, alarm_manager, display: Display { dims, rotation }, library, settings, fonts, dictionaries: BTreeMap::new(), keyboard_layouts: BTreeMap::new(), input_history: FxHashMap::default(), battery, frontlight, lightsensor, notification_index: 0, diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 88896dd7..f9befb07 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -20,6 +20,8 @@ pub mod font; pub mod context; pub mod gesture; +pub use rtc::{AlarmType, AlarmManager}; + pub use anyhow; pub use fxhash; pub use chrono; diff --git a/crates/core/src/rtc.rs b/crates/core/src/rtc.rs index 81971234..b9a411be 100644 --- a/crates/core/src/rtc.rs +++ b/crates/core/src/rtc.rs @@ -4,7 +4,8 @@ use std::path::Path; use std::os::unix::io::AsRawFd; use anyhow::Error; use nix::{ioctl_read, ioctl_write_ptr, ioctl_none}; -use chrono::{Duration, Utc, Datelike, Timelike}; +use chrono::{DateTime, Datelike, Duration, Timelike, Utc}; +use std::collections::BTreeMap; ioctl_read!(rtc_read_alarm, b'p', 0x10, RtcWkalrm); ioctl_write_ptr!(rtc_write_alarm, b'p', 0x0f, RtcWkalrm); @@ -71,8 +72,8 @@ impl Rtc { } } - pub fn set_alarm(&self, days: f32) -> Result { - let wt = Utc::now() + Duration::seconds((86_400.0 * days) as i64); + pub fn set_alarm(&self, duration: Duration) -> Result { + let wt = Utc::now() + duration; let rwa = RtcWkalrm { enabled: 1, pending: 0, @@ -95,3 +96,120 @@ impl Rtc { unsafe { rtc_disable_alarm(self.0.as_raw_fd()).map_err(|e| e.into()) } } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum AlarmType { + AutoPowerOff, +} + +pub struct ScheduledAlarm { + pub alarm_type: AlarmType, + pub wake_time: DateTime, +} + +pub struct AlarmManager { + rtc: Rtc, + scheduled_alarms: BTreeMap, +} + +impl AlarmManager { + pub fn new(rtc: Rtc) -> Self { + AlarmManager { + rtc, + scheduled_alarms: BTreeMap::new(), + } + } + + pub fn schedule_alarm( + &mut self, + alarm_type: AlarmType, + seconds_from_now: i64, + ) -> Result<(), Error> { + let wake_time = Utc::now() + Duration::seconds(seconds_from_now as i64); + self.scheduled_alarms.insert( + alarm_type, + ScheduledAlarm { + alarm_type, + wake_time, + }, + ); + self.update_hardware_alarm()?; + Ok(()) + } + + pub fn cancel_alarm(&mut self, alarm_type: AlarmType) -> Result<(), Error> { + self.scheduled_alarms.remove(&alarm_type); + self.update_hardware_alarm()?; + Ok(()) + } + + fn update_hardware_alarm(&self) -> Result<(), Error> { + if let Some((_, earliest_alarm)) = self + .scheduled_alarms + .iter() + .min_by_key(|(_, alarm)| &alarm.wake_time) + { + let now = Utc::now(); + let duration = earliest_alarm.wake_time.signed_duration_since(now); + if duration.num_seconds() > 0 { + self.rtc.set_alarm(duration)?; + } else { + // If the earliest alarm is in the past or now, disable the hardware alarm + // and let the system wake up naturally or by other means. + // The check_fired_alarms will handle what needs to be done. + self.rtc.disable_alarm()?; + } + } else { + self.rtc.disable_alarm()?; + } + Ok(()) + } + + pub fn is_alarm_scheduled(&self, alarm_type: AlarmType) -> bool { + if let Some(scheduled_alarm) = self.scheduled_alarms.get(&alarm_type) { + scheduled_alarm.wake_time > Utc::now() + } else { + false + } + } + + pub fn check_fired_alarms( + &mut self, + after: DateTime, + before: DateTime, + ) -> Result, Error> { + let mut fired_types = Vec::new(); + let now = Utc::now(); + + // Get the earliest scheduled alarm for duration comparison + if let Some((_, earliest_alarm)) = self + .scheduled_alarms + .iter() + .min_by_key(|(_, alarm)| &alarm.wake_time) + { + let expected_duration = earliest_alarm.wake_time.signed_duration_since(now); + + // Check hardware alarm state + let rwa = self.rtc.alarm()?; + let hardware_alarm_fired = !rwa.enabled() + || (rwa.year() <= 1970 + && ((after - before) - expected_duration).num_seconds().abs() < 3); + + if hardware_alarm_fired { + // Check which logical alarms should fire + let mut to_remove = Vec::new(); + for (alarm_type, scheduled_alarm) in &self.scheduled_alarms { + if (now - scheduled_alarm.wake_time).abs().num_milliseconds() <= 3000 { + fired_types.push(*alarm_type); + to_remove.push(*alarm_type); + } + } + for alarm_type in to_remove { + self.scheduled_alarms.remove(&alarm_type); + } + } + } + self.update_hardware_alarm()?; + Ok(fired_types) + } +} diff --git a/crates/plato/src/app.rs b/crates/plato/src/app.rs index 980ba81e..4e217958 100644 --- a/crates/plato/src/app.rs +++ b/crates/plato/src/app.rs @@ -40,6 +40,7 @@ use plato_core::library::Library; use plato_core::font::Fonts; use plato_core::rtc::Rtc; use plato_core::context::Context; +use plato_core::AlarmType; pub const APP_NAME: &str = "Plato"; const FB_DEVICE: &str = "/dev/fb0"; @@ -584,13 +585,14 @@ pub fn run() -> Result<(), Error> { SUSPEND_WAIT_DELAY, &tx, &mut tasks); }, Event::Suspend => { - if context.settings.auto_power_off > 0.0 { - context.rtc.iter().for_each(|rtc| { - rtc.set_alarm(context.settings.auto_power_off) - .map_err(|e| eprintln!("Can't set alarm: {:#}.", e)) - .ok(); - }); + if let Some(alarm_manager) = context.alarm_manager.as_mut() { + if context.settings.auto_power_off > 0.0 && !alarm_manager.is_alarm_scheduled(AlarmType::AutoPowerOff) { + alarm_manager.schedule_alarm(AlarmType::AutoPowerOff, (context.settings.auto_power_off * 86400.0) as i64) + .map_err(|e| eprintln!("Can't schedule auto power off alarm: {:#}.", e)) + .ok(); + } } + let before = Local::now(); println!("{}", before.format("Went to sleep on %B %-d, %Y at %H:%M:%S.")); Command::new("scripts/suspend.sh") @@ -605,25 +607,19 @@ pub fn run() -> Result<(), Error> { // If the wake is legitimate, the task will be cancelled by `resume`. schedule_task(TaskId::Suspend, Event::Suspend, SUSPEND_WAIT_DELAY, &tx, &mut tasks); - if context.settings.auto_power_off > 0.0 { - let dur = plato_core::chrono::Duration::seconds((86_400.0 * context.settings.auto_power_off) as i64); - if let Some(fired) = context.rtc.as_ref() - .and_then(|rtc| rtc.alarm() - .map_err(|e| eprintln!("Can't get alarm: {:#}", e)) - .map(|rwa| !rwa.enabled() || - (rwa.year() <= 1970 && - ((after - before) - dur).num_seconds().abs() < 3)) - .ok()) { - if fired { - power_off(view.as_mut(), &mut history, &mut updating, &mut context); - exit_status = ExitStatus::PowerOff; - break; - } else { - context.rtc.iter().for_each(|rtc| { - rtc.disable_alarm() - .map_err(|e| eprintln!("Can't disable alarm: {:#}.", e)) - .ok(); - }); + if let Some(alarm_manager) = context.alarm_manager.as_mut() { + match alarm_manager.check_fired_alarms(after.to_utc(), before.to_utc()) { + Ok(fired_alarms) => { + println!("Alarms fired: {:?}", fired_alarms); + + if fired_alarms.contains(&AlarmType::AutoPowerOff) { + power_off(view.as_mut(), &mut history, &mut updating, &mut context); + exit_status = ExitStatus::PowerOff; + break; + } + }, + Err(e) => { + eprintln!("Error checking fired alarms: {:#}.", e); } } } From c79c5bfd2801195836cdfd956b1739cb07916581 Mon Sep 17 00:00:00 2001 From: ewired <37567272+ewired@users.noreply.github.com> Date: Sat, 24 May 2025 20:20:33 -0400 Subject: [PATCH 3/5] TRMNL settings struct --- crates/core/src/settings/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/core/src/settings/mod.rs b/crates/core/src/settings/mod.rs index 2cffab5e..4e580bc8 100644 --- a/crates/core/src/settings/mod.rs +++ b/crates/core/src/settings/mod.rs @@ -55,6 +55,13 @@ impl fmt::Display for ButtonScheme { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Trmnl { + pub api_base: String, + pub mac_address: String, + pub access_key: Option, +} + #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum IntermKind { @@ -114,6 +121,7 @@ pub struct Settings { pub external_urls_queue: Option, #[serde(skip_serializing_if = "Vec::is_empty")] pub libraries: Vec, + pub trmnl: Option, pub intermissions: Intermissions, #[serde(skip_serializing_if = "Vec::is_empty")] pub frontlight_presets: Vec, @@ -537,6 +545,7 @@ impl Default for Settings { auto_power_off: 3.0, time_format: "%H:%M".to_string(), date_format: "%A, %B %-d, %Y".to_string(), + trmnl: None, intermissions: Intermissions { suspend: PathBuf::from(LOGO_SPECIAL_PATH), power_off: PathBuf::from(LOGO_SPECIAL_PATH), From f7974ac6ed5ad807582ac016b3ab7a21392fc103 Mon Sep 17 00:00:00 2001 From: ewired <37567272+ewired@users.noreply.github.com> Date: Sat, 24 May 2025 20:29:56 -0400 Subject: [PATCH 4/5] TRMNL dashboard for suspend intermission screen --- crates/core/src/context.rs | 6 +- crates/core/src/lib.rs | 1 + crates/core/src/rtc.rs | 1 + crates/core/src/trmnl.rs | 160 +++++++++++++++++++++++++++ crates/core/src/view/intermission.rs | 25 +++++ crates/emulator/src/main.rs | 2 +- crates/plato/src/app.rs | 40 ++++++- 7 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 crates/core/src/trmnl.rs diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs index 3bdda581..a43e44be 100644 --- a/crates/core/src/context.rs +++ b/crates/core/src/context.rs @@ -1,3 +1,4 @@ +use crate::trmnl::TrmnlClient; use crate::view::keyboard::Layout; use std::path::Path; use std::collections::{BTreeMap, VecDeque}; @@ -29,6 +30,7 @@ const INPUT_HISTORY_SIZE: usize = 32; pub struct Context { pub fb: Box, pub alarm_manager: Option, + pub trmnl_client: Option, pub display: Display, pub settings: Settings, pub library: Library, @@ -57,7 +59,9 @@ impl Context { let rng = Xoroshiro128Plus::seed_from_u64(Local::now().timestamp_subsec_nanos() as u64); let alarm_manager = rtc.map(AlarmManager::new); - Context { fb, alarm_manager, display: Display { dims, rotation }, + let trmnl_client = settings.trmnl.is_some().then(|| TrmnlClient::new()); + + Context { fb, alarm_manager, trmnl_client, display: Display { dims, rotation }, library, settings, fonts, dictionaries: BTreeMap::new(), keyboard_layouts: BTreeMap::new(), input_history: FxHashMap::default(), battery, frontlight, lightsensor, notification_index: 0, diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index f9befb07..59b14771 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -18,6 +18,7 @@ pub mod rtc; pub mod settings; pub mod font; pub mod context; +pub mod trmnl; pub mod gesture; pub use rtc::{AlarmType, AlarmManager}; diff --git a/crates/core/src/rtc.rs b/crates/core/src/rtc.rs index b9a411be..f249b619 100644 --- a/crates/core/src/rtc.rs +++ b/crates/core/src/rtc.rs @@ -99,6 +99,7 @@ impl Rtc { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum AlarmType { + TrmnlRefresh, AutoPowerOff, } diff --git a/crates/core/src/trmnl.rs b/crates/core/src/trmnl.rs new file mode 100644 index 00000000..1c1a8ffc --- /dev/null +++ b/crates/core/src/trmnl.rs @@ -0,0 +1,160 @@ +use crate::settings; +use anyhow::{anyhow, Result}; +use image::DynamicImage; +use lazy_static::lazy_static; +use serde::Deserialize; +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; + +lazy_static! { + static ref GLOBAL_TRMNL_CLIENT: Arc>> = Arc::new(Mutex::new(None)); +} + +#[derive(Debug, Deserialize)] +struct SetupResponse { + status: u32, + api_key: Option, +} + +#[derive(Debug, Deserialize)] +struct DisplayResponse { + status: u32, + image_url: Option, + refresh_rate: Option, +} + +#[derive(Clone)] +pub struct TrmnlClient { + refresh_rate: u32, + next_refresh_time: Option, +} + +impl TrmnlClient { + pub fn new() -> Self { + Self { + refresh_rate: 1800, + next_refresh_time: None, + } + } + + pub fn refresh_rate(&self) -> u32 { + self.refresh_rate + } + + fn set_next_refresh_time(&mut self) { + self.next_refresh_time = + Some(Instant::now() + Duration::from_secs(self.refresh_rate as u64)); + } + + fn setup(&mut self, config: &mut settings::Trmnl) -> Result<()> { + if config.access_key.is_some() { + return Ok(()); + } + + let setup_response: SetupResponse = ureq::get(&format!("{}/setup", config.api_base)) + .header("ID", &config.mac_address) + .call()? + .body_mut() + .read_json()?; + + if setup_response.status != 200 { + return Err(anyhow!( + "Setup failed, API provided status: {}", + setup_response.status + )); + } + + let api_key = setup_response + .api_key + .ok_or_else(|| anyhow!("Missing API key in response"))?; + + config.access_key = Some(api_key); + + Ok(()) + } + + fn fetch_display(&mut self, config: &mut settings::Trmnl) -> Result { + self.setup(config)?; + + let api_key = config + .access_key + .as_ref() + .ok_or_else(|| anyhow!("TRMNL API key not available"))?; + + let display_response: DisplayResponse = ureq::get(&format!("{}/display", config.api_base)) + .header("ID", &config.mac_address) + .header("Access-Token", api_key) + .header("Refresh-Rate", "1800") + .header("Battery-Voltage", "4.0") + .header("FW-Version", "Plato") + .header("RSSI", "-60") + .call()? + .body_mut() + .read_json()?; + + if display_response.status != 0 { + return Err(anyhow!( + "Display fetch failed, API provided status: {}", + display_response.status + )); + } + + let image_url = display_response + .image_url + .ok_or_else(|| anyhow!("Missing image URL in response"))?; + + if let Some(rate) = display_response.refresh_rate { + self.refresh_rate = rate; + } + + self.set_next_refresh_time(); + + let image_data = ureq::get(&image_url).call()?.body_mut().read_to_vec()?; + + let image = image::load_from_memory(&image_data)?; + + Ok(image) + } + + pub fn save_current_display( + &mut self, + rotation: i8, + config: &mut settings::Trmnl, + ) -> Option { + let mut image = match self.fetch_display(config) { + Ok(img) => img, + Err(e) => { + eprintln!("Failed to fetch TRMNL display: {:?}", e); + return None; + } + }; + + // Always rotate back to landscape upright + match rotation { + 3 => image = image.rotate90(), + 2 => image = image.rotate180(), + 1 => image = image.rotate270(), + _ => {} + } + + let temp_dir = std::env::temp_dir(); + let path = temp_dir.join("trmnl_display.png"); + + match image.save(&path) { + Ok(_) => { + println!( + "Saved TRMNL display to: {:?} with given rotation {}", + path, rotation + ); + Some(path) + } + Err(e) => { + eprintln!("Failed to save TRMNL display: {:?}", e); + None + } + } + } +} diff --git a/crates/core/src/view/intermission.rs b/crates/core/src/view/intermission.rs index cfdb93e3..6a621a6f 100644 --- a/crates/core/src/view/intermission.rs +++ b/crates/core/src/view/intermission.rs @@ -52,6 +52,31 @@ impl Intermission { halt: kind == IntermKind::PowerOff, } } + + pub fn trmnl_or_new(rect: Rectangle, kind: IntermKind, context: &mut Context) -> Intermission { + let trmnl_client = match context.trmnl_client.as_mut() { + Some(client) => client, + None => return Intermission::new(rect, kind, context), + }; + + let trmnl_config = match context.settings.trmnl.as_mut() { + Some(config) => config, + None => return Intermission::new(rect, kind, context), + }; + + let path = match trmnl_client.save_current_display(context.display.rotation, trmnl_config) { + Some(path) => path, + None => return Intermission::new(rect, kind, context), + }; + + Intermission { + id: ID_FEEDER.next(), + rect, + children: Vec::new(), + message: Message::Image(path), + halt: kind == IntermKind::PowerOff, + } + } } impl View for Intermission { diff --git a/crates/emulator/src/main.rs b/crates/emulator/src/main.rs index 82a1b9f7..51cd9a1b 100644 --- a/crates/emulator/src/main.rs +++ b/crates/emulator/src/main.rs @@ -376,7 +376,7 @@ fn main() -> Result<(), Error> { Scancode::C => IntermKind::Share, _ => unreachable!(), }; - let interm = Intermission::new(context.fb.rect(), kind, &context); + let interm = Intermission::trmnl_or_new(context.fb.rect(), kind, &mut context); rq.add(RenderData::new(interm.id(), *interm.rect(), UpdateMode::Full)); view.children_mut().push(Box::new(interm) as Box); } diff --git a/crates/plato/src/app.rs b/crates/plato/src/app.rs index 4e217958..7db1d8b5 100644 --- a/crates/plato/src/app.rs +++ b/crates/plato/src/app.rs @@ -64,6 +64,7 @@ const CLOCK_REFRESH_INTERVAL: Duration = Duration::from_secs(60); const BATTERY_REFRESH_INTERVAL: Duration = Duration::from_secs(299); const AUTO_SUSPEND_REFRESH_INTERVAL: Duration = Duration::from_secs(60); const SUSPEND_WAIT_DELAY: Duration = Duration::from_secs(15); +const SUSPEND_WAIT_DELAY_TRMNL: Duration = Duration::from_secs(300); const PREPARE_SUSPEND_WAIT_DELAY: Duration = Duration::from_secs(3); struct Task { @@ -350,7 +351,7 @@ pub fn run() -> Result<(), Error> { resume(TaskId::Suspend, &mut tasks, view.as_mut(), &tx, &mut rq, &mut context); } else { view.handle_event(&Event::Suspend, &tx, &mut bus, &mut rq, &mut context); - let interm = Intermission::new(context.fb.rect(), IntermKind::Suspend, &context); + let interm = Intermission::trmnl_or_new(context.fb.rect(), IntermKind::Suspend, &mut context); rq.add(RenderData::new(interm.id(), *interm.rect(), UpdateMode::Full)); schedule_task(TaskId::PrepareSuspend, Event::PrepareSuspend, PREPARE_SUSPEND_WAIT_DELAY, &tx, &mut tasks); @@ -374,7 +375,7 @@ pub fn run() -> Result<(), Error> { } view.handle_event(&Event::Suspend, &tx, &mut bus, &mut rq, &mut context); - let interm = Intermission::new(context.fb.rect(), IntermKind::Suspend, &context); + let interm = Intermission::trmnl_or_new(context.fb.rect(), IntermKind::Suspend, &mut context); rq.add(RenderData::new(interm.id(), *interm.rect(), UpdateMode::Full)); schedule_task(TaskId::PrepareSuspend, Event::PrepareSuspend, PREPARE_SUSPEND_WAIT_DELAY, &tx, &mut tasks); @@ -398,6 +399,22 @@ pub fn run() -> Result<(), Error> { } }, DeviceEvent::NetUp => { + if context.settings.trmnl.is_some() + && tasks.iter().any(|t| t.id == TaskId::Suspend) { + tasks.retain(|task| task.id != TaskId::Suspend); + + println!("Network is up, cancelling fallback suspend and preparing TRMNL refresh."); + // destroy and recreate the Interm::Suspend Intermission + if let Some(index) = locate::(view.as_ref()) { + view.children_mut().remove(index); + let new_interm = Intermission::trmnl_or_new(context.fb.rect(), IntermKind::Suspend, &mut context); + rq.add(RenderData::new(new_interm.id(), *new_interm.rect(), UpdateMode::Full)); + view.children_mut().push(Box::new(new_interm) as Box); + } + + schedule_task(TaskId::PrepareSuspend, Event::PrepareSuspend, + PREPARE_SUSPEND_WAIT_DELAY, &tx, &mut tasks); + } if tasks.iter().any(|task| task.id == TaskId::PrepareSuspend || task.id == TaskId::Suspend) { continue; @@ -586,6 +603,11 @@ pub fn run() -> Result<(), Error> { }, Event::Suspend => { if let Some(alarm_manager) = context.alarm_manager.as_mut() { + if let Some(trmnl_client) = &context.trmnl_client { + alarm_manager.schedule_alarm(AlarmType::TrmnlRefresh, trmnl_client.refresh_rate() as i64) + .map_err(|e| eprintln!("Can't schedule TRMNL refresh alarm: {:#}.", e)) + .ok(); + } if context.settings.auto_power_off > 0.0 && !alarm_manager.is_alarm_scheduled(AlarmType::AutoPowerOff) { alarm_manager.schedule_alarm(AlarmType::AutoPowerOff, (context.settings.auto_power_off * 86400.0) as i64) .map_err(|e| eprintln!("Can't schedule auto power off alarm: {:#}.", e)) @@ -612,6 +634,18 @@ pub fn run() -> Result<(), Error> { Ok(fired_alarms) => { println!("Alarms fired: {:?}", fired_alarms); + if fired_alarms.contains(&AlarmType::TrmnlRefresh) { + if context.settings.wifi { + println!("TRMNL refresh time reached, enabling Wi-Fi to fetch new image"); + Command::new("scripts/wifi-enable.sh").status().ok(); + tasks.retain(|task| task.id != TaskId::Suspend); + schedule_task(TaskId::Suspend, Event::Suspend, + SUSPEND_WAIT_DELAY_TRMNL, &tx, &mut tasks); + } else { + println!("TRMNL refresh time reached, but Wi-Fi is disabled. Not enabling Wi-Fi to fetch new image."); + } + } + if fired_alarms.contains(&AlarmType::AutoPowerOff) { power_off(view.as_mut(), &mut history, &mut updating, &mut context); exit_status = ExitStatus::PowerOff; @@ -962,7 +996,7 @@ pub fn run() -> Result<(), Error> { let seconds = 60.0 * context.settings.auto_suspend; if inactive_since.elapsed() > Duration::from_secs_f32(seconds) { view.handle_event(&Event::Suspend, &tx, &mut bus, &mut rq, &mut context); - let interm = Intermission::new(context.fb.rect(), IntermKind::Suspend, &context); + let interm = Intermission::trmnl_or_new(context.fb.rect(), IntermKind::Suspend, &mut context); rq.add(RenderData::new(interm.id(), *interm.rect(), UpdateMode::Full)); schedule_task(TaskId::PrepareSuspend, Event::PrepareSuspend, PREPARE_SUSPEND_WAIT_DELAY, &tx, &mut tasks); From fbf9c935046e72a69b193988a85c44dea585195c Mon Sep 17 00:00:00 2001 From: ewired <37567272+ewired@users.noreply.github.com> Date: Tue, 10 Jun 2025 20:31:50 -0400 Subject: [PATCH 5/5] Do not overwrite lighting settings on sleep --- crates/plato/src/app.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/plato/src/app.rs b/crates/plato/src/app.rs index 7db1d8b5..5b0d5447 100644 --- a/crates/plato/src/app.rs +++ b/crates/plato/src/app.rs @@ -411,9 +411,11 @@ pub fn run() -> Result<(), Error> { rq.add(RenderData::new(new_interm.id(), *new_interm.rect(), UpdateMode::Full)); view.children_mut().push(Box::new(new_interm) as Box); } + Command::new("scripts/wifi-disable.sh").status().ok(); + context.online = false; - schedule_task(TaskId::PrepareSuspend, Event::PrepareSuspend, - PREPARE_SUSPEND_WAIT_DELAY, &tx, &mut tasks); + schedule_task(TaskId::Suspend, Event::Suspend, + SUSPEND_WAIT_DELAY, &tx, &mut tasks); } if tasks.iter().any(|task| task.id == TaskId::PrepareSuspend || task.id == TaskId::Suspend) {