clean up imports and reorganize the ssd bits into a graphics mod

This commit is contained in:
2025-10-26 11:14:28 +01:00
parent 27e92edef0
commit 4776227793
17 changed files with 396 additions and 375 deletions

241
src/graphics/display.rs Normal file
View 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()
}
}
}

View File

@@ -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
View 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
View 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()
}
}

View File

@@ -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
}