embassy_st7789v_plot/lib.rs
1#![no_std]
2#![forbid(unsafe_code)]
3//! # embassy-st7789v-plot
4//!
5//! Moteur de tracé de graphiques cartésiens (X, Y) adaptatifs et configurables
6//! pour écrans TFT LCD ST7789V, s'appuyant sur [`embassy-st7789v`](https://docs.rs/embassy-st7789v).
7//!
8//! ## Caractéristiques principales
9//!
10//! - **`#![no_std]` + `#![forbid(unsafe_code)]`** : Sûr et optimisé pour le bare-metal.
11//! - **Zéro allocation dynamique** : Buffers statiques uniquement (ring buffer).
12//! - **Rendu intelligent (Double Phase)** : Cadre, étiquettes et titres tracés une seule fois.
13//! Seuls la grille interne et le signal sont rafraîchis dynamiquement.
14//! - **Axes configurables** : Graduations statiques avec labels personnalisés et détection
15//! automatique / coloration du zéro.
16//! - **Historique circulaire** : Jusqu'à 240 points (limite physique de l'écran).
17//! - **Protection stricte des bordures** : Clamping des primitives à l'espace interne utile.
18//! - **Deux modes de rendu** :
19//!
20//! | Mode | Type | Clignotement |
21//! |------|------|:------------:|
22//! | [`LineChart`] + [`render()`](LineChart::render) | async, direct SPI | possible |
23//! | [`LineChartBuffered`] + [`render_buf()`](LineChartBuffered::render_buf) + `flush()` | sync, RAM | **aucun** |
24//!
25//! ## Structures principales
26//!
27//! - [`Graphics`] : Contexte graphique async (direct SPI).
28//! - [`AxisConfig`] : Configuration d'un axe (min, max, pas, label).
29//! - [`PlotConfig`] : Configuration complète (position, marges, couleurs, axes).
30//! - [`LineChart`] : Graphique async avec rendu direct SPI.
31//! - [`LineChartBuffered`] : Graphique sync avec rendu framebuffer — zéro clignotement.
32//!
33//! ## Exemple — mode direct async
34//!
35//! ```no_run
36//! # use embassy_st7789v::{Color, St7789v, NoPin};
37//! # use embedded_hal::digital::OutputPin;
38//! # use embedded_hal_async::spi::SpiDevice;
39//! use embassy_st7789v_plot::{Graphics, AxisConfig, PlotConfig, LineChart};
40//!
41//! # async fn example<SPI, DC>(display: &mut St7789v<SPI, DC, NoPin>)
42//! # where SPI: SpiDevice, DC: OutputPin,
43//! # {
44//! let x_axis = AxisConfig::new(0.0, 10.0, 2.0, b"Time (s)");
45//! let y_axis = AxisConfig::new(-50.0, 50.0, 20.0, b"Volt (mV)");
46//!
47//! let config = PlotConfig {
48//! x: 10, y: 10, width: 220, height: 200,
49//! margin_left: 30, margin_right: 10,
50//! margin_top: 10, margin_bottom: 30,
51//! x_axis, y_axis,
52//! bg_color: Color::BLACK,
53//! line_color: Color::GREEN,
54//! axis_color: Color::WHITE,
55//! grid_color: Color::GRAY,
56//! text_color: Color::WHITE,
57//! label_color: Color::CYAN,
58//! };
59//!
60//! let mut chart: LineChart<100> = LineChart::new(config);
61//! chart.push(12.4);
62//! chart.push(-5.2);
63//! chart.push(22.1);
64//!
65//! let mut gfx = Graphics::new_no_rst(display);
66//! chart.render(&mut gfx).await;
67//! # }
68//! ```
69//!
70//! ## Exemple : mode framebuffer sync (zéro clignotement)
71//!
72//! ```no_run
73//! # use embassy_st7789v::{Color, St7789v, St7789vBuffered, NoPin};
74//! # use embedded_hal::digital::OutputPin;
75//! # use embedded_hal_async::spi::SpiDevice;
76//! use embassy_st7789v_plot::{AxisConfig, PlotConfig, LineChartBuffered};
77//!
78//! # async fn example<SPI, DC>(spi: SPI, dc: DC, rst: NoPin)
79//! # where SPI: SpiDevice, DC: OutputPin,
80//! # {
81//! let config = PlotConfig {
82//! x: 10, y: 10, width: 220, height: 200,
83//! margin_left: 30, margin_right: 10,
84//! margin_top: 10, margin_bottom: 30,
85//! x_axis: AxisConfig::new(0.0, 10.0, 2.0, b"Time (s)"),
86//! y_axis: AxisConfig::new(-50.0, 50.0, 20.0, b"Volt (mV)"),
87//! bg_color: Color::BLACK,
88//! line_color: Color::GREEN,
89//! axis_color: Color::WHITE,
90//! grid_color: Color::GRAY,
91//! text_color: Color::WHITE,
92//! label_color: Color::CYAN,
93//! };
94//!
95//! let mut ecran = St7789vBuffered::new(St7789v::new(spi, dc, rst));
96//! ecran.driver().init().await.unwrap();
97//!
98//! let mut chart: LineChartBuffered<100> = LineChartBuffered::new(config);
99//!
100//! loop {
101//! chart.push(12.4);
102//! chart.render_buf(&mut ecran); // tout en RAM, synchrone
103//! ecran.flush().await.unwrap(); // envoi unique → zéro clignotement
104//! }
105//! # }
106//! ```
107//!
108//! ## Patron de borrow : mode direct
109//!
110//! `Graphics` tient un `&mut St7789v` pour toute sa durée de vie.
111//! Pour appeler les méthodes du driver (texte, orientation…),
112//! `gfx` doit être sorti de portée au préalable.
113//!
114//! ```rust,no_run
115//! {
116//! let mut gfx = Graphics::new_no_rst(&mut display);
117//! chart.render(&mut gfx).await;
118//! } // ← borrow libéré
119//! display.draw_str(8, 8, b"OK", Color::WHITE, Color::BLACK).await.unwrap();
120//! ```
121//!
122//! ## Patron de borrow : mode framebuffer
123//!
124//! `render_buf` prend `&mut St7789vBuffered` directement.
125//! Appeler `flush()` après le rendu.
126//!
127//! ```rust,no_run
128//! chart.render_buf(&mut ecran); // sync
129//! ecran.flush().await.unwrap(); // async, envoi SPI unique
130//! ```
131//!
132//! ## Note sur les erreurs SPI
133//!
134//! Les fonctions de ce crate ignorent silencieusement les erreurs SPI.
135//! Si votre application requiert une gestion d'erreur fine, utilisez
136//! directement `ecran.draw_pixel()`.
137//! Les primitives `render_buf` n'accèdent jamais au SPI — aucune erreur possible.
138use embedded_hal::digital::OutputPin;
139use embedded_hal_async::spi::SpiDevice;
140use embassy_st7789v::{Color, NoPin, St7789v, St7789vBuffered, SCREEN_H, SCREEN_W};
141use embassy_st7789v_graphics::{GraphicsBuffered, line_buf};
142
143
144/// Taille maximale de l'historique des données du graphique.
145pub const PLOT_HISTORY_LIMIT: usize = 240;
146
147// ─────────────────────────────────────────────────────────────────────────────
148// Contexte graphique embarqué (repris de tes primitives)
149// ─────────────────────────────────────────────────────────────────────────────
150
151pub struct Graphics<'a, SPI, DC, RST = NoPin>
152where
153 SPI: SpiDevice,
154 DC: OutputPin,
155 RST: OutputPin,
156{
157 /// Référence vers l'affichage ST7789V
158 pub display: &'a mut St7789v<SPI, DC, RST>,
159}
160
161impl<'a, SPI, DC> Graphics<'a, SPI, DC, NoPin>
162where
163 SPI: SpiDevice,
164 DC: OutputPin,
165{
166 /// Crée un nouveau contexte graphique sans broche RST.
167 ///
168 /// # Arguments
169 ///
170 /// * `display` - Référence mutable vers l'affichage ST7789V
171 #[inline]
172 pub fn new_no_rst(display: &'a mut St7789v<SPI, DC, NoPin>) -> Self {
173 Self { display }
174 }
175}
176
177impl<'a, SPI, DC, RST> Graphics<'a, SPI, DC, RST>
178where
179 SPI: SpiDevice,
180 DC: OutputPin,
181 RST: OutputPin,
182{
183 /// Crée un nouveau contexte graphique avec broche RST.
184 ///
185 /// # Arguments
186 ///
187 /// * `display` - Référence mutable vers l'affichage ST7789V
188 #[inline]
189 pub fn new(display: &'a mut St7789v<SPI, DC, RST>) -> Self {
190 Self { display }
191 }
192
193 /// Trace un pixel à la position (x, y) avec la couleur donnée.
194 ///
195 /// Les coordonnées négatives ou en dehors de l'écran sont ignorées silencieusement.
196 ///
197 /// # Arguments
198 ///
199 /// * `x` - Coordonnée X (peut être négative ou hors écran)
200 /// * `y` - Coordonnée Y (peut être négative ou hors écran)
201 /// * `color` - Couleur du pixel
202
203 #[inline(always)]
204 pub async fn pixel(&mut self, x: i32, y: i32, color: Color) {
205 if x >= 0 && y >= 0 && x < SCREEN_W as i32 && y < SCREEN_H as i32 {
206 let _ = self.display.draw_pixel(x as u16, y as u16, color).await;
207 }
208 }
209}
210
211/// Trace une ligne entre deux points en utilisant l'algorithme de Bresenham.
212///
213/// Cette fonction utilise l'algorithme de Bresenham pour tracer une ligne
214/// entre les points (x0, y0) et (x1, y1). Elle gère correctement les lignes
215/// en dehors de l'écran via la vérification de limites dans [`Graphics::pixel`].
216///
217/// # Arguments
218///
219/// * `gfx` - Contexte graphique
220/// * `x0` - Coordonnée X du point de départ
221/// * `y0` - Coordonnée Y du point de départ
222/// * `x1` - Coordonnée X du point d'arrivée
223/// * `y1` - Coordonnée Y du point d'arrivée
224/// * `color` - Couleur de la ligne
225pub async fn line<SPI, DC, RST>(
226 gfx: &mut Graphics<'_, SPI, DC, RST>,
227 mut x0: i32,
228 mut y0: i32,
229 x1: i32,
230 y1: i32,
231 color: Color,
232) where
233 SPI: SpiDevice,
234 DC: OutputPin,
235 RST: OutputPin,
236{
237 let dx = (x1 - x0).abs();
238 let sx = if x0 < x1 { 1 } else { -1 };
239 let dy = -(y1 - y0).abs();
240 let sy = if y0 < y1 { 1 } else { -1 };
241 let mut err = dx + dy;
242
243 loop {
244 gfx.pixel(x0, y0, color).await;
245 if x0 == x1 && y0 == y1 {
246 break;
247 }
248 let e2 = 2 * err;
249 if e2 >= dy {
250 err += dy;
251 x0 += sx;
252 }
253 if e2 <= dx {
254 err += dx;
255 y0 += sy;
256 }
257 }
258}
259
260// ─────────────────────────────────────────────────────────────────────────────
261// Configuration des Axes
262// ─────────────────────────────────────────────────────────────────────────────
263
264/// Définit un axe avec graduation statique fixe.
265///
266/// Cette structure configure un axe du graphique avec une plage de valeurs,
267/// un pas de graduation régulier et un label descriptif.
268///
269/// # Champs
270///
271/// * `start` - Valeur minimale de l'axe (ex: 0.0)
272/// * `end` - Valeur maximale de l'axe (ex: 10.0)
273/// * `step` - Espacement régulier entre graduations (ex: 1.0)
274/// * `label` - Label texte affiché le long de l'axe (ex: b"Temp (C)")
275#[derive(Clone, Copy, Debug)]
276pub struct AxisConfig {
277 pub start: f32,
278 pub end: f32,
279 pub step: f32,
280 pub label: &'static [u8],
281}
282
283impl AxisConfig {
284 /// Crée une nouvelle configuration d'axe.
285 ///
286 /// # Arguments
287 ///
288 /// * `start` - Valeur minimale (doit être < `end`)
289 /// * `end` - Valeur maximale (doit être > `start`)
290 /// * `step` - Pas de graduation (doit être > 0)
291 /// * `label` - Label statique affiché (par exemple b"Temp (C)")
292 ///
293 /// # Panics
294 ///
295 /// Ne paniquera pas ici, mais utilisez [`is_valid`](Self::is_valid) après construction
296 /// pour vérifier la cohérence.
297 pub const fn new(start: f32, end: f32, step: f32, label: &'static [u8]) -> Self {
298 Self { start, end, step, label }
299 }
300
301 /// Vérifie la cohérence de la configuration.
302 ///
303 /// Retourne `true` si :
304 /// - `step` > 0.0
305 /// - `end` > `start`
306 ///
307
308 pub fn is_valid(&self) -> bool {
309 self.step > 0.0 && self.end > self.start
310 }
311
312 /// Calcule le nombre de graduations (incluant start et end).
313 ///
314 /// En `no_std`, le cast direct remplace `f32::floor()`.
315 ///
316 /// # Retour
317 ///
318 /// Nombre de ticks incluant les extrémités, ou 0 si la configuration est invalide.
319
320 pub fn tick_count(&self) -> usize {
321 if !self.is_valid() {
322 return 0;
323 }
324 let count = ((self.end - self.start) / self.step) as usize;
325 count + 1
326 }
327}
328
329// ─────────────────────────────────────────────────────────────────────────────
330// Configuration et Structure de Traçage
331// ─────────────────────────────────────────────────────────────────────────────
332
333/// Configuration complète du tracé graphique.
334///
335/// Cette structure définit tous les paramètres visuels et géométriques du graphique :
336/// position, taille, marges, axes, et palette de couleurs.
337///
338/// # Champs
339///
340/// * `x`, `y` - Position du coin haut-gauche du graphique (en pixels)
341/// * `width`, `height` - Dimensions du graphique (en pixels)
342/// * `margin_*` - Marges internes pour les axes et labels
343/// * `x_axis`, `y_axis` - Configurations des deux axes
344/// * `*_color` - Couleurs pour le fond, les lignes, axes, grille, texte, labels
345
346#[derive(Clone, Copy, Debug)]
347pub struct PlotConfig {
348 pub x: i32,
349 pub y: i32,
350 pub width: i32,
351 pub height: i32,
352 pub margin_left: i32,
353 pub margin_right: i32,
354 pub margin_top: i32,
355 pub margin_bottom: i32,
356 pub x_axis: AxisConfig,
357 pub y_axis: AxisConfig,
358 pub bg_color: Color,
359 pub line_color: Color,
360 pub axis_color: Color,
361 pub grid_color: Color,
362 pub text_color: Color,
363 pub label_color: Color,
364}
365
366/// Gestionnaire du graphique avec axes statiques configurables.
367///
368/// `LineChart<N>` maintient un historique circulaire de N points de données et
369/// gère le rendu du graphique avec grille, axes, graduations et labels.
370///
371/// # Paramètre générique
372///
373/// * `N` - Nombre maximum de points historiques (doit être ≤ `PLOT_HISTORY_LIMIT` = 240)
374///
375/// # Fonctionnement interne
376///
377/// - **Ring buffer** : Les données sont stockées dans un tableau fixe avec un pointeur
378/// `head` qui tourne. Quand le buffer est plein, les nouvelles données écrasent les
379/// plus anciennes.
380/// - **Historique** : Seuls les N points les plus récents sont affichés.
381/// - **Rendu** : Les données sont converties en pixels via `scale_x()` et `scale_y()`,
382/// puis connectées par des lignes (Bresenham).
383pub struct LineChart<const N: usize> {
384 config: PlotConfig,
385 data: [f32; N],
386 head: usize,
387 count: usize,
388 plot_x: i32,
389 plot_y: i32,
390 plot_w: i32,
391 plot_h: i32,
392 initialized: bool, // Ajout du flag pour suivre l'état du tracé
393}
394
395impl<const N: usize> LineChart<N> {
396 /// Crée un nouveau gestionnaire de graphique.
397 ///
398 /// # Arguments
399 ///
400 /// * `config` - Configuration complète du graphique
401 ///
402 /// # Panics
403 ///
404 /// Panique si :
405 /// - N > `PLOT_HISTORY_LIMIT` (240)
406 /// - La configuration d'axe X est invalide
407 /// - La configuration d'axe Y est invalide
408
409 pub fn new(config: PlotConfig) -> Self {
410 assert!(N <= PLOT_HISTORY_LIMIT, "L'historique dépasse la limite physique horizontale.");
411 assert!(config.x_axis.is_valid(), "Configuration axe X invalide.");
412 assert!(config.y_axis.is_valid(), "Configuration axe Y invalide.");
413
414 let plot_x = config.x + config.margin_left;
415 let plot_y = config.y + config.margin_top;
416 let plot_w = config.width - config.margin_left - config.margin_right;
417 let plot_h = config.height - config.margin_top - config.margin_bottom;
418
419 Self {
420 config,
421 data: [0.0; N],
422 head: 0,
423 count: 0,
424 plot_x,
425 plot_y,
426 plot_w,
427 plot_h,
428 initialized: false, // Initialisé à false par défaut
429 }
430 }
431
432 /// Retourne une référence à la configuration du graphique.
433 #[inline]
434 pub fn config(&self) -> &PlotConfig {
435 &self.config
436 }
437
438 /// Ajoute une nouvelle valeur à l'historique.
439 ///
440 /// Si le buffer est plein (N points), la valeur la plus ancienne est remplacée.
441 ///
442 /// # Arguments
443 ///
444 /// * `value` - Valeur à ajouter (sera clampée à la plage Y-axis lors du rendu)
445
446 pub fn push(&mut self, value: f32) {
447 self.data[self.head] = value;
448 self.head = (self.head + 1) % N;
449 if self.count < N {
450 self.count += 1;
451 }
452 }
453
454 /// Efface l'historique et réinitialise le graphique.
455 pub fn clear(&mut self) {
456 self.head = 0;
457 self.count = 0;
458 self.data.fill(0.0);
459 self.initialized = false; // Permet de redessiner le cadre lors du prochain render
460 }
461
462 #[inline]
463 fn get_sample(&self, index: usize) -> f32 {
464 let oldest = if self.count < N { 0 } else { self.head };
465 self.data[(oldest + index) % N]
466 }
467
468 /// Convertit une valeur Y en coordonnée écran (pixels).
469 ///
470 /// La valeur est clampée à la plage [y_min, y_max] définie par `y_axis`,
471 /// puis convertie linéairement en pixels.
472 #[inline]
473 fn scale_y(&self, value: f32) -> i32 {
474 let y_min = self.config.y_axis.start;
475 let y_max = self.config.y_axis.end;
476
477 if y_max <= y_min {
478 return self.plot_y + self.plot_h - 1;
479 }
480
481 let clamped_val = value.max(y_min).min(y_max);
482 let ratio = (clamped_val - y_min) / (y_max - y_min);
483
484 self.plot_y + self.plot_h - 1 - (ratio * (self.plot_h - 1) as f32) as i32
485 }
486
487 /// Convertit un index de données en coordonnée X écran (pixels).
488 ///
489 /// Les N points sont distribués uniformément sur la largeur de la zone de tracé.
490 #[inline]
491 fn scale_x(&self, index: usize) -> i32 {
492 if N <= 1 {
493 return self.plot_x;
494 }
495 self.plot_x + (index as i32 * (self.plot_w - 1)) / (N as i32 - 1)
496 }
497
498 /// Convertit une valeur d'axe X en coordonnée écran (pour labels).
499 ///
500 /// Similaire à `scale_y`, mais pour l'axe X.
501 #[inline]
502 fn scale_x_value(&self, value: f32) -> i32 {
503 let x_min = self.config.x_axis.start;
504 let x_max = self.config.x_axis.end;
505
506 if x_max <= x_min {
507 return self.plot_x;
508 }
509
510 let clamped = value.max(x_min).min(x_max);
511 let ratio = (clamped - x_min) / (x_max - x_min);
512 self.plot_x + (ratio * (self.plot_w - 1) as f32) as i32
513 }
514
515 /// Affiche le graphique complet avec grille, axes, graduations et courbe.
516 ///
517 /// Cette méthode effectue :
518 /// 1. Remplissage du fond
519 /// 2. Grille horizontale (Y) et labels des graduations
520 /// 3. Grille verticale (X) et labels des graduations
521 /// 4. Labels des axes (titres)
522 /// 5. Bordures externes
523 /// 6. Tracé de la courbe (données)
524 ///
525 /// # Arguments
526 ///
527 /// * `gfx` - Contexte graphique initialisé
528 ///
529
530 pub async fn render<SPI, DC, RST>(&mut self, gfx: &mut Graphics<'_, SPI, DC, RST>)
531 where
532 SPI: SpiDevice,
533 DC: OutputPin,
534 RST: OutputPin,
535 {
536 let right_edge = self.plot_x + self.plot_w - 1;
537 let bottom_edge = self.plot_y + self.plot_h - 1;
538
539 // 1. TRACÉ UNIQUE DU CADRE ET DU TEXTE (Uniquement au premier passage) ──
540 if !self.initialized {
541 // Effacer l'intégralité de l'espace alloué au composant
542 let _ = gfx.display.fill_rect(
543 self.config.x as u16,
544 (self.config.y - 16).max(0) as u16,
545 (self.config.x + self.config.width) as u16,
546 (self.config.y + self.config.height + 16).min(SCREEN_H as i32) as u16,
547 self.config.bg_color,
548 ).await;
549
550 //Labels Y
551 let y_axis = &self.config.y_axis;
552 let y_range = y_axis.end - y_axis.start;
553 let tick_count_y = y_axis.tick_count();
554
555 for i in 0..tick_count_y {
556 let value = y_axis.start + (i as f32 * y_axis.step);
557 let ratio = (value - y_axis.start) / y_range;
558 let y_grid = bottom_edge - (ratio * (self.plot_h - 1) as f32) as i32;
559
560 // Couleur spéciale pour le vrai zéro
561 let label_color = if value.abs() < 0.001 {
562 Color::GREEN
563 } else {
564 self.config.text_color
565 };
566
567 let label_y = (y_grid - 4).max(self.config.y + 8).min(self.config.y + self.config.height - 8);
568
569 let _ = gfx.display.draw_f32(
570 (self.config.x + 2) as u16,
571 label_y as u16,
572 value,
573 1,
574 label_color,
575 self.config.bg_color,
576 ).await;
577 }
578
579 //Labels X
580 let x_axis = &self.config.x_axis;
581 let tick_count_x = x_axis.tick_count();
582
583 for i in 0..tick_count_x {
584 let value = x_axis.start + (i as f32 * x_axis.step);
585 let x_grid = self.scale_x_value(value);
586
587 let label_x = (x_grid - 8).max(self.config.x + 2).min(self.config.x + self.config.width - 20);
588
589 let _ = gfx.display.draw_f32(
590 label_x as u16,
591 (bottom_edge + 4) as u16,
592 value,
593 1,
594 self.config.text_color,
595 self.config.bg_color,
596 ).await;
597 }
598
599 // Titres des axes
600 let _ = gfx.display.draw_str(
601 (self.config.x + 2) as u16,
602 (self.config.y - 16).max(0) as u16,
603 y_axis.label,
604 self.config.label_color,
605 self.config.bg_color,
606 ).await;
607
608 let label_x_x = self.plot_x + (self.plot_w / 2) - ((x_axis.label.len() as i32 * 6) / 2);
609 let _ = gfx.display.draw_str(
610 label_x_x.max(self.config.x + 2) as u16,
611 (self.config.y + self.config.height + 4).min(SCREEN_H as i32 - 8) as u16,
612 x_axis.label,
613 self.config.label_color,
614 self.config.bg_color,
615 ).await;
616
617 // Bordures externes fixes
618 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;
619 let _ = gfx.display.draw_hline(self.plot_x as u16, bottom_edge as u16, self.plot_w as u16, self.config.axis_color).await;
620 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;
621 let _ = gfx.display.draw_vline(right_edge as u16, self.plot_y as u16, self.plot_h as u16, self.config.axis_color).await;
622
623 self.initialized = true;
624 }
625
626 // 2. NETTOYAGE DYNAMIQUE DE L'INTÉRIEUR STRICT (Sans toucher aux bordures) ──
627 let _ = gfx.display.fill_rect(
628 (self.plot_x + 1) as u16,
629 (self.plot_y + 1) as u16,
630 (right_edge - 1) as u16,
631 (bottom_edge - 1) as u16,
632 self.config.bg_color,
633 ).await;
634
635 // Grille horizontale (Y) interne
636 let y_axis = &self.config.y_axis;
637 let y_range = y_axis.end - y_axis.start;
638 let tick_count_y = y_axis.tick_count();
639 for i in 1..tick_count_y - 1 {
640 let value = y_axis.start + (i as f32 * y_axis.step);
641 let ratio = (value - y_axis.start) / y_range;
642 let y_grid = bottom_edge - (ratio * (self.plot_h - 1) as f32) as i32;
643
644 // Éviter de dessiner une grille si elle se superpose aux bordures de 1 pixel
645 if y_grid > self.plot_y && y_grid < bottom_edge {
646 // Si c'est la ligne du zéro, on peut optionnellement lui donner une couleur distinctive
647 let color = if value.abs() < 0.001 { Color::GREEN } else { self.config.grid_color };
648
649 let _ = gfx.display.draw_hline(
650 (self.plot_x + 1) as u16,
651 y_grid as u16,
652 (self.plot_w - 2) as u16,
653 color
654 ).await;
655 }
656 }
657
658 // Grille verticale (X) interne
659 let x_axis = &self.config.x_axis;
660 let tick_count_x = x_axis.tick_count();
661 for i in 1..tick_count_x - 1 {
662 let value = x_axis.start + (i as f32 * x_axis.step);
663 let x_grid = self.scale_x_value(value);
664
665 if x_grid > self.plot_x && x_grid < right_edge {
666 let _ = gfx.display.draw_vline(
667 x_grid as u16,
668 (self.plot_y + 1) as u16,
669 (self.plot_h - 2) as u16,
670 self.config.grid_color
671 ).await;
672 }
673 }
674
675 // 3. TRACÉ DE LA COURBE DES DONNÉES (Contrainte à l'intérieur strict) ──
676 if self.count < 2 {
677 if self.count == 1 {
678 let px = self.scale_x(0).max(self.plot_x + 1).min(right_edge - 1);
679 let py = self.scale_y(self.get_sample(0)).max(self.plot_y + 1).min(bottom_edge - 1);
680 gfx.pixel(px, py, self.config.line_color).await;
681 }
682 return;
683 }
684
685 // Récupération et clamping des points pour qu'ils ne bavent jamais sur la bordure blanche
686 let mut prev_x = self.scale_x(0).max(self.plot_x + 1).min(right_edge - 1);
687 let mut prev_y = self.scale_y(self.get_sample(0)).max(self.plot_y + 1).min(bottom_edge - 1);
688
689 for i in 1..self.count {
690 let next_x = self.scale_x(i).max(self.plot_x + 1).min(right_edge - 1);
691 let next_y = self.scale_y(self.get_sample(i)).max(self.plot_y + 1).min(bottom_edge - 1);
692
693 line(gfx, prev_x, prev_y, next_x, next_y, self.config.line_color).await;
694
695 prev_x = next_x;
696 prev_y = next_y;
697 }
698 }
699
700}
701
702 // ─────────────────────────────────────────────────────────────────────────────
703// LineChart framebuffer — zéro clignotement
704// ─────────────────────────────────────────────────────────────────────────────
705
706/// Version framebuffer de [`LineChart`].
707///
708/// Identique à `LineChart` mais toutes les opérations de dessin écrivent
709/// dans le framebuffer RAM via `GraphicsBuffered` et les primitives `*_buf`.
710/// Aucun pixel n'est envoyé à l'écran avant l'appel à `flush()`.
711///
712/// # Patron d'utilisation
713///
714/// ```rust,no_run
715/// let mut chart: LineChartBuffered<100> = LineChartBuffered::new(config);
716/// chart.push(12.4);
717/// chart.render_buf(&mut ecran); // tout en RAM, synchrone
718/// ecran.flush().await.unwrap(); // envoi unique → zéro clignotement
719/// ```
720pub struct LineChartBuffered<const N: usize> {
721 config: PlotConfig,
722 data: [f32; N],
723 head: usize,
724 count: usize,
725 plot_x: i32,
726 plot_y: i32,
727 plot_w: i32,
728 plot_h: i32,
729 initialized: bool,
730}
731
732impl<const N: usize> LineChartBuffered<N> {
733 pub fn new(config: PlotConfig) -> Self {
734 assert!(N <= PLOT_HISTORY_LIMIT);
735 assert!(config.x_axis.is_valid());
736 assert!(config.y_axis.is_valid());
737
738 let plot_x = config.x + config.margin_left;
739 let plot_y = config.y + config.margin_top;
740 let plot_w = config.width - config.margin_left - config.margin_right;
741 let plot_h = config.height - config.margin_top - config.margin_bottom;
742
743 Self { config, data: [0.0; N], head: 0, count: 0,
744 plot_x, plot_y, plot_w, plot_h, initialized: false }
745 }
746
747 pub fn push(&mut self, value: f32) {
748 self.data[self.head] = value;
749 self.head = (self.head + 1) % N;
750 if self.count < N { self.count += 1; }
751 }
752
753 pub fn clear(&mut self) {
754 self.head = 0; self.count = 0;
755 self.data.fill(0.0);
756 self.initialized = false;
757 }
758
759 #[inline]
760 fn get_sample(&self, index: usize) -> f32 {
761 let oldest = if self.count < N { 0 } else { self.head };
762 self.data[(oldest + index) % N]
763 }
764
765 #[inline]
766 fn scale_y(&self, value: f32) -> i32 {
767 let y_min = self.config.y_axis.start;
768 let y_max = self.config.y_axis.end;
769 if y_max <= y_min { return self.plot_y + self.plot_h - 1; }
770 let ratio = (value.max(y_min).min(y_max) - y_min) / (y_max - y_min);
771 self.plot_y + self.plot_h - 1 - (ratio * (self.plot_h - 1) as f32) as i32
772 }
773
774 #[inline]
775 fn scale_x(&self, index: usize) -> i32 {
776 if N <= 1 { return self.plot_x; }
777 self.plot_x + (index as i32 * (self.plot_w - 1)) / (N as i32 - 1)
778 }
779
780 #[inline]
781 fn scale_x_value(&self, value: f32) -> i32 {
782 let x_min = self.config.x_axis.start;
783 let x_max = self.config.x_axis.end;
784 if x_max <= x_min { return self.plot_x; }
785 let ratio = (value.max(x_min).min(x_max) - x_min) / (x_max - x_min);
786 self.plot_x + (ratio * (self.plot_w - 1) as f32) as i32
787 }
788
789 /// Rendu complet dans le framebuffer — **synchrone, sans `.await`**.
790 ///
791 /// Appeler `ecran.flush().await` après pour envoyer à l'écran.
792 pub fn render_buf<SPI, DC, RST>(
793 &mut self,
794 ecran: &mut St7789vBuffered<SPI, DC, RST>,
795 ) where
796 SPI: embedded_hal_async::spi::SpiDevice,
797 DC: embedded_hal::digital::OutputPin,
798 RST: embedded_hal::digital::OutputPin,
799 {
800 let right_edge = self.plot_x + self.plot_w - 1;
801 let bottom_edge = self.plot_y + self.plot_h - 1;
802
803 // ── 1. Cadre, labels, titres — une seule fois ─────────────────────
804 if !self.initialized {
805 // Fond
806 ecran.fill_rect_buf(
807 self.config.x as u16,
808 (self.config.y - 16).max(0) as u16,
809 (self.config.x + self.config.width) as u16,
810 (self.config.y + self.config.height + 16).min(SCREEN_H as i32) as u16,
811 self.config.bg_color,
812 );
813
814 // Labels Y
815 let y_axis = &self.config.y_axis;
816 let y_range = y_axis.end - y_axis.start;
817 for i in 0..y_axis.tick_count() {
818 let value = y_axis.start + i as f32 * y_axis.step;
819 let ratio = (value - y_axis.start) / y_range;
820 let y_grid = bottom_edge - (ratio * (self.plot_h - 1) as f32) as i32;
821 let label_y = (y_grid - 4).max(self.config.y + 8).min(self.config.y + self.config.height - 8);
822 let color = if value.abs() < 0.001 { Color::GREEN } else { self.config.text_color };
823 // Rendu du label f32 via draw_f32_buf (voir ci-dessous)
824 self.draw_f32_buf(ecran, (self.config.x + 2) as u16, label_y as u16, value, 1, color);
825 }
826
827 // Labels X
828 let x_axis = &self.config.x_axis;
829 for i in 0..x_axis.tick_count() {
830 let value = x_axis.start + i as f32 * x_axis.step;
831 let x_grid = self.scale_x_value(value);
832 let label_x = (x_grid - 8).max(self.config.x + 2).min(self.config.x + self.config.width - 20);
833 self.draw_f32_buf(ecran, label_x as u16, (bottom_edge + 4) as u16, value, 1, self.config.text_color);
834 }
835
836 // Titres
837 ecran.draw_str_buf(
838 (self.config.x + 2) as u16,
839 (self.config.y - 16).max(0) as u16,
840 y_axis.label,
841 self.config.label_color,
842 self.config.bg_color,
843 );
844 let label_x_x = self.plot_x + (self.plot_w / 2) - (x_axis.label.len() as i32 * 6 / 2);
845 ecran.draw_str_buf(
846 label_x_x.max(self.config.x + 2) as u16,
847 (self.config.y + self.config.height + 4).min(SCREEN_H as i32 - 8) as u16,
848 x_axis.label,
849 self.config.label_color,
850 self.config.bg_color,
851 );
852
853 // Bordures
854 for px in self.plot_x..=right_edge {
855 ecran.set_pixel(px as u16, self.plot_y as u16, self.config.axis_color);
856 ecran.set_pixel(px as u16, bottom_edge as u16, self.config.axis_color);
857 }
858 for py in self.plot_y..=bottom_edge {
859 ecran.set_pixel(self.plot_x as u16, py as u16, self.config.axis_color);
860 ecran.set_pixel(right_edge as u16, py as u16, self.config.axis_color);
861 }
862
863 self.initialized = true;
864 }
865
866 // ── 2. Nettoyage intérieur ────────────────────────────────────────
867 ecran.fill_rect_buf(
868 (self.plot_x + 1) as u16,
869 (self.plot_y + 1) as u16,
870 (right_edge - 1) as u16,
871 (bottom_edge - 1) as u16,
872 self.config.bg_color,
873 );
874
875 // ── 3. Grille Y ───────────────────────────────────────────────────
876 let y_axis = &self.config.y_axis;
877 let y_range = y_axis.end - y_axis.start;
878 for i in 1..y_axis.tick_count().saturating_sub(1) {
879 let value = y_axis.start + i as f32 * y_axis.step;
880 let ratio = (value - y_axis.start) / y_range;
881 let y_grid = bottom_edge - (ratio * (self.plot_h - 1) as f32) as i32;
882 if y_grid > self.plot_y && y_grid < bottom_edge {
883 let color = if value.abs() < 0.001 { Color::GREEN } else { self.config.grid_color };
884 ecran.fill_rect_buf(
885 (self.plot_x + 1) as u16, y_grid as u16,
886 (right_edge - 1) as u16, y_grid as u16,
887 color,
888 );
889 }
890 }
891
892 // ── 4. Grille X ───────────────────────────────────────────────────
893 let x_axis = &self.config.x_axis;
894 for i in 1..x_axis.tick_count().saturating_sub(1) {
895 let value = x_axis.start + i as f32 * x_axis.step;
896 let x_grid = self.scale_x_value(value);
897 if x_grid > self.plot_x && x_grid < right_edge {
898 ecran.fill_rect_buf(
899 x_grid as u16, (self.plot_y + 1) as u16,
900 x_grid as u16, (bottom_edge - 1) as u16,
901 self.config.grid_color,
902 );
903 }
904 }
905
906 // ── 5. Courbe ─────────────────────────────────────────────────────
907 if self.count < 2 {
908 if self.count == 1 {
909 let px = self.scale_x(0).max(self.plot_x + 1).min(right_edge - 1);
910 let py = self.scale_y(self.get_sample(0)).max(self.plot_y + 1).min(bottom_edge - 1);
911 ecran.set_pixel(px as u16, py as u16, self.config.line_color);
912 }
913 return;
914 }
915
916 let mut gfx = GraphicsBuffered::new(ecran);
917 let mut prev_x = self.scale_x(0).max(self.plot_x + 1).min(right_edge - 1);
918 let mut prev_y = self.scale_y(self.get_sample(0)).max(self.plot_y + 1).min(bottom_edge - 1);
919
920 for i in 1..self.count {
921 let next_x = self.scale_x(i).max(self.plot_x + 1).min(right_edge - 1);
922 let next_y = self.scale_y(self.get_sample(i)).max(self.plot_y + 1).min(bottom_edge - 1);
923 line_buf(&mut gfx, prev_x, prev_y, next_x, next_y, self.config.line_color);
924 prev_x = next_x;
925 prev_y = next_y;
926 }
927 }
928
929 /// Rendu d'un f32 en framebuffer (équivalent de `draw_f32` pour St7789vBuffered).
930 fn draw_f32_buf<SPI, DC, RST>(
931 &self,
932 ecran: &mut St7789vBuffered<SPI, DC, RST>,
933 x: u16, y: u16,
934 val: f32,
935 decimales: u8,
936 fg: Color,
937 ) where
938 SPI: embedded_hal_async::spi::SpiDevice,
939 DC: embedded_hal::digital::OutputPin,
940 RST: embedded_hal::digital::OutputPin,
941 {
942 let bg = self.config.bg_color;
943 if val.is_nan() { ecran.draw_str_buf(x, y, b"NaN", fg, bg); return; }
944 if val.is_infinite() {
945 if val > 0.0 { ecran.draw_str_buf(x, y, b"+Inf", fg, bg); }
946 else { ecran.draw_str_buf(x, y, b"-Inf", fg, bg); }
947 return;
948 }
949 let negatif = val < 0.0;
950 let mut abs = if negatif { -val } else { val };
951 let mut cx = x;
952 if negatif { cx = ecran.draw_char_buf(cx, y, 10, fg, bg); } // '-'
953 let facteur = { let mut f = 1u32; for _ in 0..decimales { f *= 10; } f };
954 abs += 0.5 / facteur as f32;
955 let entier = abs as u32;
956 cx = ecran.draw_u32_buf(cx, y, entier, fg, bg);
957 if decimales > 0 {
958 cx = ecran.draw_char_buf(cx, y, 38, fg, bg); // '.'
959 let mut frac = abs - entier as f32;
960 let mut chiffres = [0u8; 8];
961 for i in 0..decimales as usize { frac *= 10.0; chiffres[i] = frac as u8; frac -= chiffres[i] as f32; }
962 for i in 0..decimales as usize { cx = ecran.draw_char_buf(cx, y, chiffres[i] as usize, fg, bg); }
963 }
964 let _ = cx;
965 }
966}