Skip to main content

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}