clean up imports and reorganize the ssd bits into a graphics mod
This commit is contained in:
241
src/graphics/display.rs
Normal file
241
src/graphics/display.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal, watch::{Receiver, Watch}};
|
||||
use figments::prelude::*;
|
||||
use core::{fmt::Debug, sync::atomic::{AtomicBool, AtomicU8}};
|
||||
use alloc::sync::Arc;
|
||||
|
||||
//use super::{Output};
|
||||
use figments_render::{
|
||||
gamma::{GammaCurve, WithGamma}, output::{Brightness, GammaCorrected, OutputAsync}, power::AsMilliwatts, smart_leds::PowerManagedWriterAsync
|
||||
};
|
||||
use smart_leds::SmartLedsWriteAsync;
|
||||
|
||||
pub const NUM_PIXELS: usize = 178;
|
||||
// FIXME: We need a way to specify a different buffer format from the 'native' hardware output, due to sometimes testing with GRB strips instead of RGB
|
||||
pub struct BikeOutput<T: SmartLedsWriteAsync> {
|
||||
pixbuf: [T::Color; NUM_PIXELS],
|
||||
writer: PowerManagedWriterAsync<T>,
|
||||
controls: DisplayControls
|
||||
}
|
||||
|
||||
impl<T:SmartLedsWriteAsync> GammaCorrected for BikeOutput<T> where T::Color: PixelBlend<Rgb<u8>> + PixelFormat + Debug + WithGamma + Default + Clone, T::Error: Debug {
|
||||
fn set_gamma(&mut self, gamma: GammaCurve) {
|
||||
self.writer.controls().set_gamma(gamma);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SmartLedsWriteAsync> BikeOutput<T> where T::Color: PixelBlend<Rgb<u8>> + PixelFormat + WithGamma + 'static + Default + Clone + Copy, T::Error: core::fmt::Debug {
|
||||
pub fn new(target: T, max_mw: u32, controls: DisplayControls) -> Self {
|
||||
Self {
|
||||
pixbuf: [Default::default(); NUM_PIXELS],
|
||||
writer: PowerManagedWriterAsync::new(target, max_mw),
|
||||
controls
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blank(&mut self) {
|
||||
self.pixbuf = [Default::default(); NUM_PIXELS];
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: SmartLedsWriteAsync + 'a> OutputAsync<'a, SegmentSpace> for BikeOutput<T> where T::Color: PixelSink<T::Color> + PixelBlend<Rgb<u8>> + Default + Clone + Debug + 'static + AsMilliwatts + PixelFormat + WithGamma, [T::Color; NUM_PIXELS]: AsMilliwatts + WithGamma + Copy, T::Error: core::fmt::Debug {
|
||||
async fn commit_async(&mut self) -> Result<(), T::Error> {
|
||||
self.writer.controls().set_brightness(self.controls.brightness());
|
||||
self.writer.controls().set_on(self.controls.is_on());
|
||||
// TODO: We should grab the power used here and somehow feed it back into the telemetry layer, probably just via another atomic u32
|
||||
self.writer.write(&self.pixbuf).await
|
||||
}
|
||||
|
||||
//type HardwarePixel = T::Color;
|
||||
type Error = T::Error;
|
||||
type Controls = DisplayControls;
|
||||
|
||||
fn controls(&self) -> Option<&Self::Controls> {
|
||||
Some(&self.controls)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: SmartLedsWriteAsync> Sample<'a, SegmentSpace> for BikeOutput<T> where T::Color: 'a + Debug + PixelFormat, [T::Color; NUM_PIXELS]: Sample<'a, SegmentSpace, Output = T::Color> {
|
||||
type Output = T::Color;
|
||||
|
||||
fn sample(&mut self, rect: &Rectangle<SegmentSpace>) -> impl Iterator<Item = (Coordinates<SegmentSpace>, &'a mut Self::Output)> {
|
||||
self.pixbuf.sample(rect)
|
||||
}
|
||||
}
|
||||
|
||||
const STRIP_MAP: [Segment; 6] = [
|
||||
Segment { start: 0, length: 28 },
|
||||
Segment { start: 28, length: 17 },
|
||||
Segment { start: 45, length: 14 },
|
||||
Segment { start: 59, length: 17 },
|
||||
Segment { start: 76, length: 14 },
|
||||
Segment { start: 90, length: 88 }
|
||||
];
|
||||
|
||||
pub struct Segment {
|
||||
start: usize,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl<'a, T: PixelFormat> Sample<'a, SegmentSpace> for [T; NUM_PIXELS] where T: 'static {
|
||||
type Output = T;
|
||||
|
||||
fn sample(&mut self, rect: &Rectangle<SegmentSpace>) -> impl Iterator<Item = (Coordinates<SegmentSpace>, &'a mut Self::Output)> {
|
||||
let bufref = unsafe {
|
||||
&mut *(self as *mut [T; NUM_PIXELS])
|
||||
};
|
||||
SegmentIter {
|
||||
pixbuf: bufref,
|
||||
cur: rect.top_left,
|
||||
end: rect.bottom_right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct SegmentSpace {}
|
||||
impl CoordinateSpace for SegmentSpace {
|
||||
type Data = usize;
|
||||
}
|
||||
|
||||
pub struct SegmentIter<'a, Format: PixelFormat> {
|
||||
pixbuf: &'a mut [Format; NUM_PIXELS],
|
||||
cur: Coordinates<SegmentSpace>,
|
||||
end: Coordinates<SegmentSpace>,
|
||||
}
|
||||
|
||||
impl<'a, Format: PixelFormat + Debug> Debug for SegmentIter<'a, Format> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("BikeIter").field("cur", &self.cur).field("end", &self.end).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Format: PixelFormat> Iterator for SegmentIter<'a, Format> {
|
||||
type Item = (Coordinates<SegmentSpace>, &'a mut Format);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.cur.y > self.end.y || self.cur.y >= STRIP_MAP.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let this_strip = &STRIP_MAP[self.cur.y];
|
||||
|
||||
let offset = this_strip.start + self.cur.x;
|
||||
let pixel_coords = Coordinates::new(self.cur.x, self.cur.y);
|
||||
|
||||
self.cur.x += 1;
|
||||
|
||||
if self.cur.x >= this_strip.length || self.cur.x > self.end.x {
|
||||
self.cur.x = 0;
|
||||
self.cur.y += 1;
|
||||
}
|
||||
|
||||
let bufref = unsafe {
|
||||
&mut *(self.pixbuf as *mut [Format; NUM_PIXELS])
|
||||
};
|
||||
|
||||
Some((pixel_coords, &mut bufref[offset]))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Uniforms {
|
||||
pub frame: usize,
|
||||
pub primary_color: Hsv
|
||||
}
|
||||
|
||||
struct ControlData {
|
||||
on: AtomicBool,
|
||||
brightness: AtomicU8
|
||||
}
|
||||
|
||||
impl Default for ControlData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
on: AtomicBool::new(true),
|
||||
brightness: AtomicU8::new(255)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A watch that indicates whether or not the rendering engine is running. If the display is off or sleeping, this will be false.
|
||||
static RENDER_IS_RUNNING: Watch<CriticalSectionRawMutex, bool, 7> = Watch::new();
|
||||
|
||||
// TODO: Implement something similar for a system-wide sleep mechanism
|
||||
pub struct DisplayControls {
|
||||
data: Arc<ControlData>,
|
||||
render_pause: Arc<Signal<CriticalSectionRawMutex, bool>>,
|
||||
render_run_receiver: Receiver<'static, CriticalSectionRawMutex, bool, 7>
|
||||
}
|
||||
|
||||
impl Clone for DisplayControls {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
data: Arc::clone(&self.data),
|
||||
render_pause: Arc::clone(&self.render_pause),
|
||||
render_run_receiver: RENDER_IS_RUNNING.receiver().expect("Could not create enough render running receivers")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayControls {
|
||||
pub fn is_on(&self) -> bool {
|
||||
self.data.on.load(core::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn brightness(&self) -> u8 {
|
||||
self.data.brightness.load(core::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
// FIXME: its a bit weird we have a pub function for the renderer's privates to wait while hiding render_pause, but directly expose render_is_running for any task to wait on
|
||||
pub async fn wait_until_display_is_on(&self) {
|
||||
if let Some(true) = self.render_pause.try_take() {
|
||||
while self.render_pause.wait().await {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_render_is_running(&mut self, value: bool) {
|
||||
RENDER_IS_RUNNING.sender().send(value);
|
||||
}
|
||||
|
||||
pub async fn wait_until_render_is_running(&mut self) {
|
||||
while !self.render_run_receiver.get().await {}
|
||||
}
|
||||
}
|
||||
|
||||
impl GammaCorrected for DisplayControls {
|
||||
fn set_gamma(&mut self, _gamma: GammaCurve) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Brightness for DisplayControls {
|
||||
fn set_brightness(&mut self, brightness: u8) {
|
||||
self.data.brightness.store(brightness, core::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn set_on(&mut self, is_on: bool) {
|
||||
self.data.on.store(is_on, core::sync::atomic::Ordering::Relaxed);
|
||||
self.render_pause.signal(!is_on);
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for DisplayControls {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
||||
f.debug_struct("DisplayControls")
|
||||
.field("on", &self.data.on)
|
||||
.field("brightness", &self.data.brightness)
|
||||
.field("render_pause", &self.render_pause.signaled())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DisplayControls {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data: Default::default(),
|
||||
render_pause: Default::default(),
|
||||
render_run_receiver: RENDER_IS_RUNNING.receiver().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,12 @@
|
||||
pub mod ssd1306;
|
||||
pub mod ssd1306;
|
||||
pub mod display;
|
||||
pub mod shaders;
|
||||
pub mod oled_ui;
|
||||
|
||||
mod images {
|
||||
use embedded_graphics::{
|
||||
image::ImageRaw,
|
||||
pixelcolor::BinaryColor
|
||||
};
|
||||
include!("../../target/images.rs");
|
||||
}
|
||||
182
src/graphics/oled_ui.rs
Normal file
182
src/graphics/oled_ui.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use core::f32::consts::PI;
|
||||
use core::fmt::Binary;
|
||||
|
||||
use alloc::format;
|
||||
use embedded_graphics::mono_font::ascii::*;
|
||||
use embedded_graphics::mono_font::{MonoTextStyle, MonoTextStyleBuilder};
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
use embedded_graphics::prelude::Size;
|
||||
use embedded_graphics::primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, StyledDrawable};
|
||||
use embedded_graphics::text::{Alignment, Text};
|
||||
use embedded_graphics::{image::Image, prelude::Point, Drawable};
|
||||
use figments::liber8tion::trig::sin8;
|
||||
use figments::mappings::embedded_graphics::Matrix2DSpace;
|
||||
use figments::{liber8tion::trig::cos8, mappings::embedded_graphics::EmbeddedGraphicsSampler};
|
||||
use figments::prelude::*;
|
||||
use nalgebra::Vector2;
|
||||
use micromath::F32Ext;
|
||||
use embedded_graphics::geometry::OriginDimensions;
|
||||
|
||||
use crate::graphics::images;
|
||||
use crate::{ego::engine::MotionState, events::Scene};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct OledUI {
|
||||
pub scene: Scene,
|
||||
pub motion: MotionState,
|
||||
pub brakelight: bool,
|
||||
pub headlight: bool,
|
||||
pub gps_online: bool,
|
||||
pub imu_online: bool,
|
||||
pub velocity: f32,
|
||||
pub location: Vector2<f64>,
|
||||
pub sleep: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct OledUniforms {
|
||||
pub ui: OledUI,
|
||||
pub frame: usize,
|
||||
pub current_screen: Screen
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub enum Screen {
|
||||
#[default]
|
||||
Blank,
|
||||
Bootsplash,
|
||||
Home,
|
||||
Sleeping,
|
||||
Waking
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
pub fn draw_screen<'a, T>(&self, sampler: &mut EmbeddedGraphicsSampler<'a, T>, state: &OledUniforms) where T: Sample<'a, Matrix2DSpace>, T::Output: PixelSink<BinaryColor> {
|
||||
match self {
|
||||
Screen::Blank => (),
|
||||
Screen::Bootsplash => {
|
||||
Image::new(&images::BOOT_LOGO, Point::zero()).draw(sampler).unwrap();
|
||||
const SPARKLE_COUNT: i32 = 8;
|
||||
for n in 0..SPARKLE_COUNT {
|
||||
let sparkle_center = Point::new(128u8.scale8(cos8(state.frame.wrapping_mul(n as usize))) as i32, 64u8.scale8(sin8(state.frame.wrapping_mul(n as usize) as u8)) as i32);
|
||||
let offset = (state.frame / 2 % 32) as i32 - 16;
|
||||
let rotation = PI * 2.0 * (sin8(state.frame) as f32 / 255.0);
|
||||
let normal = Point::new((rotation.cos() * offset as f32) as i32, (rotation.sin() * offset as f32) as i32);
|
||||
let cross_normal = Point::new((rotation.sin() * offset as f32) as i32, (rotation.cos() * offset as f32) as i32);
|
||||
// Draw horizontal
|
||||
Line::new(sparkle_center - normal, sparkle_center + normal)
|
||||
.draw_styled(&SPARKLE_STYLE, sampler).unwrap();
|
||||
// Draw vertical
|
||||
Line::new(sparkle_center - cross_normal, sparkle_center + cross_normal)
|
||||
.draw_styled(&SPARKLE_STYLE, sampler).unwrap();
|
||||
}
|
||||
},
|
||||
Screen::Home => {
|
||||
// Status bar
|
||||
// Sensor indicators
|
||||
let gps_img = if state.ui.gps_online {
|
||||
&images::GPS_ON
|
||||
} else {
|
||||
&images::GPS_OFF
|
||||
};
|
||||
let imu_img = if state.ui.imu_online {
|
||||
&images::IMU_ON
|
||||
} else {
|
||||
&images::IMU_OFF
|
||||
};
|
||||
Image::new(gps_img, Point::zero()).draw(sampler).unwrap();
|
||||
Image::new(imu_img, Point::new((gps_img.size().width + 2) as i32, 0)).draw(sampler).unwrap();
|
||||
|
||||
#[cfg(feature="demo")]
|
||||
Text::with_alignment("Demo", Point::new(128, 10), TEXT_STYLE, Alignment::Right)
|
||||
.draw(sampler)
|
||||
.unwrap();
|
||||
|
||||
#[cfg(feature="simulation")]
|
||||
Text::with_alignment("Sim", Point::new(128, 10), TEXT_STYLE, Alignment::Right)
|
||||
.draw(sampler)
|
||||
.unwrap();
|
||||
|
||||
// Separates the status bar from the UI
|
||||
Line::new(Point::new(0, 18), Point::new(128, 18)).draw_styled(&INACTIVE_STYLE, sampler).unwrap();
|
||||
|
||||
// Speed display at the top
|
||||
Text::with_alignment(&format!("{}", state.ui.velocity), Point::new(128 / 2, 12), SPEED_STYLE, Alignment::Center)
|
||||
.draw(sampler)
|
||||
.unwrap();
|
||||
|
||||
// The main UI content
|
||||
Image::new(&images::BIKE, Point::new((128 / 2 - images::BIKE.size().width / 2) as i32, 24)).draw(sampler).unwrap();
|
||||
|
||||
let headlight_img = if state.ui.headlight {
|
||||
&images::HEADLIGHT_ON
|
||||
} else {
|
||||
&images::HEADLIGHT_OFF
|
||||
};
|
||||
let brakelight_img = if state.ui.brakelight {
|
||||
&images::BRAKELIGHT_ON
|
||||
} else {
|
||||
&images::BRAKELIGHT_OFF
|
||||
};
|
||||
Image::new(headlight_img, Point::new(((128 / 2 - images::BIKE.size().width / 2) - 18) as i32, 28)).draw(sampler).unwrap();
|
||||
Image::new(brakelight_img, Point::new(((128 / 2 + images::BIKE.size().width / 2) + 2) as i32, 28)).draw(sampler).unwrap();
|
||||
|
||||
// TODO: Replace the state texts with cute animations or smth
|
||||
// Current prediction from the motion engine
|
||||
Text::with_alignment(&format!("{:?}", state.ui.motion), Point::new(128 / 2, 64 - 3), TEXT_STYLE, Alignment::Center)
|
||||
.draw(sampler)
|
||||
.unwrap();
|
||||
|
||||
// Current scene in the UI
|
||||
Text::with_alignment(&format!("{:?}", state.ui.scene), Point::new(128 / 2, 64 - 13), TEXT_STYLE, Alignment::Center)
|
||||
.draw(sampler)
|
||||
.unwrap();
|
||||
},
|
||||
Screen::Sleeping => {
|
||||
Text::with_alignment("so eepy Zzz...", Point::new(128 / 2, 64 / 2), LOGO_STYLE, Alignment::Center)
|
||||
.draw(sampler)
|
||||
.unwrap();
|
||||
},
|
||||
Screen::Waking => {
|
||||
Text::with_alignment("OwO hewwo again :D", Point::new(128 / 2, 64 / 2), LOGO_STYLE, Alignment::Center)
|
||||
.draw(sampler)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<HwPixel: PixelSink<BinaryColor>> RenderSource<OledUniforms, Matrix2DSpace, BinaryColor, HwPixel> for Screen {
|
||||
fn render_to<'a, Smp>(&'a self, output: &'a mut Smp, uniforms: &OledUniforms)
|
||||
where
|
||||
Smp: Sample<'a, Matrix2DSpace, Output = HwPixel> {
|
||||
let mut sampler = EmbeddedGraphicsSampler(output, embedded_graphics::primitives::Rectangle::new(Point::new(0, 0), Size::new(128, 64)));
|
||||
self.draw_screen(&mut sampler, uniforms);
|
||||
}
|
||||
}
|
||||
|
||||
const SPARKLE_STYLE: PrimitiveStyle<BinaryColor> = PrimitiveStyleBuilder::new()
|
||||
.stroke_color(BinaryColor::On)
|
||||
.stroke_width(2)
|
||||
.build();
|
||||
|
||||
const TEXT_STYLE: MonoTextStyle<BinaryColor> = MonoTextStyleBuilder::new()
|
||||
.font(&FONT_6X10)
|
||||
.text_color(BinaryColor::On)
|
||||
.build();
|
||||
|
||||
const LOGO_STYLE: MonoTextStyle<BinaryColor> = MonoTextStyleBuilder::new()
|
||||
.font(&FONT_9X18_BOLD)
|
||||
.text_color(BinaryColor::On)
|
||||
.build();
|
||||
|
||||
const SPEED_STYLE: MonoTextStyle<BinaryColor> = MonoTextStyleBuilder::new()
|
||||
.font(&FONT_10X20)
|
||||
.text_color(BinaryColor::On)
|
||||
.build();
|
||||
|
||||
const INACTIVE_STYLE: PrimitiveStyle<BinaryColor> = PrimitiveStyleBuilder::new()
|
||||
.fill_color(BinaryColor::Off)
|
||||
.stroke_color(BinaryColor::On)
|
||||
.stroke_width(2)
|
||||
.build();
|
||||
149
src/graphics/shaders.rs
Normal file
149
src/graphics/shaders.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use core::cmp::max;
|
||||
|
||||
use figments::{liber8tion::{interpolate::{ease_in_out_quad}, noise::inoise8, trig::{cos8, sin8}}, prelude::*};
|
||||
use rgb::Rgba;
|
||||
|
||||
use crate::graphics::display::{SegmentSpace, Uniforms};
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Movement {
|
||||
reverse: bool
|
||||
}
|
||||
|
||||
impl Movement {
|
||||
pub fn reversed(self) -> Self {
|
||||
Self {
|
||||
reverse: !self.reverse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Movement {
|
||||
fn draw(&self, surface_coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
|
||||
let offset = if self.reverse {
|
||||
uniforms.frame.wrapping_add(surface_coords.x)
|
||||
} else {
|
||||
uniforms.frame.wrapping_sub(surface_coords.x)
|
||||
};
|
||||
let idx = sin8(offset).wrapping_add(uniforms.primary_color.hue);
|
||||
Rgba::new(idx, idx.wrapping_mul(2), idx.wrapping_div(2), 128)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Background {
|
||||
color: Option<Rgb<u8>>
|
||||
}
|
||||
|
||||
impl Background {
|
||||
pub fn from_color(color: Rgb<u8>) -> Self {
|
||||
Self {
|
||||
color: Some(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Background {
|
||||
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
|
||||
let noise_x = sin8(uniforms.frame % 255) as i16;
|
||||
let noise_y = cos8(uniforms.frame % 255) as i16;
|
||||
let brightness = inoise8(noise_x.wrapping_add(coords.x as i16), noise_y.wrapping_add(coords.y as i16));
|
||||
let saturation = inoise8(noise_y.wrapping_add(coords.y as i16), noise_x.wrapping_add(coords.x as i16));
|
||||
let rgb: Rgb<u8> = match self.color {
|
||||
None => Hsv::new(uniforms.primary_color.hue, max(128, saturation), brightness).into(),
|
||||
Some(c) => c
|
||||
};
|
||||
|
||||
Rgba::new(rgb.r, rgb.g, rgb.b, 255)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Tail {}
|
||||
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Tail {
|
||||
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
|
||||
let hue_offset: u8 = 32.scale8(sin8(uniforms.frame.wrapping_sub(coords.x)));
|
||||
let value = max(30, inoise8(coords.x.wrapping_add(uniforms.frame) as i16, coords.y.wrapping_add(uniforms.frame) as i16));
|
||||
Hsv::new(uniforms.primary_color.hue.wrapping_sub(hue_offset), max(210, sin8(uniforms.frame.wrapping_add(coords.x))), value).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Brakelight {}
|
||||
|
||||
impl Brakelight {
|
||||
pub const fn end() -> usize {
|
||||
87
|
||||
}
|
||||
|
||||
pub const fn length() -> usize {
|
||||
50
|
||||
}
|
||||
|
||||
pub const fn safety_length() -> usize {
|
||||
15
|
||||
}
|
||||
|
||||
pub const fn rectangle() -> Rectangle<SegmentSpace> {
|
||||
Rectangle::new_from_coordinates(Self::end() - Self::length(), 5, Self::end(), 5)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Brakelight {
|
||||
|
||||
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
|
||||
let distance_from_end = Self::end() - coords.x;
|
||||
|
||||
if distance_from_end < Self::safety_length() {
|
||||
Rgba::new(max(128, sin8(uniforms.frame.wrapping_sub(coords.x))), 0, 0, 255)
|
||||
} else {
|
||||
let pct = (distance_from_end as f32 / Self::length() as f32) * 255f32;
|
||||
Rgba::new(max(100, ease_in_out_quad((pct as u8).wrapping_add(uniforms.frame as u8))), 0, 0, ease_in_out_quad(255 - pct as u8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Headlight {}
|
||||
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Headlight {
|
||||
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
|
||||
Hsv::new(0, 0, max(130, ease_in_out_quad(sin8(uniforms.frame.wrapping_sub(coords.x))))).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Panel {}
|
||||
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Panel {
|
||||
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
|
||||
let noise_offset = max(180, inoise8(coords.x.wrapping_add(uniforms.frame) as i16, coords.y.wrapping_add(uniforms.frame) as i16));
|
||||
let pct = (coords.x as f32 / 18f32) * 255f32;
|
||||
let shift = match coords.y {
|
||||
1..=2 => 106, // 150 degrees
|
||||
3..=4 => 148, // 210 degrees
|
||||
_ => 0
|
||||
};
|
||||
Hsv::new(uniforms.primary_color.hue.wrapping_add(shift), noise_offset, max(100, sin8(pct as u8).wrapping_add(uniforms.frame as u8))).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Thinking {}
|
||||
|
||||
impl Shader<Uniforms, SegmentSpace, Rgba<u8>> for Thinking {
|
||||
fn draw(&self, coords: &Coordinates<SegmentSpace>, uniforms: &Uniforms) -> Rgba<u8> {
|
||||
//let noise_x = sin8(sin8((frame % 255) as u8).wrapping_add(coords.x));
|
||||
//let noise_y = cos8(cos8((frame % 255) as u8).wrapping_add(coords.y));
|
||||
let offset_x = sin8(uniforms.frame.wrapping_add(coords.x));
|
||||
let offset_y = cos8(uniforms.frame.wrapping_add(coords.y));
|
||||
let noise_x = offset_x / 2;
|
||||
let noise_y = offset_y / 2;
|
||||
//let noise_x = coords.x.wrapping_add(offset_x);
|
||||
//let noise_y = coords.y.wrapping_add(offset_y);
|
||||
Hsv::new(
|
||||
inoise8(offset_x as i16, offset_y as i16),
|
||||
128_u8.saturating_add(inoise8(noise_y.into(), noise_x.into())),
|
||||
255
|
||||
).into()
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
#![cfg(feature="oled")]
|
||||
use core::{cmp::min, sync::atomic::{AtomicBool, AtomicU8}};
|
||||
use core::cmp::min;
|
||||
|
||||
use alloc::sync::Arc;
|
||||
use display_interface::DisplayError;
|
||||
use embedded_graphics::prelude::*;
|
||||
use esp_hal::rng;
|
||||
use esp_hal::{gpio::Output, i2c::master::I2c, Async};
|
||||
use figments::{liber8tion::{interpolate::Fract8, noise}, mappings::embedded_graphics::Matrix2DSpace, prelude::*};
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
use figments::pixels::PixelSink;
|
||||
use figments_render::{gamma::GammaCurve, output::{Brightness, GammaCorrected, NullControls, OutputAsync}};
|
||||
use log::*;
|
||||
use figments_render::output::OutputAsync;
|
||||
use ssd1306::{prelude::DisplayRotation, size::DisplaySize128x64, I2CDisplayInterface, Ssd1306Async};
|
||||
use embassy_time::Delay;
|
||||
|
||||
pub struct SsdOutput([u8; 128 * 64 / 8], ssd1306::Ssd1306Async<ssd1306::prelude::I2CInterface<esp_hal::i2c::master::I2c<'static, esp_hal::Async>>, ssd1306::prelude::DisplaySize128x64, ssd1306::mode::BasicMode>, SsdControls);
|
||||
use crate::graphics::display::DisplayControls;
|
||||
|
||||
pub struct SsdOutput {
|
||||
pixbuf: [u8; 128 * 64 / 8],
|
||||
target: ssd1306::Ssd1306Async<ssd1306::prelude::I2CInterface<esp_hal::i2c::master::I2c<'static, esp_hal::Async>>, ssd1306::prelude::DisplaySize128x64, ssd1306::mode::BasicMode>,
|
||||
controls: DisplayControls,
|
||||
is_on: bool,
|
||||
last_brightness: u8
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct SsdPixel(*mut u8, u8, Coordinates<Matrix2DSpace>);
|
||||
@@ -42,7 +50,7 @@ impl PixelSink<BinaryColor> for SsdPixel {
|
||||
|
||||
impl PixelBlend<BinaryColor> for SsdPixel {
|
||||
fn blend_pixel(self, overlay: BinaryColor, opacity: Fract8) -> Self {
|
||||
let scale = 32;
|
||||
let scale = 48;
|
||||
let x = self.2.x * scale;
|
||||
let y = self.2.y * scale;
|
||||
let stiple_idx = noise::inoise8(x as i16, y as i16);
|
||||
@@ -60,18 +68,6 @@ impl PixelBlend<BinaryColor> for SsdPixel {
|
||||
}
|
||||
}
|
||||
|
||||
static mut DUMMY: u8 = 0;
|
||||
|
||||
impl Default for SsdPixel {
|
||||
fn default() -> Self {
|
||||
Self (
|
||||
unsafe { &mut DUMMY },
|
||||
0,
|
||||
Coordinates::new(0, 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SsdSampler<'a> {
|
||||
buf: &'a mut SsdOutput,
|
||||
rect: figments::prelude::Rectangle<Matrix2DSpace>,
|
||||
@@ -110,12 +106,23 @@ impl SsdOutput {
|
||||
let idx = (coords.y / 8 * 128 + coords.x) as usize;
|
||||
let bit = (coords.y % 8) as u8;
|
||||
//info!("sample {coords:?} {idx}");
|
||||
let pixref = unsafe { &mut self.0[idx] as *mut u8};
|
||||
let pixref = &mut self.pixbuf[idx] as *mut u8;
|
||||
Some(SsdPixel(pixref, bit, coords))
|
||||
}
|
||||
|
||||
pub fn new(display: ssd1306::Ssd1306Async<ssd1306::prelude::I2CInterface<esp_hal::i2c::master::I2c<'static, esp_hal::Async>>, ssd1306::prelude::DisplaySize128x64, ssd1306::mode::BasicMode>) -> Self {
|
||||
Self([0; 128 * 64 / 8], display, Default::default())
|
||||
pub async fn new(i2c: I2c<'static, Async>, mut reset_pin: Output<'static>, controls: DisplayControls) -> Self {
|
||||
let interface = I2CDisplayInterface::new(i2c);
|
||||
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0);
|
||||
display.reset(&mut reset_pin, &mut Delay).await.unwrap();
|
||||
display.init_with_addr_mode(ssd1306::command::AddrMode::Horizontal).await.unwrap();
|
||||
|
||||
Self {
|
||||
pixbuf: [0; 128 * 64 / 8],
|
||||
target: display,
|
||||
controls,
|
||||
last_brightness: 255,
|
||||
is_on: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,12 +155,6 @@ impl DrawTarget for SsdOutput {
|
||||
where
|
||||
I: IntoIterator<Item = Pixel<Self::Color>> {
|
||||
for epix in pixels {
|
||||
/*let point = figments::geometry::Rectangle::new(
|
||||
epix.0.into(), epix.0.into()
|
||||
);
|
||||
for (coords, pix) in self.sample(&point) {
|
||||
pix.set_pixel(epix.1);
|
||||
}*/
|
||||
self.get_pixel(epix.0.into()).unwrap().set_pixel(epix.1);
|
||||
}
|
||||
Ok(())
|
||||
@@ -166,7 +167,7 @@ impl DrawTarget for SsdOutput {
|
||||
area.top_left.into(),
|
||||
area.bottom_right().unwrap().into()
|
||||
);
|
||||
for (color, (coords, pix)) in colors.into_iter().zip(self.sample(&rect)) {
|
||||
for (color, (_coords, pix)) in colors.into_iter().zip(self.sample(&rect)) {
|
||||
pix.set_pixel(color);
|
||||
}
|
||||
Ok(())
|
||||
@@ -177,7 +178,7 @@ impl DrawTarget for SsdOutput {
|
||||
area.top_left.into(),
|
||||
area.bottom_right().unwrap().into()
|
||||
);
|
||||
for (coords, pix) in self.sample(&rect) {
|
||||
for (_coords, pix) in self.sample(&rect) {
|
||||
pix.set_pixel(color);
|
||||
}
|
||||
Ok(())
|
||||
@@ -185,9 +186,9 @@ impl DrawTarget for SsdOutput {
|
||||
|
||||
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
|
||||
if color.is_on() {
|
||||
self.0.fill(255);
|
||||
self.pixbuf.fill(255);
|
||||
} else {
|
||||
self.0.fill(0);
|
||||
self.pixbuf.fill(0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -202,50 +203,24 @@ impl OriginDimensions for SsdOutput {
|
||||
impl<'a> OutputAsync<'a, Matrix2DSpace> for SsdOutput {
|
||||
type Error = DisplayError;
|
||||
|
||||
type Controls = SsdControls;
|
||||
type Controls = DisplayControls;
|
||||
|
||||
async fn commit_async(&mut self) -> Result<(), Self::Error> {
|
||||
self.1.set_brightness(ssd1306::prelude::Brightness::custom(1, self.2.data.brightness.load(core::sync::atomic::Ordering::Relaxed))).await.unwrap();
|
||||
self.1.set_display_on(self.2.data.is_on.load(core::sync::atomic::Ordering::Relaxed)).await.unwrap();
|
||||
self.1.draw(&self.0).await
|
||||
let new_brightness = self.controls.brightness();
|
||||
let new_power = self.controls.is_on();
|
||||
if self.is_on != new_power {
|
||||
self.target.set_display_on(self.controls.is_on()).await.unwrap();
|
||||
self.is_on = new_power;
|
||||
}
|
||||
if self.last_brightness != new_brightness {
|
||||
self.target.set_brightness(ssd1306::prelude::Brightness::custom(1, new_brightness)).await.unwrap();
|
||||
self.last_brightness = new_brightness;
|
||||
}
|
||||
|
||||
self.target.draw(&self.pixbuf).await
|
||||
}
|
||||
|
||||
fn controls(&self) -> Option<&Self::Controls> {
|
||||
Some(&self.2)
|
||||
Some(&self.controls)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We can probably just replace this entirely with a DisplayControls instance
|
||||
#[derive(Debug)]
|
||||
struct SsdControlData {
|
||||
is_on: AtomicBool,
|
||||
brightness: AtomicU8
|
||||
}
|
||||
|
||||
impl Default for SsdControlData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
is_on: AtomicBool::new(true),
|
||||
brightness: AtomicU8::new(255)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SsdControls {
|
||||
data: Arc<SsdControlData>
|
||||
}
|
||||
|
||||
impl Brightness for SsdControls {
|
||||
fn set_brightness(&mut self, brightness: u8) {
|
||||
self.data.brightness.store(brightness, core::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn set_on(&mut self, is_on: bool) {
|
||||
self.data.is_on.store(is_on, core::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl GammaCorrected for SsdControls {
|
||||
fn set_gamma(&mut self, gamma: GammaCurve) {} // Not handled
|
||||
}
|
||||
Reference in New Issue
Block a user