clean up imports and reorganize the ssd bits into a graphics mod
This commit is contained in:
@@ -21,7 +21,7 @@ max-usb-power = []
|
|||||||
wokwi = ["max-usb-power"]
|
wokwi = ["max-usb-power"]
|
||||||
mpu = ["dep:mpu6050-dmp"]
|
mpu = ["dep:mpu6050-dmp"]
|
||||||
gps = ["dep:nmea"]
|
gps = ["dep:nmea"]
|
||||||
oled = ["dep:ssd1306", "dep:embedded-graphics"]
|
oled = ["dep:ssd1306"]
|
||||||
demo = []
|
demo = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -77,7 +77,7 @@ esp-wifi = { version = "0.15.0", optional = true, features = [
|
|||||||
"ble"
|
"ble"
|
||||||
] }
|
] }
|
||||||
bleps = { git = "https://github.com/bjoernQ/bleps", optional = true, package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [ "macros", "async"] }
|
bleps = { git = "https://github.com/bjoernQ/bleps", optional = true, package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [ "macros", "async"] }
|
||||||
embedded-graphics = { version = "0.8.1", features = ["nalgebra_support"], optional = true}
|
embedded-graphics = { version = "0.8.1", features = ["nalgebra_support"] }
|
||||||
ssd1306 = { version = "0.10.0", features = ["async"], optional = true }
|
ssd1306 = { version = "0.10.0", features = ["async"], optional = true }
|
||||||
|
|
||||||
# Sensors
|
# Sensors
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use figments_render::output::Brightness;
|
|||||||
use core::{fmt::Debug, ops::{Deref, DerefMut}};
|
use core::{fmt::Debug, ops::{Deref, DerefMut}};
|
||||||
use log::*;
|
use log::*;
|
||||||
|
|
||||||
use crate::display::DisplayControls;
|
use crate::graphics::display::DisplayControls;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy)]
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
pub struct Animation {
|
pub struct Animation {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
use core::ptr::addr_of_mut;
|
use core::ptr::addr_of_mut;
|
||||||
|
|
||||||
|
use alloc::sync::Arc;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_time::{Instant, Timer};
|
use embassy_time::{Instant, Timer};
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ use esp_hal::{
|
|||||||
|
|
||||||
use esp_hal_embassy::{Executor, InterruptExecutor};
|
use esp_hal_embassy::{Executor, InterruptExecutor};
|
||||||
use log::*;
|
use log::*;
|
||||||
use renderbug_embassy::{logging::RenderbugLogger, tasks::{safetyui::{safety_ui_main, SafetyUi}, ui::UiSurfacePool}};
|
use renderbug_embassy::{logging::RenderbugLogger, tasks::{oled::{oled_ui, OledUI, OledUiSurfacePool}, safetyui::{safety_ui_main, SafetyUi}, ui::UiSurfacePool}};
|
||||||
use renderbug_embassy::events::BusGarage;
|
use renderbug_embassy::events::BusGarage;
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
use esp_backtrace as _;
|
use esp_backtrace as _;
|
||||||
@@ -29,6 +30,8 @@ use renderbug_embassy::tasks::{
|
|||||||
ui::{Ui, ui_main}
|
ui::{Ui, ui_main}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use figments_render::output::OutputAsync;
|
||||||
|
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
// This creates a default app-descriptor required by the esp-idf bootloader.
|
// This creates a default app-descriptor required by the esp-idf bootloader.
|
||||||
@@ -68,6 +71,11 @@ async fn main(spawner: Spawner) {
|
|||||||
info!("Setting up rendering pipeline");
|
info!("Setting up rendering pipeline");
|
||||||
let mut surfaces = UiSurfacePool::default();
|
let mut surfaces = UiSurfacePool::default();
|
||||||
let ui = Ui::new(&mut surfaces);
|
let ui = Ui::new(&mut surfaces);
|
||||||
|
|
||||||
|
let mut oled_surfaces = OledUiSurfacePool::default();
|
||||||
|
let oled_uniforms = Default::default();
|
||||||
|
let oledui = OledUI::new(&mut oled_surfaces, garage.oled_display.clone(), Arc::clone(&oled_uniforms));
|
||||||
|
|
||||||
let mut safety_surfaces = UiSurfacePool::default();
|
let mut safety_surfaces = UiSurfacePool::default();
|
||||||
let safety_ui = SafetyUi::new(&mut safety_surfaces, garage.display.clone());
|
let safety_ui = SafetyUi::new(&mut safety_surfaces, garage.display.clone());
|
||||||
|
|
||||||
@@ -102,14 +110,15 @@ async fn main(spawner: Spawner) {
|
|||||||
#[cfg(feature="oled")]
|
#[cfg(feature="oled")]
|
||||||
{
|
{
|
||||||
use esp_hal::i2c::master::{Config, I2c};
|
use esp_hal::i2c::master::{Config, I2c};
|
||||||
|
use renderbug_embassy::graphics::ssd1306::SsdOutput;
|
||||||
|
|
||||||
let rst = Output::new(peripherals.GPIO21, esp_hal::gpio::Level::Low, OutputConfig::default());
|
let rst = Output::new(peripherals.GPIO21, esp_hal::gpio::Level::Low, OutputConfig::default());
|
||||||
let i2c = I2c::new(
|
let i2c = I2c::new(
|
||||||
peripherals.I2C0,
|
peripherals.I2C0,
|
||||||
Config::default().with_frequency(Rate::from_khz(400))
|
Config::default().with_frequency(Rate::from_khz(400))
|
||||||
).unwrap().with_scl(peripherals.GPIO18).with_sda(peripherals.GPIO17).into_async();
|
).unwrap().with_scl(peripherals.GPIO18).with_sda(peripherals.GPIO17).into_async();
|
||||||
|
let output = SsdOutput::new(i2c, rst, garage.oled_display.clone()).await;
|
||||||
spawner.must_spawn(renderbug_embassy::tasks::oled::oled_task(i2c, rst, garage.telemetry.dyn_subscriber().unwrap()));
|
spawner.must_spawn(renderbug_embassy::tasks::oled_render::oled_render(output, oled_surfaces, oled_uniforms));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature="simulation")]
|
#[cfg(feature="simulation")]
|
||||||
@@ -136,6 +145,8 @@ async fn main(spawner: Spawner) {
|
|||||||
spawner.must_spawn(safety_ui_main(garage.notify.dyn_subscriber().unwrap(), safety_ui));
|
spawner.must_spawn(safety_ui_main(garage.notify.dyn_subscriber().unwrap(), safety_ui));
|
||||||
info!("Launching UI");
|
info!("Launching UI");
|
||||||
spawner.must_spawn(ui_main(garage.notify.dyn_subscriber().unwrap(), garage.telemetry.dyn_publisher().unwrap(), ui));
|
spawner.must_spawn(ui_main(garage.notify.dyn_subscriber().unwrap(), garage.telemetry.dyn_publisher().unwrap(), ui));
|
||||||
|
info!("Launching OLED UI");
|
||||||
|
spawner.must_spawn(oled_ui(garage.telemetry.dyn_subscriber().unwrap(), oledui));
|
||||||
#[cfg(feature="demo")]
|
#[cfg(feature="demo")]
|
||||||
{
|
{
|
||||||
warn!("Launching with demo sequencer");
|
warn!("Launching with demo sequencer");
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use embassy_sync::{blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex},
|
|||||||
use embassy_time::Duration;
|
use embassy_time::Duration;
|
||||||
use nalgebra::{Vector2, Vector3};
|
use nalgebra::{Vector2, Vector3};
|
||||||
|
|
||||||
use crate::{display::DisplayControls, ego::engine::MotionState};
|
use crate::{graphics::display::DisplayControls, ego::engine::MotionState};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, Debug)]
|
#[derive(Clone, Copy, Default, Debug)]
|
||||||
pub enum Scene {
|
pub enum Scene {
|
||||||
@@ -80,7 +80,8 @@ pub struct BusGarage {
|
|||||||
pub notify: PubSubChannel<CriticalSectionRawMutex, Notification, 5, 2, 4>,
|
pub notify: PubSubChannel<CriticalSectionRawMutex, Notification, 5, 2, 4>,
|
||||||
pub predict: Channel<CriticalSectionRawMutex, Prediction, 15>,
|
pub predict: Channel<CriticalSectionRawMutex, Prediction, 15>,
|
||||||
pub telemetry: PubSubChannel<CriticalSectionRawMutex, Telemetry, 15, 2, 4>,
|
pub telemetry: PubSubChannel<CriticalSectionRawMutex, Telemetry, 15, 2, 4>,
|
||||||
pub display: DisplayControls
|
pub display: DisplayControls,
|
||||||
|
pub oled_display: DisplayControls
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BusGarage {
|
impl Default for BusGarage {
|
||||||
@@ -90,7 +91,8 @@ impl Default for BusGarage {
|
|||||||
notify: PubSubChannel::new(),
|
notify: PubSubChannel::new(),
|
||||||
predict: Channel::new(),
|
predict: Channel::new(),
|
||||||
telemetry: PubSubChannel::new(),
|
telemetry: PubSubChannel::new(),
|
||||||
display: Default::default()
|
display: Default::default(),
|
||||||
|
oled_display: Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,13 +159,13 @@ impl Default for ControlData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A watch that indicates whether or not the rendering engine is running. If the display is off or sleeping, this will be false.
|
// 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, 5> = Watch::new();
|
static RENDER_IS_RUNNING: Watch<CriticalSectionRawMutex, bool, 7> = Watch::new();
|
||||||
|
|
||||||
// TODO: Implement something similar for a system-wide sleep mechanism
|
// TODO: Implement something similar for a system-wide sleep mechanism
|
||||||
pub struct DisplayControls {
|
pub struct DisplayControls {
|
||||||
data: Arc<ControlData>,
|
data: Arc<ControlData>,
|
||||||
render_pause: Arc<Signal<CriticalSectionRawMutex, bool>>,
|
render_pause: Arc<Signal<CriticalSectionRawMutex, bool>>,
|
||||||
render_run_receiver: Receiver<'static, CriticalSectionRawMutex, bool, 5>
|
render_run_receiver: Receiver<'static, CriticalSectionRawMutex, bool, 7>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for DisplayControls {
|
impl Clone for DisplayControls {
|
||||||
@@ -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();
|
||||||
@@ -3,7 +3,7 @@ use core::cmp::max;
|
|||||||
use figments::{liber8tion::{interpolate::{ease_in_out_quad}, noise::inoise8, trig::{cos8, sin8}}, prelude::*};
|
use figments::{liber8tion::{interpolate::{ease_in_out_quad}, noise::inoise8, trig::{cos8, sin8}}, prelude::*};
|
||||||
use rgb::Rgba;
|
use rgb::Rgba;
|
||||||
|
|
||||||
use crate::display::{SegmentSpace, Uniforms};
|
use crate::graphics::display::{SegmentSpace, Uniforms};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy, Default)]
|
||||||
pub struct Movement {
|
pub struct Movement {
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
#![cfg(feature="oled")]
|
#![cfg(feature="oled")]
|
||||||
use core::{cmp::min, sync::atomic::{AtomicBool, AtomicU8}};
|
use core::cmp::min;
|
||||||
|
|
||||||
use alloc::sync::Arc;
|
|
||||||
use display_interface::DisplayError;
|
use display_interface::DisplayError;
|
||||||
use embedded_graphics::prelude::*;
|
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 figments::{liber8tion::{interpolate::Fract8, noise}, mappings::embedded_graphics::Matrix2DSpace, prelude::*};
|
||||||
use embedded_graphics::pixelcolor::BinaryColor;
|
use embedded_graphics::pixelcolor::BinaryColor;
|
||||||
use figments::pixels::PixelSink;
|
use figments::pixels::PixelSink;
|
||||||
use figments_render::{gamma::GammaCurve, output::{Brightness, GammaCorrected, NullControls, OutputAsync}};
|
use figments_render::output::OutputAsync;
|
||||||
use log::*;
|
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)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct SsdPixel(*mut u8, u8, Coordinates<Matrix2DSpace>);
|
pub struct SsdPixel(*mut u8, u8, Coordinates<Matrix2DSpace>);
|
||||||
@@ -42,7 +50,7 @@ impl PixelSink<BinaryColor> for SsdPixel {
|
|||||||
|
|
||||||
impl PixelBlend<BinaryColor> for SsdPixel {
|
impl PixelBlend<BinaryColor> for SsdPixel {
|
||||||
fn blend_pixel(self, overlay: BinaryColor, opacity: Fract8) -> Self {
|
fn blend_pixel(self, overlay: BinaryColor, opacity: Fract8) -> Self {
|
||||||
let scale = 32;
|
let scale = 48;
|
||||||
let x = self.2.x * scale;
|
let x = self.2.x * scale;
|
||||||
let y = self.2.y * scale;
|
let y = self.2.y * scale;
|
||||||
let stiple_idx = noise::inoise8(x as i16, y as i16);
|
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> {
|
pub struct SsdSampler<'a> {
|
||||||
buf: &'a mut SsdOutput,
|
buf: &'a mut SsdOutput,
|
||||||
rect: figments::prelude::Rectangle<Matrix2DSpace>,
|
rect: figments::prelude::Rectangle<Matrix2DSpace>,
|
||||||
@@ -110,12 +106,23 @@ impl SsdOutput {
|
|||||||
let idx = (coords.y / 8 * 128 + coords.x) as usize;
|
let idx = (coords.y / 8 * 128 + coords.x) as usize;
|
||||||
let bit = (coords.y % 8) as u8;
|
let bit = (coords.y % 8) as u8;
|
||||||
//info!("sample {coords:?} {idx}");
|
//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))
|
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 {
|
pub async fn new(i2c: I2c<'static, Async>, mut reset_pin: Output<'static>, controls: DisplayControls) -> Self {
|
||||||
Self([0; 128 * 64 / 8], display, Default::default())
|
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
|
where
|
||||||
I: IntoIterator<Item = Pixel<Self::Color>> {
|
I: IntoIterator<Item = Pixel<Self::Color>> {
|
||||||
for epix in pixels {
|
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);
|
self.get_pixel(epix.0.into()).unwrap().set_pixel(epix.1);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -166,7 +167,7 @@ impl DrawTarget for SsdOutput {
|
|||||||
area.top_left.into(),
|
area.top_left.into(),
|
||||||
area.bottom_right().unwrap().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);
|
pix.set_pixel(color);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -177,7 +178,7 @@ impl DrawTarget for SsdOutput {
|
|||||||
area.top_left.into(),
|
area.top_left.into(),
|
||||||
area.bottom_right().unwrap().into()
|
area.bottom_right().unwrap().into()
|
||||||
);
|
);
|
||||||
for (coords, pix) in self.sample(&rect) {
|
for (_coords, pix) in self.sample(&rect) {
|
||||||
pix.set_pixel(color);
|
pix.set_pixel(color);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -185,9 +186,9 @@ impl DrawTarget for SsdOutput {
|
|||||||
|
|
||||||
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
|
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
|
||||||
if color.is_on() {
|
if color.is_on() {
|
||||||
self.0.fill(255);
|
self.pixbuf.fill(255);
|
||||||
} else {
|
} else {
|
||||||
self.0.fill(0);
|
self.pixbuf.fill(0);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -202,50 +203,24 @@ impl OriginDimensions for SsdOutput {
|
|||||||
impl<'a> OutputAsync<'a, Matrix2DSpace> for SsdOutput {
|
impl<'a> OutputAsync<'a, Matrix2DSpace> for SsdOutput {
|
||||||
type Error = DisplayError;
|
type Error = DisplayError;
|
||||||
|
|
||||||
type Controls = SsdControls;
|
type Controls = DisplayControls;
|
||||||
|
|
||||||
async fn commit_async(&mut self) -> Result<(), Self::Error> {
|
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();
|
let new_brightness = self.controls.brightness();
|
||||||
self.1.set_display_on(self.2.data.is_on.load(core::sync::atomic::Ordering::Relaxed)).await.unwrap();
|
let new_power = self.controls.is_on();
|
||||||
self.1.draw(&self.0).await
|
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> {
|
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
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
#![no_std]
|
#![no_std]
|
||||||
|
|
||||||
pub mod display;
|
|
||||||
pub mod backoff;
|
pub mod backoff;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
pub mod shaders;
|
|
||||||
pub mod ego;
|
pub mod ego;
|
||||||
pub mod animation;
|
pub mod animation;
|
||||||
pub mod idle;
|
pub mod idle;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub mod simulation;
|
|||||||
#[cfg(feature="demo")]
|
#[cfg(feature="demo")]
|
||||||
pub mod demo;
|
pub mod demo;
|
||||||
#[cfg(feature="oled")]
|
#[cfg(feature="oled")]
|
||||||
pub mod oled;
|
pub mod oled_render;
|
||||||
|
|
||||||
// Prediction engines
|
// Prediction engines
|
||||||
pub mod predict;
|
pub mod predict;
|
||||||
@@ -19,3 +19,4 @@ pub mod motion;
|
|||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod safetyui;
|
pub mod safetyui;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
pub mod oled;
|
||||||
@@ -1,312 +1,113 @@
|
|||||||
use core::{cell::{Cell, RefCell}, cmp::min, f32::consts::PI, fmt::Binary, ops::DerefMut};
|
use alloc::sync::Arc;
|
||||||
|
|
||||||
use alloc::{format, sync::Arc};
|
|
||||||
use display_interface::DisplayError;
|
use display_interface::DisplayError;
|
||||||
use embassy_executor::{raw, Spawner};
|
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, pubsub::DynSubscriber};
|
||||||
use embassy_sync::{blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}, mutex::Mutex, pubsub::DynSubscriber};
|
use embassy_time::{Duration, Instant, Timer};
|
||||||
use embassy_time::{Delay, Duration, Instant, Timer};
|
use embedded_graphics::{pixelcolor::BinaryColor, prelude::DrawTarget};
|
||||||
use embedded_graphics::{image::Image, mono_font::{ascii::{FONT_6X10, FONT_10X20, FONT_9X18_BOLD}, MonoTextStyleBuilder}, pixelcolor::BinaryColor, prelude::*, primitives::{Line, PrimitiveStyleBuilder, Rectangle, StyledDrawable}, text::{Alignment, Baseline, Text}};
|
use figments::{mappings::embedded_graphics::Matrix2DSpace, prelude::{Coordinates, Rectangle}, render::{RenderSource, Shader}, surface::{BufferedSurfacePool, NullSurface, NullBufferPool, Surface, SurfaceBuilder, Surfaces}};
|
||||||
use esp_hal::{i2c::master::I2c, Async, gpio::Output};
|
use figments_render::output::{Brightness, OutputAsync};
|
||||||
use figments::{liber8tion::{interpolate::{ease_in_out_quad, scale8, Fract8}, trig::{cos8, sin8}}, mappings::{embedded_graphics::{EmbeddedGraphicsSampler, Matrix2DSpace}, linear::LinearSpace}, pixels::{PixelBlend, PixelSink}, prelude::{CoordinateSpace, Coordinates, Fract8Ops}, render::{RenderSource, Sample}, surface::{BufferedSurface, BufferedSurfacePool, Surface, SurfaceBuilder, Surfaces}};
|
|
||||||
use figments_render::output::{Brightness, NullControls, OutputAsync};
|
|
||||||
use futures::{join, FutureExt};
|
|
||||||
use nalgebra::{Matrix, Vector2};
|
|
||||||
use ssd1306::{mode::{BufferedGraphicsModeAsync, DisplayConfigAsync}, prelude::{DisplayRotation, I2CInterface}, size::DisplaySize128x64, I2CDisplayInterface, Ssd1306Async};
|
|
||||||
use log::*;
|
use log::*;
|
||||||
use embedded_graphics::mono_font::MonoTextStyle;
|
|
||||||
use embedded_graphics::primitives::PrimitiveStyle;
|
|
||||||
use nalgebra::ComplexField;
|
|
||||||
|
|
||||||
use crate::{animation::{AnimatedSurface, Animation}, backoff::Backoff, ego::engine::MotionState, events::{Notification, Prediction, Scene, SensorSource, Telemetry}, graphics::ssd1306::{SsdControls, SsdOutput, SsdPixel}, tasks::ui};
|
use crate::{animation::Animation, backoff::Backoff, events::{Notification, Prediction, SensorSource, Telemetry}, graphics::{display::DisplayControls, oled_ui::{OledUniforms, Screen}}};
|
||||||
|
|
||||||
mod images {
|
#[cfg(feature="oled")]
|
||||||
use embedded_graphics::{
|
pub type OledUiSurfacePool = BufferedSurfacePool<OledUniforms, Matrix2DSpace, BinaryColor>;
|
||||||
image::ImageRaw,
|
|
||||||
pixelcolor::BinaryColor
|
#[cfg(not(feature="oled"))]
|
||||||
};
|
pub type OledUiSurfacePool = NullBufferPool<NullSurface<OledUniforms, Matrix2DSpace, BinaryColor>>;
|
||||||
include!("../../target/images.rs");
|
|
||||||
|
type OledSurface = <OledUiSurfacePool as Surfaces<Matrix2DSpace>>::Surface;
|
||||||
|
|
||||||
|
pub type LockedUniforms = Arc<Mutex<CriticalSectionRawMutex, OledUniforms>>;
|
||||||
|
|
||||||
|
pub struct OledUI<S: Surface + core::fmt::Debug> {
|
||||||
|
overlay: S,
|
||||||
|
controls: DisplayControls,
|
||||||
|
uniforms: Arc<Mutex<CriticalSectionRawMutex, OledUniforms>>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
struct OverlayShader {}
|
||||||
struct OledUI {
|
impl Shader<OledUniforms, Matrix2DSpace, BinaryColor> for OverlayShader {
|
||||||
scene: Scene,
|
fn draw(&self, _surface_coords: &Coordinates<Matrix2DSpace>, _uniforms: &OledUniforms) -> BinaryColor {
|
||||||
motion: MotionState,
|
BinaryColor::On
|
||||||
brakelight: bool,
|
}
|
||||||
headlight: bool,
|
|
||||||
gps_online: bool,
|
|
||||||
imu_online: bool,
|
|
||||||
velocity: f32,
|
|
||||||
location: Vector2<f64>,
|
|
||||||
sleep: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
impl<S: core::fmt::Debug + Surface<CoordinateSpace = Matrix2DSpace, Pixel = BinaryColor, Uniforms = OledUniforms>> OledUI<S> {
|
||||||
struct OledUniforms {
|
pub fn new<SS: Surfaces<Matrix2DSpace, Surface = S>>(surfaces: &mut SS, controls: DisplayControls, uniforms: LockedUniforms) -> Self where SS::Error: core::fmt::Debug {
|
||||||
ui: OledUI,
|
Self {
|
||||||
frame: usize,
|
overlay: SurfaceBuilder::build(surfaces)
|
||||||
current_screen: Screen,
|
.rect(Rectangle::everything())
|
||||||
next_screen: Screen
|
.shader(OverlayShader{})
|
||||||
}
|
.opacity(0)
|
||||||
|
.finish().unwrap(),
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
controls,
|
||||||
enum Screen {
|
uniforms
|
||||||
#[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 RenderSource<OledUniforms, Matrix2DSpace, BinaryColor, SsdPixel> for Screen {
|
pub async fn screen_transition(&mut self, next_screen: Screen) {
|
||||||
fn render_to<'a, Smp>(&'a self, output: &'a mut Smp, uniforms: &OledUniforms)
|
const FADE_IN: Animation = Animation::new().from(0).to(255).duration(Duration::from_millis(300));
|
||||||
where
|
const FADE_OUT: Animation = Animation::new().from(255).to(0).duration(Duration::from_millis(300));
|
||||||
Smp: Sample<'a, Matrix2DSpace, Output = SsdPixel> {
|
info!("Fading in to screen {next_screen:?}");
|
||||||
let mut sampler = EmbeddedGraphicsSampler(output, embedded_graphics::primitives::Rectangle::new(Point::new(0, 0), Size::new(128, 64)));
|
FADE_IN.apply(&mut self.overlay).await;
|
||||||
self.draw_screen(&mut sampler, uniforms);
|
{
|
||||||
}
|
let mut locked = self.uniforms.lock().await;
|
||||||
}
|
locked.current_screen = next_screen;
|
||||||
|
|
||||||
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 DRAW_STYLE: PrimitiveStyle<BinaryColor> = PrimitiveStyleBuilder::new()
|
|
||||||
.fill_color(BinaryColor::On)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
const INACTIVE_STYLE: PrimitiveStyle<BinaryColor> = PrimitiveStyleBuilder::new()
|
|
||||||
.fill_color(BinaryColor::Off)
|
|
||||||
.stroke_color(BinaryColor::On)
|
|
||||||
.stroke_width(2)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
async fn oled_render(mut output: SsdOutput, surfaces: BufferedSurfacePool<OledUniforms, Matrix2DSpace, BinaryColor>, uniforms: Arc<Mutex<NoopRawMutex, OledUniforms>>) {
|
|
||||||
Backoff::from_secs(1).forever().attempt::<_, (), DisplayError>(async || {
|
|
||||||
const FPS: u64 = 30;
|
|
||||||
const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS);
|
|
||||||
|
|
||||||
const ANIMATION_TPS: u64 = 30;
|
|
||||||
const ANIMATION_FRAME_TIME: Duration = Duration::from_millis(1000 / ANIMATION_TPS);
|
|
||||||
info!("Starting Oled renderer");
|
|
||||||
loop {
|
|
||||||
let start = Instant::now();
|
|
||||||
{
|
|
||||||
let mut locked = uniforms.lock().await;
|
|
||||||
output.clear(BinaryColor::Off).unwrap();
|
|
||||||
locked.frame = (Instant::now().as_millis() / ANIMATION_FRAME_TIME.as_millis()) as usize;
|
|
||||||
locked.current_screen.render_to(&mut output, &locked);
|
|
||||||
surfaces.render_to(&mut output, &locked);
|
|
||||||
}
|
|
||||||
output.commit_async().await?;
|
|
||||||
let frame_time = Instant::now() - start;
|
|
||||||
if frame_time < RENDER_BUDGET {
|
|
||||||
Timer::after(RENDER_BUDGET - frame_time).await;
|
|
||||||
} else {
|
|
||||||
//warn!("OLED Frame took too long to render! {}ms", frame_time.as_millis());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).await.unwrap();
|
FADE_OUT.apply(&mut self.overlay).await;
|
||||||
}
|
|
||||||
|
|
||||||
async fn screen_transition(overlay: &mut BufferedSurface<OledUniforms, Matrix2DSpace, BinaryColor>, uniforms: &mut Arc<Mutex<NoopRawMutex, OledUniforms>>, next_screen: Screen) {
|
|
||||||
const FADE_IN: Animation = Animation::new().from(0).to(255).duration(Duration::from_millis(300));
|
|
||||||
const FADE_OUT: Animation = Animation::new().from(255).to(0).duration(Duration::from_millis(300));
|
|
||||||
info!("Fading in to screen {next_screen:?}");
|
|
||||||
FADE_IN.apply(overlay).await;
|
|
||||||
{
|
|
||||||
let mut locked = uniforms.lock().await;
|
|
||||||
locked.current_screen = next_screen;
|
|
||||||
}
|
}
|
||||||
FADE_OUT.apply(overlay).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
pub async fn on_event(&mut self, event: Telemetry) {
|
||||||
async fn oled_ui(mut events: DynSubscriber<'static, Telemetry>, mut overlay: BufferedSurface<OledUniforms, Matrix2DSpace, BinaryColor>, mut uniforms: Arc<Mutex<NoopRawMutex, OledUniforms>>, mut controls: SsdControls) {
|
match event {
|
||||||
|
// Waking and sleeping
|
||||||
screen_transition(&mut overlay, &mut uniforms, Screen::Bootsplash).await;
|
|
||||||
Timer::after_secs(3).await;
|
|
||||||
screen_transition(&mut overlay, &mut uniforms, Screen::Home).await;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let evt = events.next_message_pure().await;
|
|
||||||
//info!("Oled UI event: {evt:?}");
|
|
||||||
match evt {
|
|
||||||
Telemetry::Notification(Notification::Sleep) => {
|
Telemetry::Notification(Notification::Sleep) => {
|
||||||
screen_transition(&mut overlay, &mut uniforms, Screen::Sleeping).await;
|
warn!("Putting OLED display to sleep");
|
||||||
|
self.screen_transition(Screen::Sleeping).await;
|
||||||
Timer::after_secs(1).await;
|
Timer::after_secs(1).await;
|
||||||
screen_transition(&mut overlay, &mut uniforms, Screen::Blank).await;
|
self.screen_transition(Screen::Blank).await;
|
||||||
controls.set_on(false);
|
self.controls.set_on(false);
|
||||||
//ui_state.sleep = true
|
//ui_state.sleep = true
|
||||||
},
|
},
|
||||||
Telemetry::Notification(Notification::WakeUp) => {
|
Telemetry::Notification(Notification::WakeUp) => {
|
||||||
controls.set_on(true);
|
warn!("Waking up OLED display");
|
||||||
screen_transition(&mut overlay, &mut uniforms, Screen::Waking).await;
|
self.controls.set_on(true);
|
||||||
|
self.screen_transition(Screen::Waking).await;
|
||||||
Timer::after_secs(1).await;
|
Timer::after_secs(1).await;
|
||||||
screen_transition(&mut overlay, &mut uniforms, Screen::Home).await;
|
self.screen_transition(Screen::Home).await;
|
||||||
//ui_state.sleep = false
|
//ui_state.sleep = false
|
||||||
},
|
},
|
||||||
_ => ()
|
|
||||||
}
|
// State updates
|
||||||
let mut locked = uniforms.lock().await;
|
Telemetry::Prediction(Prediction::Velocity(v)) => self.with_uniforms(|state| {state.ui.velocity = v;}).await,
|
||||||
let ui_state = &mut locked.ui;
|
Telemetry::Prediction(Prediction::Location(loc)) => self.with_uniforms(|state| {state.ui.location = loc}).await,
|
||||||
match evt {
|
Telemetry::Prediction(Prediction::Motion(motion)) => self.with_uniforms(|state| {state.ui.motion = motion}).await,
|
||||||
Telemetry::Prediction(Prediction::Velocity(v)) => ui_state.velocity = v,
|
Telemetry::Notification(Notification::SceneChange(scene)) => self.with_uniforms(|state| {state.ui.scene = scene}).await,
|
||||||
Telemetry::Prediction(Prediction::Location(loc)) => ui_state.location = loc,
|
Telemetry::Notification(Notification::SetBrakelight(b)) => self.with_uniforms(|state| {state.ui.brakelight = b}).await,
|
||||||
Telemetry::Prediction(Prediction::Motion(motion)) => ui_state.motion = motion,
|
Telemetry::Notification(Notification::SetHeadlight(b)) => self.with_uniforms(|state| {state.ui.headlight = b}).await,
|
||||||
Telemetry::Notification(Notification::SceneChange(scene)) => ui_state.scene = scene,
|
Telemetry::Notification(Notification::SensorOffline(SensorSource::IMU)) => self.with_uniforms(|state| {state.ui.imu_online = false}).await,
|
||||||
Telemetry::Notification(Notification::SetBrakelight(b)) => ui_state.brakelight = b,
|
Telemetry::Notification(Notification::SensorOnline(SensorSource::IMU)) => self.with_uniforms(|state| {state.ui.imu_online = true}).await,
|
||||||
Telemetry::Notification(Notification::SetHeadlight(b)) => ui_state.headlight = b,
|
Telemetry::Notification(Notification::SensorOffline(SensorSource::GPS)) => self.with_uniforms(|state| {state.ui.gps_online = false}).await,
|
||||||
Telemetry::Notification(Notification::SensorOffline(SensorSource::IMU)) => ui_state.imu_online = false,
|
Telemetry::Notification(Notification::SensorOnline(SensorSource::GPS)) => self.with_uniforms(|state| {state.ui.gps_online = true}).await,
|
||||||
Telemetry::Notification(Notification::SensorOnline(SensorSource::IMU)) => ui_state.imu_online = true,
|
|
||||||
Telemetry::Notification(Notification::SensorOffline(SensorSource::GPS)) => ui_state.gps_online = false,
|
|
||||||
Telemetry::Notification(Notification::SensorOnline(SensorSource::GPS)) => ui_state.gps_online = true,
|
|
||||||
_ => ()
|
_ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*for (coords, pix) in output.sample(&figments::prelude::Rectangle::everything()) {
|
|
||||||
pix.set(&BinaryColor::Off);
|
pub async fn with_uniforms(&self, f: impl Fn(&mut OledUniforms)) {
|
||||||
|
let mut locked = self.uniforms.lock().await;
|
||||||
|
let mutref: &mut OledUniforms = &mut locked;
|
||||||
|
f(mutref);
|
||||||
}
|
}
|
||||||
Text::with_alignment("Foo", Point::new(128 / 2, 64 / 2), LOGO_STYLE, Alignment::Center).draw(&mut output).unwrap();
|
|
||||||
output.commit_async().await.unwrap();
|
|
||||||
Timer::after_secs(1).await;
|
|
||||||
|
|
||||||
let painter = UiPainter { current_screen: Screen::Bootsplash, next_screen: Screen::Bootsplash };
|
|
||||||
painter.render_to(&mut output, &OledUniforms { frame: 0, ui: ui_state });*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn oled_task(i2c: I2c<'static, Async>, mut reset_pin: Output<'static>, events: DynSubscriber<'static, Telemetry>) {
|
pub async fn oled_ui(mut events: DynSubscriber<'static, Telemetry>, mut ui: OledUI<OledSurface>) {
|
||||||
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();
|
|
||||||
|
|
||||||
let output = SsdOutput::new(display);
|
ui.screen_transition(Screen::Bootsplash).await;
|
||||||
let mut surfaces = BufferedSurfacePool::default();
|
Timer::after_secs(3).await;
|
||||||
let uniforms = Default::default();
|
ui.screen_transition(Screen::Home).await;
|
||||||
|
|
||||||
let sfc = SurfaceBuilder::build(&mut surfaces).shader(|coords: &Coordinates<Matrix2DSpace>, uniforms: &OledUniforms| {
|
loop {
|
||||||
BinaryColor::On
|
ui.on_event(events.next_message_pure().await).await;
|
||||||
}).opacity(0).finish().unwrap();
|
}
|
||||||
|
|
||||||
info!("Starting OLED display tasks!");
|
|
||||||
let spawner = Spawner::for_current_executor().await;
|
|
||||||
let ui_controls = output.controls().unwrap().clone();
|
|
||||||
spawner.must_spawn(oled_render(output, surfaces, Arc::clone(&uniforms)));
|
|
||||||
spawner.must_spawn(oled_ui(events, sfc, uniforms, ui_controls));
|
|
||||||
}
|
}
|
||||||
39
src/tasks/oled_render.rs
Normal file
39
src/tasks/oled_render.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use display_interface::DisplayError;
|
||||||
|
use embassy_time::{Duration, Instant, Timer};
|
||||||
|
use embedded_graphics::{pixelcolor::BinaryColor, prelude::DrawTarget};
|
||||||
|
use figments::render::RenderSource;
|
||||||
|
use figments_render::output::OutputAsync;
|
||||||
|
use log::*;
|
||||||
|
|
||||||
|
use crate::{backoff::Backoff, graphics::ssd1306::SsdOutput, tasks::oled::{LockedUniforms, OledUiSurfacePool}};
|
||||||
|
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
pub async fn oled_render(mut output: SsdOutput, surfaces: OledUiSurfacePool, uniforms: LockedUniforms) {
|
||||||
|
warn!("Starting OLED rendering task");
|
||||||
|
Backoff::from_secs(1).forever().attempt::<_, (), DisplayError>(async || {
|
||||||
|
const FPS: u64 = 30;
|
||||||
|
const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS);
|
||||||
|
|
||||||
|
const ANIMATION_TPS: u64 = 30;
|
||||||
|
const ANIMATION_FRAME_TIME: Duration = Duration::from_millis(1000 / ANIMATION_TPS);
|
||||||
|
info!("Starting Oled renderer");
|
||||||
|
loop {
|
||||||
|
let start = Instant::now();
|
||||||
|
{
|
||||||
|
let mut locked = uniforms.lock().await;
|
||||||
|
output.clear(BinaryColor::Off).unwrap();
|
||||||
|
locked.frame = (Instant::now().as_millis() / ANIMATION_FRAME_TIME.as_millis()) as usize;
|
||||||
|
locked.current_screen.render_to(&mut output, &locked);
|
||||||
|
surfaces.render_to(&mut output, &locked);
|
||||||
|
}
|
||||||
|
output.commit_async().await?;
|
||||||
|
let frame_time = Instant::now() - start;
|
||||||
|
if frame_time < RENDER_BUDGET {
|
||||||
|
Timer::after(RENDER_BUDGET - frame_time).await;
|
||||||
|
} else {
|
||||||
|
//warn!("OLED Frame took too long to render! {}ms", frame_time.as_millis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).await.unwrap();
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
use embassy_time::{Duration, Instant, Timer};
|
use embassy_time::{Duration, Instant, Timer};
|
||||||
use esp_hal::{gpio::AnyPin, rmt::Rmt, time::Rate, timer::timg::Wdt};
|
use esp_hal::{gpio::AnyPin, rmt::Rmt, time::Rate, timer::timg::Wdt};
|
||||||
use esp_hal_smartled::{buffer_size_async, SmartLedsAdapterAsync};
|
use esp_hal_smartled::{buffer_size_async, SmartLedsAdapterAsync};
|
||||||
use figments::{prelude::*, surface::Surfaces};
|
use figments::prelude::*;
|
||||||
use figments_render::gamma::GammaCurve;
|
use figments_render::gamma::GammaCurve;
|
||||||
use figments_render::output::{GammaCorrected, OutputAsync};
|
use figments_render::output::{GammaCorrected, OutputAsync};
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use micromath::F32Ext;
|
|
||||||
|
|
||||||
use crate::display::NUM_PIXELS;
|
use crate::graphics::display::NUM_PIXELS;
|
||||||
use crate::{display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool};
|
use crate::{graphics::display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool};
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'static>, surfaces: UiSurfacePool, safety_surfaces: UiSurfacePool, mut controls: DisplayControls, mut wdt: Wdt<esp_hal::peripherals::TIMG0<'static>>) {
|
pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'static>, surfaces: UiSurfacePool, safety_surfaces: UiSurfacePool, mut controls: DisplayControls, mut wdt: Wdt<esp_hal::peripherals::TIMG0<'static>>) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use core::fmt::Debug;
|
|||||||
use futures::join;
|
use futures::join;
|
||||||
use log::*;
|
use log::*;
|
||||||
|
|
||||||
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, display::{DisplayControls, SegmentSpace, Uniforms}, events::Notification, shaders::*, tasks::ui::UiSurfacePool};
|
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, graphics::display::{DisplayControls, SegmentSpace, Uniforms}, events::Notification, graphics::shaders::*, tasks::ui::UiSurfacePool};
|
||||||
|
|
||||||
pub struct SafetyUi<S: Surface> {
|
pub struct SafetyUi<S: Surface> {
|
||||||
// Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer
|
// Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use core::fmt::Debug;
|
|||||||
use futures::join;
|
use futures::join;
|
||||||
use log::*;
|
use log::*;
|
||||||
|
|
||||||
use crate::{animation::{AnimatedSurface, Animation}, display::{SegmentSpace, Uniforms}, events::{Notification, Scene, SensorSource, Telemetry}, shaders::*};
|
use crate::{animation::{AnimatedSurface, Animation}, graphics::display::{SegmentSpace, Uniforms}, events::{Notification, Scene, SensorSource, Telemetry}, graphics::shaders::*};
|
||||||
|
|
||||||
pub struct Ui<S: Surface> {
|
pub struct Ui<S: Surface> {
|
||||||
// Background layer provides an always-running background for everything to draw on
|
// Background layer provides an always-running background for everything to draw on
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ pub async fn ble_task(_notify: DynSubscriber<'static, Notification>, wifi_init:
|
|||||||
info!("Read serial data! {data:?}");
|
info!("Read serial data! {data:?}");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// https://nextcloud.malloc.hackerbots.net/nextcloud/index.php/apps/phonetrack/logGet/cc668656ef51680c99b0eb6e5323a459/renderbug?lat=LAT&lon=LON&alt=ALT&acc=ACC&bat=BAT&sat=SAT&speed=SPD&bearing=DIR×tamp=TIME
|
||||||
|
|
||||||
// Other useful characteristics:
|
// Other useful characteristics:
|
||||||
// 0x2A67 - Location and speed
|
// 0x2A67 - Location and speed
|
||||||
// 0x2A00 - Device name
|
// 0x2A00 - Device name
|
||||||
|
|||||||
Reference in New Issue
Block a user