Skip to content

Commit f7974ac

Browse files
committed
TRMNL dashboard for suspend intermission screen
1 parent c79c5bf commit f7974ac

File tree

7 files changed

+230
-5
lines changed

7 files changed

+230
-5
lines changed

crates/core/src/context.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::trmnl::TrmnlClient;
12
use crate::view::keyboard::Layout;
23
use std::path::Path;
34
use std::collections::{BTreeMap, VecDeque};
@@ -29,6 +30,7 @@ const INPUT_HISTORY_SIZE: usize = 32;
2930
pub struct Context {
3031
pub fb: Box<dyn Framebuffer>,
3132
pub alarm_manager: Option<AlarmManager>,
33+
pub trmnl_client: Option<TrmnlClient>,
3234
pub display: Display,
3335
pub settings: Settings,
3436
pub library: Library,
@@ -57,7 +59,9 @@ impl Context {
5759
let rng = Xoroshiro128Plus::seed_from_u64(Local::now().timestamp_subsec_nanos() as u64);
5860

5961
let alarm_manager = rtc.map(AlarmManager::new);
60-
Context { fb, alarm_manager, display: Display { dims, rotation },
62+
let trmnl_client = settings.trmnl.is_some().then(|| TrmnlClient::new());
63+
64+
Context { fb, alarm_manager, trmnl_client, display: Display { dims, rotation },
6165
library, settings, fonts, dictionaries: BTreeMap::new(),
6266
keyboard_layouts: BTreeMap::new(), input_history: FxHashMap::default(),
6367
battery, frontlight, lightsensor, notification_index: 0,

crates/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod rtc;
1818
pub mod settings;
1919
pub mod font;
2020
pub mod context;
21+
pub mod trmnl;
2122
pub mod gesture;
2223

2324
pub use rtc::{AlarmType, AlarmManager};

crates/core/src/rtc.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ impl Rtc {
9999

100100
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
101101
pub enum AlarmType {
102+
TrmnlRefresh,
102103
AutoPowerOff,
103104
}
104105

crates/core/src/trmnl.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use crate::settings;
2+
use anyhow::{anyhow, Result};
3+
use image::DynamicImage;
4+
use lazy_static::lazy_static;
5+
use serde::Deserialize;
6+
use std::{
7+
path::PathBuf,
8+
sync::{Arc, Mutex},
9+
time::{Duration, Instant},
10+
};
11+
12+
lazy_static! {
13+
static ref GLOBAL_TRMNL_CLIENT: Arc<Mutex<Option<TrmnlClient>>> = Arc::new(Mutex::new(None));
14+
}
15+
16+
#[derive(Debug, Deserialize)]
17+
struct SetupResponse {
18+
status: u32,
19+
api_key: Option<String>,
20+
}
21+
22+
#[derive(Debug, Deserialize)]
23+
struct DisplayResponse {
24+
status: u32,
25+
image_url: Option<String>,
26+
refresh_rate: Option<u32>,
27+
}
28+
29+
#[derive(Clone)]
30+
pub struct TrmnlClient {
31+
refresh_rate: u32,
32+
next_refresh_time: Option<Instant>,
33+
}
34+
35+
impl TrmnlClient {
36+
pub fn new() -> Self {
37+
Self {
38+
refresh_rate: 1800,
39+
next_refresh_time: None,
40+
}
41+
}
42+
43+
pub fn refresh_rate(&self) -> u32 {
44+
self.refresh_rate
45+
}
46+
47+
fn set_next_refresh_time(&mut self) {
48+
self.next_refresh_time =
49+
Some(Instant::now() + Duration::from_secs(self.refresh_rate as u64));
50+
}
51+
52+
fn setup(&mut self, config: &mut settings::Trmnl) -> Result<()> {
53+
if config.access_key.is_some() {
54+
return Ok(());
55+
}
56+
57+
let setup_response: SetupResponse = ureq::get(&format!("{}/setup", config.api_base))
58+
.header("ID", &config.mac_address)
59+
.call()?
60+
.body_mut()
61+
.read_json()?;
62+
63+
if setup_response.status != 200 {
64+
return Err(anyhow!(
65+
"Setup failed, API provided status: {}",
66+
setup_response.status
67+
));
68+
}
69+
70+
let api_key = setup_response
71+
.api_key
72+
.ok_or_else(|| anyhow!("Missing API key in response"))?;
73+
74+
config.access_key = Some(api_key);
75+
76+
Ok(())
77+
}
78+
79+
fn fetch_display(&mut self, config: &mut settings::Trmnl) -> Result<DynamicImage> {
80+
self.setup(config)?;
81+
82+
let api_key = config
83+
.access_key
84+
.as_ref()
85+
.ok_or_else(|| anyhow!("TRMNL API key not available"))?;
86+
87+
let display_response: DisplayResponse = ureq::get(&format!("{}/display", config.api_base))
88+
.header("ID", &config.mac_address)
89+
.header("Access-Token", api_key)
90+
.header("Refresh-Rate", "1800")
91+
.header("Battery-Voltage", "4.0")
92+
.header("FW-Version", "Plato")
93+
.header("RSSI", "-60")
94+
.call()?
95+
.body_mut()
96+
.read_json()?;
97+
98+
if display_response.status != 0 {
99+
return Err(anyhow!(
100+
"Display fetch failed, API provided status: {}",
101+
display_response.status
102+
));
103+
}
104+
105+
let image_url = display_response
106+
.image_url
107+
.ok_or_else(|| anyhow!("Missing image URL in response"))?;
108+
109+
if let Some(rate) = display_response.refresh_rate {
110+
self.refresh_rate = rate;
111+
}
112+
113+
self.set_next_refresh_time();
114+
115+
let image_data = ureq::get(&image_url).call()?.body_mut().read_to_vec()?;
116+
117+
let image = image::load_from_memory(&image_data)?;
118+
119+
Ok(image)
120+
}
121+
122+
pub fn save_current_display(
123+
&mut self,
124+
rotation: i8,
125+
config: &mut settings::Trmnl,
126+
) -> Option<PathBuf> {
127+
let mut image = match self.fetch_display(config) {
128+
Ok(img) => img,
129+
Err(e) => {
130+
eprintln!("Failed to fetch TRMNL display: {:?}", e);
131+
return None;
132+
}
133+
};
134+
135+
// Always rotate back to landscape upright
136+
match rotation {
137+
3 => image = image.rotate90(),
138+
2 => image = image.rotate180(),
139+
1 => image = image.rotate270(),
140+
_ => {}
141+
}
142+
143+
let temp_dir = std::env::temp_dir();
144+
let path = temp_dir.join("trmnl_display.png");
145+
146+
match image.save(&path) {
147+
Ok(_) => {
148+
println!(
149+
"Saved TRMNL display to: {:?} with given rotation {}",
150+
path, rotation
151+
);
152+
Some(path)
153+
}
154+
Err(e) => {
155+
eprintln!("Failed to save TRMNL display: {:?}", e);
156+
None
157+
}
158+
}
159+
}
160+
}

crates/core/src/view/intermission.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,31 @@ impl Intermission {
5252
halt: kind == IntermKind::PowerOff,
5353
}
5454
}
55+
56+
pub fn trmnl_or_new(rect: Rectangle, kind: IntermKind, context: &mut Context) -> Intermission {
57+
let trmnl_client = match context.trmnl_client.as_mut() {
58+
Some(client) => client,
59+
None => return Intermission::new(rect, kind, context),
60+
};
61+
62+
let trmnl_config = match context.settings.trmnl.as_mut() {
63+
Some(config) => config,
64+
None => return Intermission::new(rect, kind, context),
65+
};
66+
67+
let path = match trmnl_client.save_current_display(context.display.rotation, trmnl_config) {
68+
Some(path) => path,
69+
None => return Intermission::new(rect, kind, context),
70+
};
71+
72+
Intermission {
73+
id: ID_FEEDER.next(),
74+
rect,
75+
children: Vec::new(),
76+
message: Message::Image(path),
77+
halt: kind == IntermKind::PowerOff,
78+
}
79+
}
5580
}
5681

5782
impl View for Intermission {

crates/emulator/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ fn main() -> Result<(), Error> {
376376
Scancode::C => IntermKind::Share,
377377
_ => unreachable!(),
378378
};
379-
let interm = Intermission::new(context.fb.rect(), kind, &context);
379+
let interm = Intermission::trmnl_or_new(context.fb.rect(), kind, &mut context);
380380
rq.add(RenderData::new(interm.id(), *interm.rect(), UpdateMode::Full));
381381
view.children_mut().push(Box::new(interm) as Box<dyn View>);
382382
}

crates/plato/src/app.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const CLOCK_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
6464
const BATTERY_REFRESH_INTERVAL: Duration = Duration::from_secs(299);
6565
const AUTO_SUSPEND_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
6666
const SUSPEND_WAIT_DELAY: Duration = Duration::from_secs(15);
67+
const SUSPEND_WAIT_DELAY_TRMNL: Duration = Duration::from_secs(300);
6768
const PREPARE_SUSPEND_WAIT_DELAY: Duration = Duration::from_secs(3);
6869

6970
struct Task {
@@ -350,7 +351,7 @@ pub fn run() -> Result<(), Error> {
350351
resume(TaskId::Suspend, &mut tasks, view.as_mut(), &tx, &mut rq, &mut context);
351352
} else {
352353
view.handle_event(&Event::Suspend, &tx, &mut bus, &mut rq, &mut context);
353-
let interm = Intermission::new(context.fb.rect(), IntermKind::Suspend, &context);
354+
let interm = Intermission::trmnl_or_new(context.fb.rect(), IntermKind::Suspend, &mut context);
354355
rq.add(RenderData::new(interm.id(), *interm.rect(), UpdateMode::Full));
355356
schedule_task(TaskId::PrepareSuspend, Event::PrepareSuspend,
356357
PREPARE_SUSPEND_WAIT_DELAY, &tx, &mut tasks);
@@ -374,7 +375,7 @@ pub fn run() -> Result<(), Error> {
374375
}
375376

376377
view.handle_event(&Event::Suspend, &tx, &mut bus, &mut rq, &mut context);
377-
let interm = Intermission::new(context.fb.rect(), IntermKind::Suspend, &context);
378+
let interm = Intermission::trmnl_or_new(context.fb.rect(), IntermKind::Suspend, &mut context);
378379
rq.add(RenderData::new(interm.id(), *interm.rect(), UpdateMode::Full));
379380
schedule_task(TaskId::PrepareSuspend, Event::PrepareSuspend,
380381
PREPARE_SUSPEND_WAIT_DELAY, &tx, &mut tasks);
@@ -398,6 +399,22 @@ pub fn run() -> Result<(), Error> {
398399
}
399400
},
400401
DeviceEvent::NetUp => {
402+
if context.settings.trmnl.is_some()
403+
&& tasks.iter().any(|t| t.id == TaskId::Suspend) {
404+
tasks.retain(|task| task.id != TaskId::Suspend);
405+
406+
println!("Network is up, cancelling fallback suspend and preparing TRMNL refresh.");
407+
// destroy and recreate the Interm::Suspend Intermission
408+
if let Some(index) = locate::<Intermission>(view.as_ref()) {
409+
view.children_mut().remove(index);
410+
let new_interm = Intermission::trmnl_or_new(context.fb.rect(), IntermKind::Suspend, &mut context);
411+
rq.add(RenderData::new(new_interm.id(), *new_interm.rect(), UpdateMode::Full));
412+
view.children_mut().push(Box::new(new_interm) as Box<dyn View>);
413+
}
414+
415+
schedule_task(TaskId::PrepareSuspend, Event::PrepareSuspend,
416+
PREPARE_SUSPEND_WAIT_DELAY, &tx, &mut tasks);
417+
}
401418
if tasks.iter().any(|task| task.id == TaskId::PrepareSuspend ||
402419
task.id == TaskId::Suspend) {
403420
continue;
@@ -586,6 +603,11 @@ pub fn run() -> Result<(), Error> {
586603
},
587604
Event::Suspend => {
588605
if let Some(alarm_manager) = context.alarm_manager.as_mut() {
606+
if let Some(trmnl_client) = &context.trmnl_client {
607+
alarm_manager.schedule_alarm(AlarmType::TrmnlRefresh, trmnl_client.refresh_rate() as i64)
608+
.map_err(|e| eprintln!("Can't schedule TRMNL refresh alarm: {:#}.", e))
609+
.ok();
610+
}
589611
if context.settings.auto_power_off > 0.0 && !alarm_manager.is_alarm_scheduled(AlarmType::AutoPowerOff) {
590612
alarm_manager.schedule_alarm(AlarmType::AutoPowerOff, (context.settings.auto_power_off * 86400.0) as i64)
591613
.map_err(|e| eprintln!("Can't schedule auto power off alarm: {:#}.", e))
@@ -612,6 +634,18 @@ pub fn run() -> Result<(), Error> {
612634
Ok(fired_alarms) => {
613635
println!("Alarms fired: {:?}", fired_alarms);
614636

637+
if fired_alarms.contains(&AlarmType::TrmnlRefresh) {
638+
if context.settings.wifi {
639+
println!("TRMNL refresh time reached, enabling Wi-Fi to fetch new image");
640+
Command::new("scripts/wifi-enable.sh").status().ok();
641+
tasks.retain(|task| task.id != TaskId::Suspend);
642+
schedule_task(TaskId::Suspend, Event::Suspend,
643+
SUSPEND_WAIT_DELAY_TRMNL, &tx, &mut tasks);
644+
} else {
645+
println!("TRMNL refresh time reached, but Wi-Fi is disabled. Not enabling Wi-Fi to fetch new image.");
646+
}
647+
}
648+
615649
if fired_alarms.contains(&AlarmType::AutoPowerOff) {
616650
power_off(view.as_mut(), &mut history, &mut updating, &mut context);
617651
exit_status = ExitStatus::PowerOff;
@@ -962,7 +996,7 @@ pub fn run() -> Result<(), Error> {
962996
let seconds = 60.0 * context.settings.auto_suspend;
963997
if inactive_since.elapsed() > Duration::from_secs_f32(seconds) {
964998
view.handle_event(&Event::Suspend, &tx, &mut bus, &mut rq, &mut context);
965-
let interm = Intermission::new(context.fb.rect(), IntermKind::Suspend, &context);
999+
let interm = Intermission::trmnl_or_new(context.fb.rect(), IntermKind::Suspend, &mut context);
9661000
rq.add(RenderData::new(interm.id(), *interm.rect(), UpdateMode::Full));
9671001
schedule_task(TaskId::PrepareSuspend, Event::PrepareSuspend,
9681002
PREPARE_SUSPEND_WAIT_DELAY, &tx, &mut tasks);

0 commit comments

Comments
 (0)