From 4432ba7ad0c15b4b0a88feb19f9a43e50f7ad139 Mon Sep 17 00:00:00 2001 From: Victoria Fischer Date: Sun, 27 Oct 2024 11:19:26 +0100 Subject: [PATCH] renderbug: first implementation of surface-based rendering --- Cargo.toml | 3 ++ src/main.rs | 86 +++++++++++++++++++----------- src/power.rs | 26 ++++----- src/render.rs | 134 ++++++++++++++++++++++++++++++++++++++++++++++ src/task.rs | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/time.rs | 30 +++++++++++ 6 files changed, 380 insertions(+), 44 deletions(-) create mode 100644 src/render.rs create mode 100644 src/task.rs create mode 100644 src/time.rs diff --git a/Cargo.toml b/Cargo.toml index c779579..3953e8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,9 @@ ws2812-esp32-rmt-driver = { version = "*", features = ["embedded-graphics-core"] embedded-graphics = { version = "0.8.1", features = ["fixed_point", "defmt"] } hsv = "0.1.1" palette = { version = "0.7.6" } +embedded-canvas = "0.3.1" +embassy-executor = "0.6.0" +running-average = "0.1.0" [build-dependencies] embuild = "0.32.0" diff --git a/src/main.rs b/src/main.rs index 74114d6..c710420 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,58 @@ +#![feature(trait_upcasting)] #![allow(arithmetic_overflow)] use esp_idf_svc::hal::prelude::Peripherals; use ws2812_esp32_rmt_driver::lib_embedded_graphics::{LedPixelStrip, Ws2812DrawTarget}; use embedded_graphics::{ prelude::*, - pixelcolor::Rgb888, }; use palette::Hsv; use palette::convert::IntoColorUnclamped; - +use std::thread; mod power; mod lib8; +mod render; +mod task; +mod time; +use crate::time::Periodically; + +struct IdleTask { + frame: u8, + surface: render::Surface, + updater: Periodically +} + +struct IdleShader { + frame: u8 +} + +impl render::Shader for IdleShader { + fn draw(&self, coords: Point) -> lib8::RGB8 { + Hsv::new_srgb(self.frame.wrapping_add(coords.x as u8), 255, 255).into_color_unclamped() + } +} + +impl IdleTask { + fn new(render: &mut dyn render::Display) -> Self { + IdleTask { + frame: 0, + surface: render.new_surface(), + updater: Periodically::new_every_n_ms(16) + } + } +} + +impl task::Task for IdleTask { + fn name(&self) -> &'static str { "Idle" } + + fn tick(&mut self) { + self.updater.run(|| { + self.frame = self.frame.wrapping_add(1); + self.surface.set_shader(Box::new(IdleShader { frame: self.frame })); + }) + } +} fn main() { // It is necessary to call this function once. Otherwise some patches to the runtime @@ -32,35 +73,18 @@ fn main() { const POWER_MA : u32 = 500; const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA; - let mut draw = Ws2812DrawTarget::>::new(channel, led_pin).unwrap(); - let mut hue : u8 = 0; - let mut length : usize = NUM_PIXELS; - let mut forwards = false; - loop { - let mut totalMW = 0; - draw.clear(Rgb888::BLACK); - for i in 0..length { - let hsvColor = Hsv::new_srgb(hue.wrapping_add(i as u8), 255, 255); - let rgbColor : lib8::RGB8 = hsvColor.into_color_unclamped(); - let color = Rgb888::new(rgbColor.red, rgbColor.green, rgbColor.blue); + log::info!("Setting up display"); + let target = Ws2812DrawTarget::>::new(channel, led_pin).unwrap(); + let mut display = render::EmbeddedDisplay::new(target, MAX_POWER_MW); - totalMW += power::colorToMW(color); - Pixel(Point::new(i as i32, 0), color).draw(&mut draw).unwrap(); - } - let brightness = power::brightnessForMW(totalMW, 255, MAX_POWER_MW); - draw.set_brightness(brightness); - draw.flush().unwrap(); - log::info!("Frame hue={} power={} brightness={}", hue, totalMW, brightness); - hue = hue.wrapping_add(1); - if forwards { - length += 1 - } else { - length -= 1 - } - if length <= 1 { - forwards = true; - } else if length >= NUM_PIXELS { - forwards = false; - } + log::info!("Creating runner"); + let mut runner = task::Scheduler::new(vec![ + Box::new(IdleTask::new(&mut display)), + Box::new(display), + ]); + + log::info!("Ready to rock and roll"); + loop { + runner.tick(); } } diff --git a/src/power.rs b/src/power.rs index 3e2d2bf..e4ba026 100644 --- a/src/power.rs +++ b/src/power.rs @@ -1,23 +1,23 @@ use embedded_graphics::pixelcolor::RgbColor; -pub fn colorToMW(color : impl RgbColor) -> u32 { - const gRed_mW : u32 = 16 * 5; //< 16mA @ 5v = 80mW - const gGreen_mW : u32 = 11 * 5; //< 11mA @ 5v = 55mW - const gBlue_mW : u32 = 15 * 5; //< 15mA @ 5v = 75mW - const gDark_mW : u32 = 1 * 5; //< 1mA @ 5v = 5mW +pub fn color_to_mw(color : &T) -> u32 { + const RED_MW : u32 = 16 * 5; //< 16mA @ 5v = 80mW + const GREEN_MW : u32 = 11 * 5; //< 11mA @ 5v = 55mW + const BLUE_MW : u32 = 15 * 5; //< 15mA @ 5v = 75mW + const DARK_MW : u32 = 1 * 5; //< 1mA @ 5v = 5mW - let redMW = (color.r() as u32 * gRed_mW).wrapping_shr(8); - let greenMW = (color.g() as u32 * gGreen_mW).wrapping_shr(8); - let blueMW = (color.b() as u32 * gBlue_mW).wrapping_shr(8); + let red = (color.r() as u32 * RED_MW).wrapping_shr(8); + let green = (color.g() as u32 * GREEN_MW).wrapping_shr(8); + let blue = (color.b() as u32 * BLUE_MW).wrapping_shr(8); - return redMW + greenMW + blueMW + gDark_mW; + return red + green + blue + DARK_MW; } -pub fn brightnessForMW(totalMW : u32, target : u8, maxPower: u32) -> u8 { +pub fn brightness_for_mw(total_mw : u32, target : u8, max_power: u32) -> u8 { let target32 = target as u32; - let requestedMW = (totalMW * target32) / 256; - if requestedMW > maxPower { - return ((target32 * maxPower) / requestedMW).try_into().unwrap(); + let requested_mw = (total_mw * target32) / 256; + if requested_mw > max_power { + return ((target32 * max_power) / requested_mw).try_into().unwrap(); } return target; } diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..cfc8eeb --- /dev/null +++ b/src/render.rs @@ -0,0 +1,134 @@ +use embedded_graphics::{ + prelude::*, + pixelcolor::Rgb888 +}; +use ws2812_esp32_rmt_driver::lib_embedded_graphics::{Ws2812DrawTarget, LedPixelShape}; + +use std::rc::Rc; +use std::cell::RefCell; + +use running_average::RealTimeRunningAverage; + +use crate::task; +use crate::lib8::RGB8; +use crate::power; +use crate::time::Periodically; + +pub trait Shader: Send { + fn draw(&self, coords: Point) -> RGB8; +} + +pub trait Surfaces { + fn new_surface(&mut self) -> Surface; +} + +pub trait Display: Surfaces { + fn start_frame(&mut self) {} + fn end_frame(&mut self) {} + + fn render_frame(&mut self) {} +} + +impl task::Task for T +where +T: Display { + fn name(&self) -> &'static str { "Renderer" } + + fn tick(&mut self) { + self.start_frame(); + self.render_frame(); + self.end_frame(); + } +} + +struct ShaderSlot { + shader: Option> +} + +pub struct Surface { + slot: Rc> +} + +impl Surface { + fn new(slot: Rc>) -> Self { + Self { + slot: slot + } + } + + pub fn set_shader(&mut self, shader: Box) { + self.slot.borrow_mut().shader = Some(shader); + } +} + +pub struct EmbeddedDisplay +where +T: DrawTarget { + shaders : RefCell>>>, + target: T, + total_mw: u32, + max_mw: u32, + fps: RealTimeRunningAverage, + frame: u32, + fps_display: Periodically +} + +impl EmbeddedDisplay +where +T: DrawTarget { + pub fn new(target: T, max_mw: u32) -> Self { + EmbeddedDisplay { + shaders: RefCell::new(Vec::new()), + target: target, + max_mw: max_mw, + total_mw: 0, + fps: RealTimeRunningAverage::default(), + frame: 0, + fps_display: Periodically::new_every_n_seconds(5) + } + } +} + +impl Surfaces for EmbeddedDisplay +where +T: DrawTarget { + fn new_surface(&mut self) -> Surface { + let slot = Rc::new(RefCell::new(ShaderSlot { + shader: None + })); + let surface = Surface::new(slot.clone()); + self.shaders.borrow_mut().push(slot); + return surface; + } +} + +impl Display for EmbeddedDisplay> { + fn start_frame(&mut self) { + self.target.clear(Rgb888::BLACK).unwrap(); + self.total_mw = 0; + self.frame = self.frame.wrapping_add(1); + } + + fn end_frame(&mut self) { + let brightness = power::brightness_for_mw(self.total_mw, 255, self.max_mw); + self.target.set_brightness(brightness); + self.target.flush().unwrap(); + self.fps.insert(1); + self.fps_display.run(|| { + log::info!("FPS: {} frame={} brightness={} mw={}", self.fps.measurement(), self.frame, brightness, self.total_mw); + }); + } + + fn render_frame(&mut self) { + for slot in self.shaders.borrow().iter() { + if let Some(ref shader) = slot.borrow().shader { + for i in 0..T::size().width { + let coords = Point::new(i as i32, 0); + let color = shader.draw(coords); + self.total_mw += power::color_to_mw(&color); + Pixel(coords, Rgb888::new(color.red, color.green, color.blue)).draw(&mut self.target).unwrap(); + } + } + } + } +} diff --git a/src/task.rs b/src/task.rs new file mode 100644 index 0000000..acd649e --- /dev/null +++ b/src/task.rs @@ -0,0 +1,145 @@ +use std::fmt; + +pub trait Task { + fn tick(&mut self) {} + fn start(&mut self) {} + fn stop(&mut self) {} + fn name(&self) -> &'static str; +} + +trait ScheduledState: std::fmt::Debug { + fn start(self: Box) -> Box; + fn stop(self: Box) -> Box; + fn tick(self: Box, task: &mut dyn Task) -> Box; +} + +#[derive(Debug)] +struct Starting {} +impl ScheduledState for Starting { + fn start(self: Box) -> Box { + self + } + + fn stop(self: Box) -> Box { + Box::new(Stopped {}) + } + + fn tick(self: Box, task: &mut dyn Task) -> Box { + task.start(); + Box::new(Running{}) + } +} + +#[derive(Debug)] +struct Running {} +impl ScheduledState for Running { + fn start(self: Box) -> Box { + self + } + + fn stop(self: Box) -> Box { + Box::new(Stopping {}) + } + + fn tick(self: Box, task: &mut dyn Task) -> Box { + task.tick(); + self + } +} + +#[derive(Debug)] +struct Stopping {} +impl ScheduledState for Stopping { + fn start(self: Box) -> Box { + Box::new(Running {}) + } + + fn stop(self: Box) -> Box { + self + } + + fn tick(self: Box, task: &mut dyn Task) -> Box { + task.stop(); + Box::new(Stopped {}) + } +} + +#[derive(Debug)] +struct Stopped {} +impl ScheduledState for Stopped { + fn start(self: Box) -> Box { + Box::new(Starting {}) + } + + fn stop(self: Box) -> Box { + self + } + + fn tick(self: Box, _task: &mut dyn Task) -> Box { + self + } +} + +struct ScheduledTask { + state: Option>, + task: Box +} + +impl std::fmt::Debug for ScheduledTask { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ScheduledTask") + .field("task", &self.task.name()) + .field("state", &self.state) + .finish() + } +} + +impl ScheduledTask { + fn new(task: Box) -> Self { + ScheduledTask { + state: Some(Box::new(Starting{})), + task: task + } + } + + fn start(&mut self) { + if let Some(s) = self.state.take() { + self.state = Some(s.start()); + } + } + + fn stop(&mut self) { + if let Some(s) = self.state.take() { + self.state = Some(s.stop()); + } + } + + fn tick(&mut self) { + if let Some(s) = self.state.take() { + self.state = Some(s.tick(self.task.as_mut())); + } + } +} + +pub struct Scheduler { + tasks: Vec +} + +impl Scheduler { + pub fn new(tasks: Vec>) -> Self { + let mut scheduled = Vec::new(); + for task in tasks { + log::info!("Scheduling task {:?}", task.name()); + scheduled.push(ScheduledTask::new(task)); + } + Scheduler { + tasks: scheduled + } + } + + pub fn tick(&mut self) { + for task in &mut self.tasks { + task.tick(); + } + } +} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..b9c4de1 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,30 @@ +use std::time::{Instant, Duration}; + +pub struct Periodically { + last_run: Instant, + duration: Duration +} + +impl Periodically { + pub fn new(duration: Duration) -> Self { + Self { + last_run: Instant::now(), + duration: duration + } + } + + pub fn new_every_n_seconds(seconds: u64) -> Self { + Self::new(Duration::new(seconds, 0)) + } + + pub fn new_every_n_ms(milliseconds: u32) -> Self { + Self::new(Duration::new(0, milliseconds*1000)) + } + + pub fn run(&mut self, f: F) where F: FnOnce() { + if self.last_run.elapsed() >= self.duration { + f(); + self.last_run = Instant::now(); + } + } +}