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

View File

@@ -9,7 +9,7 @@ pub mod simulation;
#[cfg(feature="demo")]
pub mod demo;
#[cfg(feature="oled")]
pub mod oled;
pub mod oled_render;
// Prediction engines
pub mod predict;
@@ -18,4 +18,5 @@ pub mod motion;
// Graphics stack
pub mod ui;
pub mod safetyui;
pub mod render;
pub mod render;
pub mod oled;

View File

@@ -1,312 +1,113 @@
use core::{cell::{Cell, RefCell}, cmp::min, f32::consts::PI, fmt::Binary, ops::DerefMut};
use alloc::{format, sync::Arc};
use alloc::sync::Arc;
use display_interface::DisplayError;
use embassy_executor::{raw, Spawner};
use embassy_sync::{blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}, mutex::Mutex, pubsub::DynSubscriber};
use embassy_time::{Delay, Duration, Instant, Timer};
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 esp_hal::{i2c::master::I2c, Async, gpio::Output};
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 embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, pubsub::DynSubscriber};
use embassy_time::{Duration, Instant, Timer};
use embedded_graphics::{pixelcolor::BinaryColor, prelude::DrawTarget};
use figments::{mappings::embedded_graphics::Matrix2DSpace, prelude::{Coordinates, Rectangle}, render::{RenderSource, Shader}, surface::{BufferedSurfacePool, NullSurface, NullBufferPool, Surface, SurfaceBuilder, Surfaces}};
use figments_render::output::{Brightness, OutputAsync};
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 {
use embedded_graphics::{
image::ImageRaw,
pixelcolor::BinaryColor
};
include!("../../target/images.rs");
#[cfg(feature="oled")]
pub type OledUiSurfacePool = BufferedSurfacePool<OledUniforms, Matrix2DSpace, BinaryColor>;
#[cfg(not(feature="oled"))]
pub type OledUiSurfacePool = NullBufferPool<NullSurface<OledUniforms, Matrix2DSpace, BinaryColor>>;
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 OledUI {
scene: Scene,
motion: MotionState,
brakelight: bool,
headlight: bool,
gps_online: bool,
imu_online: bool,
velocity: f32,
location: Vector2<f64>,
sleep: bool,
struct OverlayShader {}
impl Shader<OledUniforms, Matrix2DSpace, BinaryColor> for OverlayShader {
fn draw(&self, _surface_coords: &Coordinates<Matrix2DSpace>, _uniforms: &OledUniforms) -> BinaryColor {
BinaryColor::On
}
}
#[derive(Default, Debug)]
struct OledUniforms {
ui: OledUI,
frame: usize,
current_screen: Screen,
next_screen: Screen
}
#[derive(Debug, Default, Clone, Copy)]
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<S: core::fmt::Debug + Surface<CoordinateSpace = Matrix2DSpace, Pixel = BinaryColor, Uniforms = OledUniforms>> OledUI<S> {
pub fn new<SS: Surfaces<Matrix2DSpace, Surface = S>>(surfaces: &mut SS, controls: DisplayControls, uniforms: LockedUniforms) -> Self where SS::Error: core::fmt::Debug {
Self {
overlay: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything())
.shader(OverlayShader{})
.opacity(0)
.finish().unwrap(),
controls,
uniforms
}
}
}
impl RenderSource<OledUniforms, Matrix2DSpace, BinaryColor, SsdPixel> for Screen {
fn render_to<'a, Smp>(&'a self, output: &'a mut Smp, uniforms: &OledUniforms)
where
Smp: Sample<'a, Matrix2DSpace, Output = SsdPixel> {
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 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());
}
pub async fn screen_transition(&mut self, 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(&mut self.overlay).await;
{
let mut locked = self.uniforms.lock().await;
locked.current_screen = next_screen;
}
}).await.unwrap();
}
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(&mut self.overlay).await;
}
FADE_OUT.apply(overlay).await;
}
#[embassy_executor::task]
async fn oled_ui(mut events: DynSubscriber<'static, Telemetry>, mut overlay: BufferedSurface<OledUniforms, Matrix2DSpace, BinaryColor>, mut uniforms: Arc<Mutex<NoopRawMutex, OledUniforms>>, mut controls: SsdControls) {
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 {
pub async fn on_event(&mut self, event: Telemetry) {
match event {
// Waking and sleeping
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;
screen_transition(&mut overlay, &mut uniforms, Screen::Blank).await;
controls.set_on(false);
self.screen_transition(Screen::Blank).await;
self.controls.set_on(false);
//ui_state.sleep = true
},
Telemetry::Notification(Notification::WakeUp) => {
controls.set_on(true);
screen_transition(&mut overlay, &mut uniforms, Screen::Waking).await;
warn!("Waking up OLED display");
self.controls.set_on(true);
self.screen_transition(Screen::Waking).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
},
_ => ()
}
let mut locked = uniforms.lock().await;
let ui_state = &mut locked.ui;
match evt {
Telemetry::Prediction(Prediction::Velocity(v)) => ui_state.velocity = v,
Telemetry::Prediction(Prediction::Location(loc)) => ui_state.location = loc,
Telemetry::Prediction(Prediction::Motion(motion)) => ui_state.motion = motion,
Telemetry::Notification(Notification::SceneChange(scene)) => ui_state.scene = scene,
Telemetry::Notification(Notification::SetBrakelight(b)) => ui_state.brakelight = b,
Telemetry::Notification(Notification::SetHeadlight(b)) => ui_state.headlight = b,
Telemetry::Notification(Notification::SensorOffline(SensorSource::IMU)) => ui_state.imu_online = false,
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,
// State updates
Telemetry::Prediction(Prediction::Velocity(v)) => self.with_uniforms(|state| {state.ui.velocity = v;}).await,
Telemetry::Prediction(Prediction::Location(loc)) => self.with_uniforms(|state| {state.ui.location = loc}).await,
Telemetry::Prediction(Prediction::Motion(motion)) => self.with_uniforms(|state| {state.ui.motion = motion}).await,
Telemetry::Notification(Notification::SceneChange(scene)) => self.with_uniforms(|state| {state.ui.scene = scene}).await,
Telemetry::Notification(Notification::SetBrakelight(b)) => self.with_uniforms(|state| {state.ui.brakelight = b}).await,
Telemetry::Notification(Notification::SetHeadlight(b)) => self.with_uniforms(|state| {state.ui.headlight = b}).await,
Telemetry::Notification(Notification::SensorOffline(SensorSource::IMU)) => self.with_uniforms(|state| {state.ui.imu_online = false}).await,
Telemetry::Notification(Notification::SensorOnline(SensorSource::IMU)) => self.with_uniforms(|state| {state.ui.imu_online = true}).await,
Telemetry::Notification(Notification::SensorOffline(SensorSource::GPS)) => self.with_uniforms(|state| {state.ui.gps_online = false}).await,
Telemetry::Notification(Notification::SensorOnline(SensorSource::GPS)) => self.with_uniforms(|state| {state.ui.gps_online = true}).await,
_ => ()
}
}
/*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]
pub async fn oled_task(i2c: I2c<'static, Async>, mut reset_pin: Output<'static>, events: DynSubscriber<'static, Telemetry>) {
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();
pub async fn oled_ui(mut events: DynSubscriber<'static, Telemetry>, mut ui: OledUI<OledSurface>) {
ui.screen_transition(Screen::Bootsplash).await;
Timer::after_secs(3).await;
ui.screen_transition(Screen::Home).await;
let output = SsdOutput::new(display);
let mut surfaces = BufferedSurfacePool::default();
let uniforms = Default::default();
loop {
ui.on_event(events.next_message_pure().await).await;
}
let sfc = SurfaceBuilder::build(&mut surfaces).shader(|coords: &Coordinates<Matrix2DSpace>, uniforms: &OledUniforms| {
BinaryColor::On
}).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
View 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();
}

View File

@@ -1,14 +1,13 @@
use embassy_time::{Duration, Instant, Timer};
use esp_hal::{gpio::AnyPin, rmt::Rmt, time::Rate, timer::timg::Wdt};
use esp_hal_smartled::{buffer_size_async, SmartLedsAdapterAsync};
use figments::{prelude::*, surface::Surfaces};
use figments::prelude::*;
use figments_render::gamma::GammaCurve;
use figments_render::output::{GammaCorrected, OutputAsync};
use log::{info, warn};
use micromath::F32Ext;
use crate::display::NUM_PIXELS;
use crate::{display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool};
use crate::graphics::display::NUM_PIXELS;
use crate::{graphics::display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool};
#[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>>) {

View File

@@ -7,7 +7,7 @@ use core::fmt::Debug;
use futures::join;
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> {
// Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer

View File

@@ -6,7 +6,7 @@ use core::fmt::Debug;
use futures::join;
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> {
// Background layer provides an always-running background for everything to draw on

View File

@@ -29,6 +29,8 @@ pub async fn ble_task(_notify: DynSubscriber<'static, Notification>, wifi_init:
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&timestamp=TIME
// Other useful characteristics:
// 0x2A67 - Location and speed
// 0x2A00 - Device name