From e9298384c278886e1c216da74be8cb2bce48fd29 Mon Sep 17 00:00:00 2001 From: Yandrik Date: Thu, 14 Nov 2024 16:27:23 +0100 Subject: [PATCH] feat: create hardware example tester and exapp-k-timer --- app/src/bin/exapp-k-timer.rs | 468 +++++++++++++++++++++++++ app/src/bin/hardware-example-tester.rs | 261 ++++++++++++++ 2 files changed, 729 insertions(+) create mode 100644 app/src/bin/exapp-k-timer.rs create mode 100644 app/src/bin/hardware-example-tester.rs diff --git a/app/src/bin/exapp-k-timer.rs b/app/src/bin/exapp-k-timer.rs new file mode 100644 index 0000000..976d576 --- /dev/null +++ b/app/src/bin/exapp-k-timer.rs @@ -0,0 +1,468 @@ +#![no_std] +#![no_main] + +use core::{cell::RefCell, cmp::min, fmt}; + +use display_interface_spi::SPIInterface; +use embassy_embedded_hal::shared_bus::blocking::spi::SpiDevice; +use embassy_executor::Spawner; +use embassy_sync::{ + blocking_mutex::{raw::NoopRawMutex, NoopMutex}, + signal::Signal, +}; +use embassy_time::{Duration, Instant, Timer}; +use embedded_graphics::{ + mono_font::ascii, + pixelcolor::Rgb565, + prelude::{DrawTarget, Point, RgbColor, Size, WebColors}, +}; +use embedded_graphics_profiler_display::ProfilerDisplay; +use esp_backtrace as _; +use esp_hal::{ + clock::ClockControl, + gpio::{GpioPin, Input, Io, Level, Output, Pull, NO_PIN}, + peripherals::{Peripherals, SPI2, SPI3}, + prelude::*, + rtc_cntl::Rtc, + spi::{master::Spi, FullDuplexMode, SpiMode}, + system::SystemControl, + timer::timg::TimerGroup, +}; +use esp_println::println; +use kolibri_cyd_tester_app_embassy::Debouncer; +use kolibri_embedded_gui::{ + button::Button, + checkbox::Checkbox, + icon::IconWidget, + iconbutton::IconButton, + icons::{size12px, size24px, size32px, size48px, size96px}, + label::Label, + smartstate::SmartstateProvider, + spacer::Spacer, + style::medsize_rgb565_style, + ui::{Interaction, Ui}, +}; +use mipidsi::{ + models::ILI9486Rgb565, + options::{ColorInversion, ColorOrder, Orientation, Rotation}, + Builder, +}; +use static_cell::StaticCell; +use xpt2046::Xpt2046; + +#[embassy_executor::task] +async fn touch_task( + touch_irq: GpioPin<36>, + spi: &'static mut NoopMutex>>, + touch_cs: GpioPin<33>, + touch_signal: &'static Signal>, +) -> ! { + let mut touch_driver = Xpt2046::new( + SpiDevice::new(spi, Output::new(touch_cs, Level::Low)), + Input::new(touch_irq, Pull::Up), + xpt2046::Orientation::LandscapeFlipped, + ); + touch_driver.set_num_samples(16); + touch_driver.init(&mut embassy_time::Delay).unwrap(); + + println!("touch task"); + + loop { + touch_driver.run().expect("Running Touch driver failed"); + if touch_driver.is_touched() { + let point = touch_driver.get_touch_point(); + touch_signal.signal(Some(Point::new(point.x + 25, 240 - point.y))); + } else { + touch_signal.signal(None); + } + Timer::after(Duration::from_millis(1)).await; // 100 a second + + // Your touch handling logic here + } +} + +struct AppData { + timer_start: Instant, + timer_set_duration: Duration, + timer_remaining_duration: Duration, + timer_running: bool, + timer_paused: bool, +} + +impl AppData { + fn new() -> Self { + Self { + timer_start: Instant::now(), + timer_set_duration: Duration::from_secs(10), + timer_remaining_duration: Duration::from_secs(10), + timer_running: false, + timer_paused: false, + } + } + + fn set_timer_duration(&mut self, duration: Duration) { + self.timer_set_duration = duration; + } + + fn add_secs(&mut self, secs: u64) { + self.set_timer_duration( + self.timer_set_duration + .checked_add(Duration::from_secs(secs)) + .unwrap_or(Duration::from_secs(5999)) + .min(Duration::from_secs(5999)), + ); + } + + fn sub_secs(&mut self, secs: u64) { + self.set_timer_duration( + self.timer_set_duration + .checked_sub(Duration::from_secs(secs)) + .unwrap_or(Duration::from_secs(10)) + .max(Duration::from_secs(10)), + ); + } + + fn start_timer(&mut self) { + if !self.timer_paused { + self.timer_remaining_duration = self.timer_set_duration; + } + + self.timer_start = Instant::now(); + self.timer_running = true; + self.timer_paused = false; + } + + fn pause_timer(&mut self) { + self.timer_paused = true; + self.timer_remaining_duration = self + .timer_remaining_duration + .checked_sub(self.timer_start.elapsed()) + .unwrap_or(Duration::from_secs(0)); + } + + fn reset_timer(&mut self) { + self.timer_start = Instant::now(); + self.timer_paused = false; + self.timer_running = false; + self.timer_remaining_duration = self.timer_set_duration; + } + + fn remaining(&self) -> Duration { + if self.timer_stopped() { + self.timer_set_duration + } else if self.timer_paused() { + self.timer_remaining_duration + } else { + self.timer_remaining_duration + .checked_sub(self.timer_start.elapsed()) + .unwrap_or(Duration::from_secs(0)) + } + } + + fn timer_stopped(&self) -> bool { + !self.timer_running + } + + fn timer_paused(&self) -> bool { + self.timer_running && self.timer_paused + } + + fn timer_running(&self) -> bool { + self.timer_running && !self.timer_paused + } + + fn timer_finished(&self) -> bool { + self.timer_running && self.remaining() == Duration::from_secs(0) + } +} + +#[main] +async fn main(spawner: Spawner) { + let peripherals = Peripherals::take(); + let system = SystemControl::new(peripherals.SYSTEM); + let mut clocks = ClockControl::boot_defaults(system.clock_control).freeze(); + + // Enable the RWDT watchdog timer: + let mut rtc = Rtc::new(peripherals.LPWR); + rtc.rwdt.set_timeout(2.secs()); + rtc.rwdt.enable(); + println!("RWDT watchdog enabled!"); + + // Initialize the SYSTIMER peripheral, and then Embassy: + let timg0 = TimerGroup::new(peripherals.TIMG0, &clocks); + esp_hal_embassy::init(&clocks, timg0.timer0); + println!("Embassy initialized!"); + + let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); + // let mut led = Output::new(io.pins.gpio5, Level::High); + // Initialize SPI + let sclk = io.pins.gpio14; + let miso = io.pins.gpio12; + let mosi = io.pins.gpio13; + let cs = io.pins.gpio15; + let dc = io.pins.gpio2; + let mut backlight = Output::new(io.pins.gpio21, Level::Low); + + // Note: RST is not initialized as it's set to -1 in the instructions + + // up to 80MHz is possible (even tho the display driver isn't supposed to be + // that fast) Dataseheet sais 10MHz, so we're gonna go with that + let mut spi = Spi::new(peripherals.SPI2, 10.MHz(), SpiMode::Mode0, &mut clocks).with_pins( + Some(sclk), + Some(mosi), + Some(miso), + NO_PIN, + ); + + static DISP_SPI_BUS: StaticCell>>> = + StaticCell::new(); + let spi_bus = NoopMutex::new(RefCell::new(spi)); + let spi_bus = DISP_SPI_BUS.init(spi_bus); + + let di = SPIInterface::new( + SpiDevice::new(spi_bus, Output::new(cs, Level::Low)), + Output::new(dc, Level::Low), + ); + let display = Builder::new(ILI9486Rgb565, di) + .orientation(Orientation { + rotation: Rotation::Deg90, + mirrored: true, + }) + .color_order(ColorOrder::Bgr) + .invert_colors(ColorInversion::Inverted) + .init(&mut embassy_time::Delay) + .unwrap(); + + let mut display = ProfilerDisplay::new(display); + + { + let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style()); + ui.clear_background().ok(); + } + backlight.set_high(); + + // init touchscreen pins + let touch_irq = io.pins.gpio36; + let touch_mosi = io.pins.gpio32; + let touch_miso = io.pins.gpio39; + let touch_clk = io.pins.gpio25; + let touch_cs = io.pins.gpio33; + + // 2MHz is the MAX! DO NOT DECREASE! This is really important. + let mut touch_spi = Spi::new(peripherals.SPI3, 2.MHz(), SpiMode::Mode0, &mut clocks).with_pins( + Some(touch_clk), + Some(touch_mosi), + Some(touch_miso), + NO_PIN, + ); + static TOUCH_SPI_BUS: StaticCell>>> = + StaticCell::new(); + let touch_spi_bus = NoopMutex::new(RefCell::new(touch_spi)); + let touch_spi_bus = TOUCH_SPI_BUS.init(touch_spi_bus); + + let touch_signal = Signal::new(); + static TOUCH_SIGNAL: StaticCell>> = StaticCell::new(); + let touch_signal = &*TOUCH_SIGNAL.init(touch_signal); + + spawner + .spawn(touch_task(touch_irq, touch_spi_bus, touch_cs, touch_signal)) + .unwrap(); + + // TODO: Spawn some tasks + let _ = spawner; + + // variables + let mut appdata = AppData::new(); + let (mut prev_mins, mut prev_secs, mut prev_millis) = (0, 0, 0); + let mut finished = false; + + // touchpoints + + let mut last_touch = None; + + static BUF_CELL: StaticCell<[Rgb565; 100 * 100]> = StaticCell::new(); + let buf = BUF_CELL.init([Rgb565::BLACK; 100 * 100]); + + let mut textbuf = [0u8; 64]; + + // Periodically feed the RWDT watchdog timer when our tasks are not running: + let mut sm = SmartstateProvider::<20>::new(); + loop { + // SMART REDRAWING ENABLE / DISABLE + // sm.force_redraw_all(); + + let start_time = embassy_time::Instant::now(); + sm.restart_counter(); + let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style()); + if let Some(touch) = touch_signal.try_take() { + let interact = match (touch, last_touch) { + (Some(point), Some(_)) => Interaction::Drag(point), + (Some(point), None) => Interaction::Click(point), + (None, Some(point)) => Interaction::Release(point), + (None, None) => Interaction::None, + }; + ui.interact(interact); + // println!("{:?}, {:?}, {:?}", last_touch, touch, interact); + + last_touch = touch; + } + + // BUFFER ENABLE/DISABLE + ui.set_buffer(buf); + + let start_draw_time = embassy_time::Instant::now(); + ui.sub_ui(|ui| { + ui.style_mut().default_font = ascii::FONT_9X18_BOLD; + ui.add(Label::new("Kolibri Timer App").smartstate(sm.next())); + Ok(()) + }) + .ok(); + + let remaining = appdata.remaining(); + + ui.add(Spacer::new(Size::new(0, 60))); + ui.add_horizontal(Spacer::new(Size::new(80, 0))); + ui.sub_ui(|ui| { + ui.style_mut().default_font = ascii::FONT_10X20; + if remaining.as_secs() / 60 != prev_mins { + sm.peek().force_redraw(); + prev_mins = remaining.as_secs() / 60; + } + ui.add_horizontal( + Label::new( + &format_no_std::show( + &mut textbuf, + format_args!("{:02}", remaining.as_secs() / 60), + ) + .unwrap(), + ) + .smartstate(sm.next()), + ); + ui.add_horizontal(Label::new(":").smartstate(sm.next())); + + if remaining.as_secs() % 60 != prev_secs { + sm.peek().force_redraw(); + prev_secs = remaining.as_secs() % 60; + } + ui.add_horizontal( + Label::new( + &format_no_std::show( + &mut textbuf, + format_args!("{:02}", remaining.as_secs() % 60), + ) + .unwrap(), + ) + .smartstate(sm.next()), + ); + ui.add_horizontal(Label::new(":").smartstate(sm.next())); + + if remaining.as_millis() % 1000 != prev_millis { + sm.peek().force_redraw(); + prev_millis = remaining.as_millis() % 1000; + } + ui.add( + Label::new( + &format_no_std::show( + &mut textbuf, + format_args!("{:03}", remaining.as_millis() % 1000), + ) + .unwrap(), + ) + .smartstate(sm.next()), + ); + Ok(()) + }) + .ok(); + + ui.add_horizontal(Spacer::new(Size::new(65, 0))); + ui.sub_ui(|ui| { + if appdata.timer_running() { + ui.style_mut().icon_color = Rgb565::CSS_LIGHT_GRAY; + } + if ui + .add_horizontal(IconButton::new(size32px::actions::AddCircle).smartstate(sm.next())) + .clicked() + { + if !appdata.timer_running() { + appdata.add_secs(10); + } + } + ui.add_horizontal(Label::new("+/- 10s").smartstate(sm.next())); + if ui + .add(IconButton::new(size32px::actions::MinusCircle).smartstate(sm.next())) + .clicked() + { + if !appdata.timer_running() { + appdata.sub_secs(10); + } + } + Ok(()) + }) + .ok(); + + ui.add_horizontal(Spacer::new(Size::new(80, 0))); + if ui + .add_horizontal(IconButton::new(size48px::actions::Undo).smartstate(sm.next())) + .clicked() + { + appdata.reset_timer(); + sm.force_redraw_all(); + } + + if !finished && appdata.timer_finished() { + sm.peek().force_redraw(); + } + + if appdata.timer_finished() { + if ui + .add_horizontal( + IconButton::new(size48px::actions::RemoveSquare).smartstate(sm.next()), + ) + .clicked() + { + appdata.reset_timer(); + sm.force_redraw_all(); + } + } else if appdata.timer_running() { + if ui + .add_horizontal(IconButton::new(size48px::music::Pause).smartstate(sm.next())) + .clicked() + { + appdata.pause_timer(); + sm.force_redraw_all(); + } + } else { + if ui + .add_horizontal(IconButton::new(size48px::music::Play).smartstate(sm.next())) + .clicked() + { + appdata.start_timer(); + sm.force_redraw_all(); + } + } + + finished = appdata.timer_finished(); + + let end_time = embassy_time::Instant::now(); + let draw_time = display.get_time(); + let prep_time = start_draw_time - start_time; + let proc_time = end_time - start_draw_time; + let proc_time = proc_time - min(draw_time, proc_time); + rtc.rwdt.feed(); + + if draw_time.as_micros() > 0 { + println!( + "draw time: {}.{:03}ms | prep time: {}.{:03}ms | proc time: {}.{:03}ms | total time: {}.{:03}ms", + draw_time.as_millis(), + draw_time.as_micros() % 100, + prep_time.as_millis(), + prep_time.as_micros() % 100, + proc_time.as_millis(), + proc_time.as_micros() % 100, + (draw_time + prep_time + proc_time).as_millis(), + (draw_time + prep_time + proc_time).as_micros() % 100, ); + } + display.reset_time(); + Timer::after(Duration::from_millis(17)).await; // 60 a second + } +} diff --git a/app/src/bin/hardware-example-tester.rs b/app/src/bin/hardware-example-tester.rs new file mode 100644 index 0000000..66620b1 --- /dev/null +++ b/app/src/bin/hardware-example-tester.rs @@ -0,0 +1,261 @@ +#![no_std] +#![no_main] + +use core::{cell::RefCell, cmp::min}; + +use display_interface_spi::SPIInterface; +use embassy_embedded_hal::shared_bus::blocking::spi::SpiDevice; +use embassy_executor::Spawner; +use embassy_sync::{ + blocking_mutex::{raw::NoopRawMutex, NoopMutex}, + signal::Signal, +}; +use embassy_time::{Duration, Timer}; +use embedded_graphics::{ + mono_font::ascii, + pixelcolor::Rgb565, + prelude::{DrawTarget, Point, RgbColor}, +}; +use embedded_graphics_profiler_display::ProfilerDisplay; +use esp_backtrace as _; +use esp_hal::{ + clock::ClockControl, + gpio::{GpioPin, Input, Io, Level, Output, Pull, NO_PIN}, + peripherals::{Peripherals, SPI2, SPI3}, + prelude::*, + rtc_cntl::Rtc, + spi::{master::Spi, FullDuplexMode, SpiMode}, + system::SystemControl, + timer::timg::TimerGroup, +}; +use esp_println::println; +use kolibri_cyd_tester_app_embassy::Debouncer; +use kolibri_embedded_gui::{ + button::Button, + checkbox::Checkbox, + icon::IconWidget, + iconbutton::IconButton, + icons::{size12px, size24px, size32px, size48px, size96px}, + label::Label, + smartstate::SmartstateProvider, + style::medsize_rgb565_style, + ui::{Interaction, Ui}, +}; +use mipidsi::{ + models::ILI9486Rgb565, + options::{ColorInversion, ColorOrder, Orientation, Rotation}, + Builder, +}; +use static_cell::StaticCell; +use xpt2046::Xpt2046; + +#[embassy_executor::task] +async fn touch_task( + touch_irq: GpioPin<36>, + spi: &'static mut NoopMutex>>, + touch_cs: GpioPin<33>, + touch_signal: &'static Signal>, +) -> ! { + let mut touch_driver = Xpt2046::new( + SpiDevice::new(spi, Output::new(touch_cs, Level::Low)), + Input::new(touch_irq, Pull::Up), + xpt2046::Orientation::LandscapeFlipped, + ); + touch_driver.set_num_samples(16); + touch_driver.init(&mut embassy_time::Delay).unwrap(); + + let mut debounce = Debouncer::new(); + println!("touch task"); + + loop { + touch_driver.run().expect("Running Touch driver failed"); + if touch_driver.is_touched() { + let point = touch_driver.get_touch_point(); + touch_signal.signal(Some(Point::new(point.x + 25, 240 - point.y))); + } else { + touch_signal.signal(None); + } + Timer::after(Duration::from_millis(1)).await; // 100 a second + + // Your touch handling logic here + } +} + +#[main] +async fn main(spawner: Spawner) { + let peripherals = Peripherals::take(); + let system = SystemControl::new(peripherals.SYSTEM); + let mut clocks = ClockControl::boot_defaults(system.clock_control).freeze(); + + // Enable the RWDT watchdog timer: + let mut rtc = Rtc::new(peripherals.LPWR); + rtc.rwdt.set_timeout(2.secs()); + rtc.rwdt.enable(); + println!("RWDT watchdog enabled!"); + + // Initialize the SYSTIMER peripheral, and then Embassy: + let timg0 = TimerGroup::new(peripherals.TIMG0, &clocks); + esp_hal_embassy::init(&clocks, timg0.timer0); + println!("Embassy initialized!"); + + let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); + // let mut led = Output::new(io.pins.gpio5, Level::High); + // Initialize SPI + let sclk = io.pins.gpio14; + let miso = io.pins.gpio12; + let mosi = io.pins.gpio13; + let cs = io.pins.gpio15; + let dc = io.pins.gpio2; + let mut backlight = Output::new(io.pins.gpio21, Level::Low); + + // Note: RST is not initialized as it's set to -1 in the instructions + + // up to 80MHz is possible (even tho the display driver isn't supposed to be + // that fast) Dataseheet sais 10MHz, so we're gonna go with that + let mut spi = Spi::new(peripherals.SPI2, 10.MHz(), SpiMode::Mode0, &mut clocks).with_pins( + Some(sclk), + Some(mosi), + Some(miso), + NO_PIN, + ); + + static DISP_SPI_BUS: StaticCell>>> = + StaticCell::new(); + let spi_bus = NoopMutex::new(RefCell::new(spi)); + let spi_bus = DISP_SPI_BUS.init(spi_bus); + + let di = SPIInterface::new( + SpiDevice::new(spi_bus, Output::new(cs, Level::Low)), + Output::new(dc, Level::Low), + ); + let display = Builder::new(ILI9486Rgb565, di) + .orientation(Orientation { + rotation: Rotation::Deg90, + mirrored: true, + }) + .color_order(ColorOrder::Bgr) + .invert_colors(ColorInversion::Inverted) + .init(&mut embassy_time::Delay) + .unwrap(); + + let mut display = ProfilerDisplay::new(display); + + { + let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style()); + ui.clear_background().ok(); + } + backlight.set_high(); + + // init touchscreen pins + let touch_irq = io.pins.gpio36; + let touch_mosi = io.pins.gpio32; + let touch_miso = io.pins.gpio39; + let touch_clk = io.pins.gpio25; + let touch_cs = io.pins.gpio33; + + // 2MHz is the MAX! DO NOT DECREASE! This is really important. + let mut touch_spi = Spi::new(peripherals.SPI3, 2.MHz(), SpiMode::Mode0, &mut clocks).with_pins( + Some(touch_clk), + Some(touch_mosi), + Some(touch_miso), + NO_PIN, + ); + static TOUCH_SPI_BUS: StaticCell>>> = + StaticCell::new(); + let touch_spi_bus = NoopMutex::new(RefCell::new(touch_spi)); + let touch_spi_bus = TOUCH_SPI_BUS.init(touch_spi_bus); + + let touch_signal = Signal::new(); + static TOUCH_SIGNAL: StaticCell>> = StaticCell::new(); + let touch_signal = &*TOUCH_SIGNAL.init(touch_signal); + + spawner + .spawn(touch_task(touch_irq, touch_spi_bus, touch_cs, touch_signal)) + .unwrap(); + + // TODO: Spawn some tasks + let _ = spawner; + + // variables + let mut checked = false; + + // touchpoints + + let mut last_touch = None; + + static BUF_CELL: StaticCell<[Rgb565; 100 * 100]> = StaticCell::new(); + let buf = BUF_CELL.init([Rgb565::BLACK; 100 * 100]); + + // Periodically feed the RWDT watchdog timer when our tasks are not running: + let mut sm = SmartstateProvider::<20>::new(); + loop { + // SMART REDRAWING ENABLE / DISABLE + // sm.force_redraw_all(); + + let start_time = embassy_time::Instant::now(); + sm.restart_counter(); + let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style()); + if let Some(touch) = touch_signal.try_take() { + let interact = match (touch, last_touch) { + (Some(point), Some(_)) => Interaction::Drag(point), + (Some(point), None) => Interaction::Click(point), + (None, Some(point)) => Interaction::Release(point), + (None, None) => Interaction::None, + }; + ui.interact(interact); + // println!("{:?}, {:?}, {:?}", last_touch, touch, interact); + + last_touch = touch; + } + + // BUFFER ENABLE/DISABLE + // ui.set_buffer(buf); + + let start_draw_time = embassy_time::Instant::now(); + ui.sub_ui(|ui| { + ui.style_mut().default_font = ascii::FONT_9X18_BOLD; + ui.add(Label::new("Optimizations Performance Test").smartstate(sm.next())); + Ok(()) + }) + .ok(); + ui.add_horizontal(Button::new("Button 1").smartstate(sm.next())); + ui.add(Button::new("Button 2").smartstate(sm.next())); + ui.add_horizontal(IconButton::new(size32px::actions::AddCircle).smartstate(sm.next())); + ui.add_horizontal(Label::new("Add / Remove").smartstate(sm.next())); + ui.add(IconButton::new(size32px::actions::RemoveSquare).smartstate(sm.next())); + ui.add_horizontal(Label::new("Check this Checkbox: ").smartstate(sm.next())); + ui.add(Checkbox::new(&mut checked).smartstate(sm.next())); + + ui.add(Label::new("Some Icons: ").smartstate(sm.next())); + + ui.add_horizontal(IconWidget::new(size12px::actions::AddCircle).smartstate(sm.next())); + ui.add_horizontal(IconWidget::new(size24px::actions::OpenNewWindow).smartstate(sm.next())); + ui.add_horizontal(IconWidget::new(size32px::actions::Cancel).smartstate(sm.next())); + ui.add_horizontal(IconWidget::new(size48px::actions::Download).smartstate(sm.next())); + ui.add_horizontal( + IconWidget::new(size96px::actions::SaveActionFloppy).smartstate(sm.next()), + ); + + let end_time = embassy_time::Instant::now(); + let draw_time = display.get_time(); + let prep_time = start_draw_time - start_time; + let proc_time = end_time - start_draw_time; + let proc_time = proc_time - min(draw_time, proc_time); + rtc.rwdt.feed(); + + if draw_time.as_micros() > 0 { + println!( + "draw time: {}.{:03}ms | prep time: {}.{:03}ms | proc time: {}.{:03}ms | total time: {}.{:03}ms", + draw_time.as_millis(), + draw_time.as_micros() % 100, + prep_time.as_millis(), + prep_time.as_micros() % 100, + proc_time.as_millis(), + proc_time.as_micros() % 100, + (draw_time + prep_time + proc_time).as_millis(), + (draw_time + prep_time + proc_time).as_micros() % 100, ); + } + display.reset_time(); + Timer::after(Duration::from_millis(17)).await; // 60 a second + } +}