1use core::{fmt, iter};
7
8use crate::{Error as LinkageError, linkage_file, render::Projection};
9use device_envoy_core::{
10 UnwrapInfallible,
11 button::Button,
12 clock_sync::{ClockSync, h12_m_s},
13 cyd::{
14 CydDisplay,
15 display::{
16 CydFrame, DrawItem, Image565Fixed, Image565View, Orientation, tga,
17 tiling::max_rectangle_pixel_count,
18 },
19 },
20};
21use embassy_futures::select::{Either, select};
22use embedded_graphics::{
23 Drawable,
24 mono_font::{MonoFont, MonoTextStyle, ascii::FONT_6X10},
25 pixelcolor::Rgb565,
26 pixelcolor::Rgb888,
27 prelude::{Point, Size},
28 primitives::Rectangle,
29 text::{Alignment, Baseline, Text, TextStyle, TextStyleBuilder},
30};
31use log::info;
32use profont::PROFONT_18_POINT;
33use time::OffsetDateTime;
34
35pub const BACKGROUND_COLOR: Rgb888 = Rgb888::new(3, 7, 14);
39pub const FOREGROUND_COLOR: Rgb888 = Rgb888::new(210, 160, 80);
41pub const ORIENTATION: Orientation = Orientation::Landscape;
43pub const WIFI_STATUS_FONT: MonoFont<'static> = FONT_6X10;
45pub const WIFI_STATUS_RECTANGLE: Rectangle = Rectangle::new(Point::new(256, 5), Size::new(62, 10));
47pub const MAX_FRAME_PIXEL_COUNT: usize =
49 max_rectangle_pixel_count(WIFI_STATUS_RECTANGLE, TIME_RECTANGLE);
50
51const TIME_RECTANGLE: Rectangle = Rectangle::new(Point::new(55, 0), Size::new(200, 22));
54const TIME_COLOR: Rgb888 = Rgb888::new(255, 218, 118); const TIME_FONT: MonoFont<'static> = PROFONT_18_POINT;
56const TIME_TEXT_STYLE: TextStyle = TextStyleBuilder::new()
57 .alignment(Alignment::Center)
58 .baseline(Baseline::Top)
59 .build();
60const TIME_TEXT_CAPACITY: usize = 16;
61const TIME_TEXT_TOP_PADDING: i32 = -1;
62
63const CLOCK_BOUNDS: Rectangle = Rectangle::new(Point::new(50, 20), Size::new(220, 220));
64const BACKGROUND_BITMAP: Image565Fixed<320, 240, { 320 * 240 }> =
65 tga!("../assets/astronomy_window_background.tga").to_565();
66const BACKGROUND_BITMAP_VIEW: Image565View = BACKGROUND_BITMAP.view();
67const PROJECTION: Projection = Projection::top_orthographic(
68 Point::new(160, 130), 1.375, );
71const CLOCK_BACKGROUND_VIEW: Image565View = BACKGROUND_BITMAP.view_rect(CLOCK_BOUNDS);
72const CLOCK_BACKGROUND_BITMAP: DrawItem = DrawItem::Bitmap {
73 view: CLOCK_BACKGROUND_VIEW,
74 top_left: CLOCK_BOUNDS.top_left,
75};
76linkage_file! {
77 clock_linkage {
78 file: "../assets/examples/clock.lb.rs",
79 }
80}
81const LINKAGE: clock_linkage::View = clock_linkage::view();
82
83pub async fn run<CydDisplayDevice, ClockSyncDevice>(
86 display: &mut CydDisplayDevice,
87 clock_sync: &ClockSyncDevice,
88 button: &mut impl Button,
89) -> Result<Exit, Error<CydDisplayDevice::Error>>
90where
91 CydDisplayDevice: CydDisplay,
92 ClockSyncDevice: ClockSync,
93{
94 let background565 = Rgb565::from(BACKGROUND_COLOR);
95 let time_color = Rgb565::from(TIME_COLOR);
96
97 loop {
98 let tick = match select(button.wait_for_press(), clock_sync.wait_for_tick()).await {
100 Either::First(()) => return Ok(Exit::ResetWifi),
101 Either::Second(tick) => tick,
102 };
103 let local_time = &tick.local_time;
104 let time_text = text_12h(local_time)?;
105 info!("tick {}", time_text.as_str());
106
107 let mut time_frame = display.frame_mut(TIME_RECTANGLE);
109 time_frame.fill(background565);
110 Text::with_text_style(
111 time_text.as_str(),
112 TIME_RECTANGLE.top_left
113 + Point::new(TIME_RECTANGLE.size.width as i32 / 2, TIME_TEXT_TOP_PADDING),
114 MonoTextStyle::new(&TIME_FONT, time_color),
115 TIME_TEXT_STYLE,
116 )
117 .draw(&mut time_frame)
118 .unwrap_infallible();
119 time_frame.flush().await.map_err(Error::Flush)?;
120 drop(time_frame);
121
122 let params = linkage_params(local_time);
127 let draw_items_2d = LINKAGE
128 .draw_items_3d(¶ms)?
129 .map(|draw_item_3d| draw_item_3d.project(&PROJECTION));
130
131 display
134 .draw_items::<{ 1 + LINKAGE.draw_item_3d_count() }>(
135 CLOCK_BOUNDS,
136 background565, iter::once(CLOCK_BACKGROUND_BITMAP).chain(draw_items_2d),
138 )
139 .map_err(Error::Flush)?;
140 }
141}
142
143pub async fn splash<CydDisplayDevice>(
145 display: &mut CydDisplayDevice,
146) -> Result<(), Error<CydDisplayDevice::Error>>
147where
148 CydDisplayDevice: CydDisplay,
149{
150 display
151 .fill_contiguous_full(BACKGROUND_BITMAP_VIEW.rgb565_iter())
152 .map_err(Error::Flush)?;
153 Ok(())
154}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub enum Exit {
159 ResetWifi,
161}
162
163#[derive(Debug, derive_more::From)]
169pub enum Error<FlushError> {
170 Linkage(LinkageError),
172 Text(fmt::Error),
174 #[from(ignore)]
176 Flush(FlushError),
177}
178
179fn text_12h(
185 local_time: &OffsetDateTime,
186) -> Result<heapless::String<TIME_TEXT_CAPACITY>, fmt::Error> {
187 let (hour_12, minute, _) = h12_m_s(local_time);
188 let meridiem = if local_time.hour() < 12 { "AM" } else { "PM" };
189 let mut text = heapless::String::new();
190 fmt::write(&mut text, format_args!("{hour_12}:{minute:02} {meridiem}"))?;
191 Ok(text)
192}
193
194fn linkage_params(local_time: &OffsetDateTime) -> [f32; 2] {
195 let (hour_12, minute, second) = h12_m_s(local_time);
196 let second_turn = second as f32 / 60.0;
197 let minute_turn = (minute as f32 + second_turn) / 60.0;
198 let hour = ((hour_12 % 12) as f32 + minute_turn) / 12.0;
199 let face_spin = (((second % 20) as f32) / 20.0 + 0.5) % 1.0;
200 [hour, face_spin]
201}
202
203#[cfg(test)]
204mod tests {
205 use core::cell::Cell;
206
207 use device_envoy_core::button::{__ButtonMonitor, Button};
208 use device_envoy_core::clock_sync::{ClockSync, ClockSyncTick, UnixSeconds};
209 use device_envoy_core::cyd::{CydDisplay, display::CydFrame};
210 use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
211 use futures_executor::block_on;
212 use time::OffsetDateTime;
213
214 use super::{
215 BACKGROUND_COLOR, Exit, FOREGROUND_COLOR, ORIENTATION, WIFI_STATUS_FONT,
216 WIFI_STATUS_RECTANGLE, run, splash,
217 };
218
219 struct FixedClockSync {
222 local_time: OffsetDateTime,
223 }
224
225 impl ClockSync for FixedClockSync {
226 async fn wait_for_tick(&self) -> ClockSyncTick {
227 ClockSyncTick {
228 local_time: self.local_time,
229 since_last_sync: embassy_time::Duration::from_secs(0),
230 }
231 }
232
233 fn now_local(&self) -> OffsetDateTime {
234 self.local_time
235 }
236
237 fn set_offset_minutes(&self, _minutes: i32) {}
238
239 fn offset_minutes(&self) -> i32 {
240 0
241 }
242
243 fn set_tick_interval(&self, _interval: Option<embassy_time::Duration>) {}
244
245 fn set_speed(&self, _speed_multiplier: f32) {}
246
247 fn set_utc_time(&self, _unix_seconds: UnixSeconds) {}
248 }
249
250 struct ImmediateButton;
251
252 impl __ButtonMonitor for ImmediateButton {
253 fn is_pressed_raw(&self) -> bool {
254 false
255 }
256
257 async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
258 }
259
260 impl Button for ImmediateButton {
261 async fn wait_for_press(&mut self) {}
262 }
263
264 #[test]
265 fn boot_requests_wifi_reset_before_rendering_the_next_tick() {
266 let memory_cyd = CydMemory::new(
267 ORIENTATION.size(),
268 BACKGROUND_COLOR,
269 FOREGROUND_COLOR,
270 &WIFI_STATUS_FONT,
271 );
272 let clock_sync = FixedClockSync {
273 local_time: OffsetDateTime::from_unix_timestamp(1_700_003_415)
274 .expect("valid fixed timestamp"),
275 };
276 let mut button = ImmediateButton;
277
278 let result = {
279 let mut display = memory_cyd.display();
280 block_on(run(&mut display, &clock_sync, &mut button))
281 };
282
283 assert_eq!(
284 result.expect("BOOT should be a typed exit"),
285 Exit::ResetWifi
286 );
287 }
288
289 #[test]
290 fn boot_requests_wifi_reset_after_a_rendered_tick() {
291 let mut memory_cyd = CydMemory::new(
292 ORIENTATION.size(),
293 BACKGROUND_COLOR,
294 FOREGROUND_COLOR,
295 &WIFI_STATUS_FONT,
296 );
297 memory_cyd.set_frame_budget(100);
298 let clock_sync = OneTickClockSync {
299 local_time: OffsetDateTime::from_unix_timestamp(1_700_003_415)
300 .expect("valid fixed timestamp"),
301 ticks: Cell::new(0),
302 };
303 let mut button = AfterTickButton {
304 waits: Cell::new(0),
305 };
306
307 let result = {
308 let mut display = memory_cyd.display();
309 block_on(run(&mut display, &clock_sync, &mut button))
310 };
311
312 assert_eq!(
313 result.expect("BOOT should exit after a rendered tick"),
314 Exit::ResetWifi
315 );
316 assert!(memory_cyd.flush_count() > 0);
317 }
318
319 #[test]
320 fn clock_renders_expected_frame() {
321 let mut memory_cyd = CydMemory::new(
322 ORIENTATION.size(),
323 BACKGROUND_COLOR,
324 FOREGROUND_COLOR,
325 &WIFI_STATUS_FONT,
326 );
327 memory_cyd.set_frame_budget(3);
328 let clock_sync = FixedClockSync {
329 local_time: OffsetDateTime::from_unix_timestamp(1_700_003_415)
330 .expect("valid fixed timestamp"),
331 };
332 let mut memory_button = NeverButton;
333
334 {
335 let mut display = memory_cyd.display();
336 block_on(splash(&mut display))
337 .expect("clock splash should draw the static background_bitmap");
338 block_on(
339 display
340 .frame_mut(WIFI_STATUS_RECTANGLE)
341 .clear()
342 .write_text("WiFi: OK")
343 .flush(),
344 )
345 .expect("wifi status frame should flush during setup");
346 }
347
348 let clock_result = {
349 let mut display = memory_cyd.display();
350 block_on(run(&mut display, &clock_sync, &mut memory_button))
351 };
352 clock_result.expect_err("the free-running loop should stop at the frame budget");
353
354 assert_framebuffer_matches_expected_png(
355 &memory_cyd,
356 env!("CARGO_MANIFEST_DIR"),
357 "clock.png",
358 )
359 .expect("rendered frame should match the golden image");
360 }
361
362 struct NeverButton;
363
364 impl __ButtonMonitor for NeverButton {
365 fn is_pressed_raw(&self) -> bool {
366 false
367 }
368
369 async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
370 }
371
372 impl Button for NeverButton {
373 async fn wait_for_press(&mut self) {
374 core::future::pending().await
375 }
376 }
377
378 struct AfterTickButton {
379 waits: Cell<u8>,
380 }
381
382 impl __ButtonMonitor for AfterTickButton {
383 fn is_pressed_raw(&self) -> bool {
384 false
385 }
386
387 async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
388 }
389
390 impl Button for AfterTickButton {
391 async fn wait_for_press(&mut self) {
392 let wait_number = self.waits.get();
393 self.waits.set(wait_number + 1);
394 if wait_number == 0 {
395 core::future::pending().await
396 }
397 }
398 }
399
400 struct OneTickClockSync {
401 local_time: OffsetDateTime,
402 ticks: Cell<u8>,
403 }
404
405 impl ClockSync for OneTickClockSync {
406 async fn wait_for_tick(&self) -> ClockSyncTick {
407 if self.ticks.replace(1) == 0 {
408 ClockSyncTick {
409 local_time: self.local_time,
410 since_last_sync: embassy_time::Duration::from_secs(0),
411 }
412 } else {
413 core::future::pending().await
414 }
415 }
416
417 fn now_local(&self) -> OffsetDateTime {
418 self.local_time
419 }
420
421 fn set_offset_minutes(&self, _minutes: i32) {}
422
423 fn offset_minutes(&self) -> i32 {
424 0
425 }
426
427 fn set_tick_interval(&self, _interval: Option<embassy_time::Duration>) {}
428
429 fn set_speed(&self, _speed_multiplier: f32) {}
430
431 fn set_utc_time(&self, _unix_seconds: UnixSeconds) {}
432 }
433}