#![no_std]
#![forbid(unsafe_code)]
use embassy_st7789v::{Color, NoPin, St7789v, SCREEN_H, SCREEN_W};
use embedded_hal::digital::OutputPin;
use embedded_hal_async::spi::SpiDevice;
pub const PLOT_HISTORY_LIMIT: usize = 240;
pub struct Graphics<'a, SPI, DC, RST = NoPin>
where
SPI: SpiDevice,
DC: OutputPin,
RST: OutputPin,
{
pub display: &'a mut St7789v<SPI, DC, RST>,
}
impl<'a, SPI, DC> Graphics<'a, SPI, DC, NoPin>
where
SPI: SpiDevice,
DC: OutputPin,
{
#[inline]
pub fn new_no_rst(display: &'a mut St7789v<SPI, DC, NoPin>) -> Self {
Self { display }
}
}
impl<'a, SPI, DC, RST> Graphics<'a, SPI, DC, RST>
where
SPI: SpiDevice,
DC: OutputPin,
RST: OutputPin,
{
#[inline]
pub fn new(display: &'a mut St7789v<SPI, DC, RST>) -> Self {
Self { display }
}
#[inline(always)]
pub async fn pixel(&mut self, x: i32, y: i32, color: Color) {
if x >= 0 && y >= 0 && x < SCREEN_W as i32 && y < SCREEN_H as i32 {
let _ = self.display.draw_pixel(x as u16, y as u16, color).await;
}
}
}
pub async fn line<SPI, DC, RST>(
gfx: &mut Graphics<'_, SPI, DC, RST>,
mut x0: i32,
mut y0: i32,
x1: i32,
y1: i32,
color: Color,
) where
SPI: SpiDevice,
DC: OutputPin,
RST: OutputPin,
{
let dx = (x1 - x0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let dy = -(y1 - y0).abs();
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
loop {
gfx.pixel(x0, y0, color).await;
if x0 == x1 && y0 == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x0 += sx;
}
if e2 <= dx {
err += dx;
y0 += sy;
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct AxisConfig {
pub start: f32,
pub end: f32,
pub step: f32,
pub label: &'static [u8],
}
impl AxisConfig {
pub const fn new(start: f32, end: f32, step: f32, label: &'static [u8]) -> Self {
Self { start, end, step, label }
}
pub fn is_valid(&self) -> bool {
self.step > 0.0 && self.end > self.start
}
pub fn tick_count(&self) -> usize {
if !self.is_valid() {
return 0;
}
let count = ((self.end - self.start) / self.step) as usize;
count + 1
}
}
#[derive(Clone, Copy, Debug)]
pub struct PlotConfig {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
pub margin_left: i32,
pub margin_right: i32,
pub margin_top: i32,
pub margin_bottom: i32,
pub x_axis: AxisConfig,
pub y_axis: AxisConfig,
pub bg_color: Color,
pub line_color: Color,
pub axis_color: Color,
pub grid_color: Color,
pub text_color: Color,
pub label_color: Color,
}
pub struct LineChart<const N: usize> {
config: PlotConfig,
data: [f32; N],
head: usize,
count: usize,
plot_x: i32,
plot_y: i32,
plot_w: i32,
plot_h: i32,
}
impl<const N: usize> LineChart<N> {
pub fn new(config: PlotConfig) -> Self {
assert!(N <= PLOT_HISTORY_LIMIT, "L'historique dépasse la limite physique horizontale.");
assert!(config.x_axis.is_valid(), "Configuration axe X invalide.");
assert!(config.y_axis.is_valid(), "Configuration axe Y invalide.");
let plot_x = config.x + config.margin_left;
let plot_y = config.y + config.margin_top;
let plot_w = config.width - config.margin_left - config.margin_right;
let plot_h = config.height - config.margin_top - config.margin_bottom;
Self {
config,
data: [0.0; N],
head: 0,
count: 0,
plot_x,
plot_y,
plot_w,
plot_h,
}
}
#[inline]
pub fn config(&self) -> &PlotConfig {
&self.config
}
pub fn push(&mut self, value: f32) {
self.data[self.head] = value;
self.head = (self.head + 1) % N;
if self.count < N {
self.count += 1;
}
}
pub fn clear(&mut self) {
self.head = 0;
self.count = 0;
self.data.fill(0.0);
}
#[inline]
fn get_sample(&self, index: usize) -> f32 {
let oldest = if self.count < N { 0 } else { self.head };
self.data[(oldest + index) % N]
}
#[inline]
fn scale_y(&self, value: f32) -> i32 {
let y_min = self.config.y_axis.start;
let y_max = self.config.y_axis.end;
if y_max <= y_min {
return self.plot_y + self.plot_h - 1;
}
let clamped_val = value.max(y_min).min(y_max);
let ratio = (clamped_val - y_min) / (y_max - y_min);
self.plot_y + self.plot_h - 1 - (ratio * (self.plot_h - 1) as f32) as i32
}
#[inline]
fn scale_x(&self, index: usize) -> i32 {
if N <= 1 {
return self.plot_x;
}
self.plot_x + (index as i32 * (self.plot_w - 1)) / (N as i32 - 1)
}
#[inline]
fn scale_x_value(&self, value: f32) -> i32 {
let x_min = self.config.x_axis.start;
let x_max = self.config.x_axis.end;
if x_max <= x_min {
return self.plot_x;
}
let clamped = value.max(x_min).min(x_max);
let ratio = (clamped - x_min) / (x_max - x_min);
self.plot_x + (ratio * (self.plot_w - 1) as f32) as i32
}
pub async fn render<SPI, DC, RST>(&self, gfx: &mut Graphics<'_, SPI, DC, RST>)
where
SPI: SpiDevice,
DC: OutputPin,
RST: OutputPin,
{
let _ = gfx.display.fill_rect(
self.plot_x as u16,
self.plot_y as u16,
(self.plot_x + self.plot_w - 1) as u16,
(self.plot_y + self.plot_h - 1) as u16,
self.config.bg_color,
).await;
let right_edge = self.plot_x + self.plot_w - 1;
let bottom_edge = self.plot_y + self.plot_h - 1;
let y_axis = &self.config.y_axis;
let y_range = y_axis.end - y_axis.start;
let tick_count_y = y_axis.tick_count();
for i in 0..tick_count_y {
let value = y_axis.start + (i as f32 * y_axis.step);
let ratio = (value - y_axis.start) / y_range;
let y_grid = bottom_edge - (ratio * (self.plot_h - 1) as f32) as i32;
if i > 0 && i < tick_count_y - 1 {
let _ = gfx.display.draw_hline(
self.plot_x as u16,
y_grid as u16,
self.plot_w as u16,
self.config.grid_color
).await;
}
let label_color = if value.abs() < 0.01 * y_axis.step {
Color::GREEN } else {
self.config.text_color
};
let label_y = (y_grid - 4).max(self.config.y + 8).min(self.config.y + self.config.height - 8);
let _ = gfx.display.draw_f32(
(self.config.x + 2) as u16,
label_y as u16,
value,
1,
label_color,
self.config.bg_color,
).await;
}
let x_axis = &self.config.x_axis;
let tick_count_x = x_axis.tick_count();
for i in 0..tick_count_x {
let value = x_axis.start + (i as f32 * x_axis.step);
let x_grid = self.scale_x_value(value);
if i > 0 && i < tick_count_x - 1 {
let _ = gfx.display.draw_vline(
x_grid as u16,
self.plot_y as u16,
self.plot_h as u16,
self.config.grid_color
).await;
}
let label_x = (x_grid - 8).max(self.config.x + 2).min(self.config.x + self.config.width - 20);
let _ = gfx.display.draw_f32(
label_x as u16,
(bottom_edge + 4) as u16,
value,
1,
self.config.text_color,
self.config.bg_color,
).await;
}
let _ = gfx.display.draw_str(
(self.config.x + 2) as u16,
(self.config.y - 16).max(0) as u16,
y_axis.label,
self.config.label_color,
self.config.bg_color,
).await;
let label_x_x = self.plot_x + (self.plot_w / 2) - ((y_axis.label.len() as i32 * 6) / 2);
let _ = gfx.display.draw_str(
label_x_x.max(self.config.x + 2) as u16,
(self.config.y + self.config.height + 4) as u16,
x_axis.label,
self.config.label_color,
self.config.bg_color,
).await;
let _ = gfx.display.draw_hline(self.plot_x as u16, self.plot_y as u16, self.plot_w as u16, self.config.axis_color).await;
let _ = gfx.display.draw_hline(self.plot_x as u16, bottom_edge as u16, self.plot_w as u16, self.config.axis_color).await;
let _ = gfx.display.draw_vline(self.plot_x as u16, self.plot_y as u16, self.plot_h as u16, self.config.axis_color).await;
let _ = gfx.display.draw_vline(right_edge as u16, self.plot_y as u16, self.plot_h as u16, self.config.axis_color).await;
if self.count < 2 {
if self.count == 1 {
let px = self.scale_x(0);
let py = self.scale_y(self.get_sample(0));
gfx.pixel(px, py, self.config.line_color).await;
}
return;
}
let mut prev_x = self.scale_x(0);
let mut prev_y = self.scale_y(self.get_sample(0));
for i in 1..self.count {
let next_x = self.scale_x(i);
let next_y = self.scale_y(self.get_sample(i));
line(gfx, prev_x, prev_y, next_x, next_y, self.config.line_color).await;
prev_x = next_x;
prev_y = next_y;
}
}
}