use core::{array::from_fn, convert::Infallible, fmt};
use crate::{Error as LinkageError, LinkageFixed, Rgb888, linkage_file, render::Projection};
use device_envoy_core::{
UnwrapInfallible,
button::Button,
clock_sync::{ClockSync, h12_m_s},
};
use embassy_futures::select::{Either, select};
use embedded_graphics::{
Drawable,
mono_font::{MonoFont, MonoTextStyle, ascii::FONT_7X13, ascii::FONT_10X20},
pixelcolor::Rgb565,
prelude::{DrawTarget, Point, Size},
primitives::Rectangle,
text::{Alignment, Baseline, Text, TextStyleBuilder},
};
use log::info;
use time::OffsetDateTime;
use device_envoy_core::cyd::{
CydDisplay,
display::{
CydFrame, DrawItem, Image565Fixed, Image888Fixed, MaskFixed, MaskedDrawable, Orientation,
mask_byte_count, tga, tiling::TileGrid,
},
};
pub const BACKGROUND_COLOR: Rgb888 = Rgb888::new(13, 13, 11);
const FIGURE_COLOR: Rgb888 = Rgb888::new(255, 214, 123); pub const FOREGROUND_COLOR: Rgb888 = Rgb888::new(255, 214, 123);
const PLACARD_TEXT_COLOR: Rgb888 = BACKGROUND_COLOR;
linkage_file! {
pirouette {
file: "../assets/mocap/pirouette.lb.rs",
}
}
const STYLE: LinkageFixed<0, 0, 3> = LinkageFixed::start().pen_width(3.5).pen_color(FIGURE_COLOR);
const CLOCK_PARAM_NAMES: &[&str] = &["head_yrotation", "l_shldr_zrotation", "r_shldr_zrotation"];
const LINKAGE_WITH_STYLE: LinkageFixed<
{ pirouette::DOF },
{ pirouette::MARKS },
{ STYLE.step_count() + pirouette::STEP_COUNT - 1 },
> = STYLE.combine(pirouette::view());
const LINKAGE: LinkageFixed<
{ CLOCK_PARAM_NAMES.len() },
{ LINKAGE_WITH_STYLE.mark_count() },
{ LINKAGE_WITH_STYLE.step_count() },
> = LINKAGE_WITH_STYLE
.freeze_param_name::<{ pirouette::DOF - 1 }>("l_shin_yrotation", 57.6)
.retain_param_names(CLOCK_PARAM_NAMES);
const PROJECTION: Projection = Projection::front_orthographic(
Point::new(139, 306), 1.35, );
const BACKGROUND_BITMAP: Image565Fixed<240, 320, { 240 * 320 }> =
tga!("../assets/clock_back.small.tga").to_565();
const HOURS_SIGN_TGA: Image888Fixed<45, 73, { 45 * 73 }> = tga!("../assets/hours.small.tga");
const HOURS_SIGN_BITMAP: Image565Fixed<45, 73, { 45 * 73 }> = HOURS_SIGN_TGA.to_565();
const HOURS_SIGN_MASK: MaskFixed<45, 73, { mask_byte_count(45, 73) }> =
HOURS_SIGN_TGA.to_mask_magenta();
const HOURS_SIGN_ANCHOR_X: f32 = 22.0;
const HOURS_SIGN_VALUE_CENTER: Point = Point::new(22, 50);
const MINUTE_SIGN_TGA: Image888Fixed<45, 77, { 45 * 77 }> = tga!("../assets/minute.small.tga");
const MINUTE_SIGN_BITMAP: Image565Fixed<45, 77, { 45 * 77 }> = MINUTE_SIGN_TGA.to_565();
const MINUTE_SIGN_MASK: MaskFixed<45, 77, { mask_byte_count(45, 77) }> =
MINUTE_SIGN_TGA.to_mask_magenta();
const MINUTE_SIGN_ANCHOR_X: f32 = 22.0;
const MINUTE_SIGN_VALUE_CENTER: Point = Point::new(22, 56);
pub const ORIENTATION: Orientation = Orientation::Portrait;
pub const TOP_FONT: MonoFont<'static> = FONT_7X13;
pub const WIFI_STATUS_RECTANGLE: Rectangle = Rectangle::new(Point::new(6, 6), Size::new(155, 14));
const TIME_RECTANGLE: Rectangle = Rectangle::new(
Point::new(
WIFI_STATUS_RECTANGLE.top_left.x + WIFI_STATUS_RECTANGLE.size.width as i32,
WIFI_STATUS_RECTANGLE.top_left.y,
),
Size::new(
ORIENTATION.width() - WIFI_STATUS_RECTANGLE.size.width,
WIFI_STATUS_RECTANGLE.size.height,
),
);
const FIGURE_Y: u32 = if WIFI_STATUS_RECTANGLE.top_left.y as u32 + WIFI_STATUS_RECTANGLE.size.height
> TIME_RECTANGLE.top_left.y as u32 + TIME_RECTANGLE.size.height
{
WIFI_STATUS_RECTANGLE.top_left.y as u32 + WIFI_STATUS_RECTANGLE.size.height
} else {
TIME_RECTANGLE.top_left.y as u32 + TIME_RECTANGLE.size.height
};
pub const FIGURE_TILE_GRID: TileGrid = TileGrid::new(
Rectangle::new(
Point::new(0, FIGURE_Y as i32),
Size::new(ORIENTATION.width(), ORIENTATION.height() - FIGURE_Y),
),
3,
3,
);
pub async fn run<CydDisplayDevice, ClockSyncDevice>(
display: &mut CydDisplayDevice,
clock_sync: &ClockSyncDevice,
button: &mut impl Button,
) -> Result<Exit, Error<CydDisplayDevice::Error>>
where
CydDisplayDevice: CydDisplay,
ClockSyncDevice: ClockSync,
{
loop {
let tick = match select(button.wait_for_press(), clock_sync.wait_for_tick()).await {
Either::First(()) => return Ok(Exit::ResetWifi),
Either::Second(tick) => tick,
};
let local_time = &tick.local_time;
let (hour_12, minute, _) = h12_m_s(local_time);
info!("tick {}", text_24h(local_time));
display
.frame_mut(TIME_RECTANGLE)
.write_text(&text_12h(local_time))
.flush()
.await
.map_err(Error::Flush)?;
let params = linkage_params(local_time);
let linkage = LINKAGE.view();
let mut draw_items_3d = linkage.draw_items_3d(¶ms)?;
let mut projected_items =
heapless::Vec::<_, { LINKAGE.view().draw_item_3d_count() }>::new();
for draw_item_3d in &mut draw_items_3d {
projected_items
.push(draw_item_3d.project(&PROJECTION))
.map_err(Error::VecOverflow)?;
}
let (hours_anchor_x, hours_anchor_y) = draw_items_3d
.pose_by_mark_name("lMid2")?
.project(&PROJECTION);
let (minute_anchor_x, minute_anchor_y) = draw_items_3d
.pose_by_mark_name("rMid2")?
.project(&PROJECTION);
let hours_top_left = Point::new(
(hours_anchor_x - HOURS_SIGN_ANCHOR_X) as i32,
hours_anchor_y as i32,
);
let minute_top_left = Point::new(
(minute_anchor_x - MINUTE_SIGN_ANCHOR_X) as i32,
minute_anchor_y as i32,
);
display
.for_each_tile(FIGURE_TILE_GRID, |tile| {
BACKGROUND_BITMAP.draw(tile).unwrap_infallible();
for projected_item in &projected_items {
projected_item.draw(tile);
}
HOURS_SIGN_BITMAP
.at(hours_top_left)
.draw_masked(&HOURS_SIGN_MASK, tile)
.unwrap_infallible();
draw_centered_sign_value(
tile,
hours_top_left,
HOURS_SIGN_VALUE_CENTER,
hour_12 as u32,
);
MINUTE_SIGN_BITMAP
.at(minute_top_left)
.draw_masked(&MINUTE_SIGN_MASK, tile)
.unwrap_infallible();
draw_centered_sign_value(
tile,
minute_top_left,
MINUTE_SIGN_VALUE_CENTER,
minute as u32,
);
})
.await
.map_err(Error::Flush)?;
}
}
pub async fn splash<CydDisplayDevice>(
display: &mut CydDisplayDevice,
) -> Result<(), Error<CydDisplayDevice::Error>>
where
CydDisplayDevice: CydDisplay,
{
display
.frame_mut(WIFI_STATUS_RECTANGLE)
.write_text("WiFi: --")
.flush()
.await
.map_err(Error::Flush)?;
display
.frame_mut(TIME_RECTANGLE)
.write_text("--:--:-- --")
.flush()
.await
.map_err(Error::Flush)?;
display
.for_each_tile(FIGURE_TILE_GRID, |frame| {
BACKGROUND_BITMAP.draw(frame).unwrap_infallible();
})
.await
.map_err(Error::Flush)?;
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Exit {
ResetWifi,
}
#[derive(Debug, derive_more::From)]
pub enum Error<FlushError> {
Linkage(LinkageError),
#[from(ignore)]
Flush(FlushError),
#[from(ignore)]
VecOverflow(DrawItem),
}
fn text_12h(local_time: &OffsetDateTime) -> heapless::String<24> {
let (hour_12, minute, second) = h12_m_s(local_time);
let suffix = if local_time.hour() % 24 < 12 {
"AM"
} else {
"PM"
};
let mut text = heapless::String::new();
fmt::write(
&mut text,
format_args!("{hour_12:>2}:{minute:02}:{second:02} {suffix}"),
)
.expect("clock string fits in 24 bytes");
text
}
fn text_24h(local_time: &OffsetDateTime) -> heapless::String<9> {
let mut text = heapless::String::new();
fmt::write(
&mut text,
format_args!(
"{:02}:{:02}:{:02}",
local_time.hour(),
local_time.minute(),
local_time.second()
),
)
.expect("clock string fits in 9 bytes");
text
}
fn linkage_params(local_time: &OffsetDateTime) -> [f32; 3] {
const SECOND_INDEX: usize = 0;
const MINUTE_INDEX: usize = 1;
const HOUR_INDEX: usize = 2;
const SECOND_SPAN_TURNS: f32 = param_span_turns(SECOND_INDEX);
const MINUTE_SPAN_TURNS: f32 = param_span_turns(MINUTE_INDEX);
const HOUR_SPAN_TURNS: f32 = param_span_turns(HOUR_INDEX);
const _: () = {
assert_param_name(SECOND_INDEX, "head_yrotation");
assert_param_name(MINUTE_INDEX, "r_shldr_zrotation");
assert_param_name(HOUR_INDEX, "l_shldr_zrotation");
assert!(SECOND_SPAN_TURNS == 4.0);
assert!(MINUTE_SPAN_TURNS == 4.0);
assert!(HOUR_SPAN_TURNS == 4.0);
};
const HEAD_AT_12_PARAM: f32 = 0.5;
const RIGHT_ARM_AT_12_PARAM: f32 = 0.4375;
const LEFT_ARM_AT_12_PARAM: f32 = 0.5625;
let seconds_turn = local_time.second() as f32 / 60.0;
let minutes_turn = (local_time.minute() as f32 + seconds_turn) / 60.0;
let hours_turn = ((local_time.hour() % 12) as f32 + minutes_turn) / 12.0;
from_fn(|index| match index {
SECOND_INDEX => wrap_unit(HEAD_AT_12_PARAM + seconds_turn / SECOND_SPAN_TURNS),
MINUTE_INDEX => wrap_unit(RIGHT_ARM_AT_12_PARAM + minutes_turn / MINUTE_SPAN_TURNS),
HOUR_INDEX => wrap_unit(LEFT_ARM_AT_12_PARAM + hours_turn / HOUR_SPAN_TURNS),
_ => unreachable!(),
})
}
fn wrap_unit(value: f32) -> f32 {
let mut value = value;
while value >= 1.0 {
value -= 1.0;
}
while value < 0.0 {
value += 1.0;
}
value
}
const fn assert_param_name(index: usize, name: &str) {
assert!(str_eq(LINKAGE.view().param(index).name(), name));
}
const fn param_span_turns(index: usize) -> f32 {
use core::f32::consts::TAU;
let (low, high) = LINKAGE.view().scan_param_range(index);
(high - low) / TAU
}
const fn str_eq(left: &str, right: &str) -> bool {
let (left, right) = (left.as_bytes(), right.as_bytes());
if left.len() != right.len() {
return false;
}
let mut i = 0;
while i < left.len() {
if left[i] != right[i] {
return false;
}
i += 1;
}
true
}
fn draw_centered_text<D>(
target: &mut D,
text: &str,
center: Point,
font: &'static MonoFont<'static>,
color: Rgb565,
) where
D: DrawTarget<Color = Rgb565, Error = Infallible>,
{
let text_style = TextStyleBuilder::new()
.alignment(Alignment::Center)
.baseline(Baseline::Middle)
.build();
Text::with_text_style(text, center, MonoTextStyle::new(font, color), text_style)
.draw(target)
.unwrap_infallible();
}
fn draw_centered_sign_value<D>(
target: &mut D,
sign_top_left: Point,
value_center: Point,
number: u32,
) where
D: DrawTarget<Color = Rgb565, Error = Infallible>,
{
let mut value_text = heapless::String::<4>::new();
fmt::write(&mut value_text, format_args!("{:02}", number % 100))
.expect("two-digit sign value fits in 4 bytes");
draw_centered_text(
target,
&value_text,
sign_top_left + value_center,
&FONT_10X20,
Rgb565::from(PLACARD_TEXT_COLOR),
);
}
#[cfg(test)]
mod tests {
use core::cell::Cell;
use device_envoy_core::button::{__ButtonMonitor, Button};
use device_envoy_core::clock_sync::{ClockSync, ClockSyncTick, UnixSeconds};
use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
use futures_executor::block_on;
use time::OffsetDateTime;
use super::{BACKGROUND_COLOR, Exit, FOREGROUND_COLOR, ORIENTATION, TOP_FONT, run};
struct FixedClockSync {
local_time: OffsetDateTime,
}
impl ClockSync for FixedClockSync {
async fn wait_for_tick(&self) -> ClockSyncTick {
ClockSyncTick {
local_time: self.local_time,
since_last_sync: embassy_time::Duration::from_secs(0),
}
}
fn now_local(&self) -> OffsetDateTime {
self.local_time
}
fn set_offset_minutes(&self, _minutes: i32) {}
fn offset_minutes(&self) -> i32 {
0
}
fn set_tick_interval(&self, _interval: Option<embassy_time::Duration>) {}
fn set_speed(&self, _speed_multiplier: f32) {}
fn set_utc_time(&self, _unix_seconds: UnixSeconds) {}
}
struct ImmediateButton;
impl __ButtonMonitor for ImmediateButton {
fn is_pressed_raw(&self) -> bool {
false
}
async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
}
impl Button for ImmediateButton {
async fn wait_for_press(&mut self) {}
}
#[test]
fn boot_requests_wifi_reset_before_rendering_the_next_tick() {
let memory_cyd = CydMemory::new(
ORIENTATION.size(),
BACKGROUND_COLOR,
FOREGROUND_COLOR,
&TOP_FONT,
);
let clock_sync = FixedClockSync {
local_time: OffsetDateTime::from_unix_timestamp(1_700_003_415)
.expect("valid fixed timestamp"),
};
let mut button = ImmediateButton;
let result = {
let mut display = memory_cyd.display();
block_on(run(&mut display, &clock_sync, &mut button))
};
assert_eq!(
result.expect("BOOT should be a typed exit"),
Exit::ResetWifi
);
}
#[test]
fn boot_requests_wifi_reset_after_a_rendered_tick() {
let mut memory_cyd = CydMemory::new(
ORIENTATION.size(),
BACKGROUND_COLOR,
FOREGROUND_COLOR,
&TOP_FONT,
);
memory_cyd.set_frame_budget(100);
let clock_sync = OneTickClockSync {
local_time: OffsetDateTime::from_unix_timestamp(1_700_003_415)
.expect("valid fixed timestamp"),
ticks: Cell::new(0),
};
let mut button = AfterTickButton {
waits: Cell::new(0),
};
let result = {
let mut display = memory_cyd.display();
block_on(run(&mut display, &clock_sync, &mut button))
};
assert_eq!(
result.expect("BOOT should exit after a rendered tick"),
Exit::ResetWifi
);
assert!(memory_cyd.flush_count() > 0);
}
const ONE_COMPLETE_FRAME_BUDGET: usize = 10;
#[test]
fn skeleton_clock_renders_expected_frame() {
let mut memory_cyd = CydMemory::new(
ORIENTATION.size(),
BACKGROUND_COLOR,
FOREGROUND_COLOR,
&TOP_FONT,
);
memory_cyd.set_frame_budget(ONE_COMPLETE_FRAME_BUDGET);
let clock_sync = FixedClockSync {
local_time: OffsetDateTime::from_unix_timestamp(1_700_003_415)
.expect("valid fixed timestamp"),
};
let mut memory_button = NeverButton;
let skeleton_clock_result = {
let mut display = memory_cyd.display();
block_on(run(&mut display, &clock_sync, &mut memory_button))
};
skeleton_clock_result.expect_err("the free-running loop should stop at the frame budget");
assert_framebuffer_matches_expected_png(
&memory_cyd,
env!("CARGO_MANIFEST_DIR"),
"skeleton_clock.png",
)
.expect("rendered frame should match the golden image");
}
struct NeverButton;
impl __ButtonMonitor for NeverButton {
fn is_pressed_raw(&self) -> bool {
false
}
async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
}
impl Button for NeverButton {
async fn wait_for_press(&mut self) {
core::future::pending().await
}
}
struct AfterTickButton {
waits: Cell<u8>,
}
impl __ButtonMonitor for AfterTickButton {
fn is_pressed_raw(&self) -> bool {
false
}
async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
}
impl Button for AfterTickButton {
async fn wait_for_press(&mut self) {
let wait_number = self.waits.get();
self.waits.set(wait_number + 1);
if wait_number == 0 {
core::future::pending().await
}
}
}
struct OneTickClockSync {
local_time: OffsetDateTime,
ticks: Cell<u8>,
}
impl ClockSync for OneTickClockSync {
async fn wait_for_tick(&self) -> ClockSyncTick {
if self.ticks.replace(1) == 0 {
ClockSyncTick {
local_time: self.local_time,
since_last_sync: embassy_time::Duration::from_secs(0),
}
} else {
core::future::pending().await
}
}
fn now_local(&self) -> OffsetDateTime {
self.local_time
}
fn set_offset_minutes(&self, _minutes: i32) {}
fn offset_minutes(&self) -> i32 {
0
}
fn set_tick_interval(&self, _interval: Option<embassy_time::Duration>) {}
fn set_speed(&self, _speed_multiplier: f32) {}
fn set_utc_time(&self, _unix_seconds: UnixSeconds) {}
}
}