embassy-piezo 0.1.0

Driver async no_std pour capteur de vibration piézoélectrique, basé sur Embassy et esp-hal.
// Copyright (C) 2026 Jorge Andre Castro
// GPL-2.0-or-later

//! # embassy-piezo
//!
//! Driver asynchrone `no_std` pour capteur de vibration piézoélectrique (DO).
//!
//! Portage **esp-hal** (ESP32 / ESP32-Cx / ESP32-Sx). La logique métier est
//! inchangée par rapport à la version `embassy-rp` : seul le type `Input`
//! provient désormais de `esp_hal::gpio` au lieu de `embassy_rp::gpio`.
//!
//! ## Caractéristiques
//! - Async natif via GPIO `esp-hal` (trait `embedded-hal-async` `Wait`)
//! - Détection front montant sans polling (zéro CPU en attente)
//! - Comptage d'événements avec timeout configurable
//! - Debounce configurable pour filtrer les rebonds électriques
//! - Compatible signaux globaux inter-tâches
//! - Zéro allocation, zéro `unsafe`
//!
//! ## Pré-requis (Cargo.toml)
//! ```toml
//! [dependencies]
//! esp-hal          = { version = "1", features = ["esp32"] } # adapter la cible
//! esp-hal-embassy  = "0.99"   # fournit l'executor + le time-driver embassy
//! embassy-executor = "0.7"
//! embassy-time     = "0.4"
//! ```
//! `embassy-time` reste utilisé tel quel : `esp-hal-embassy` fournit
//! l'implémentation de l'horloge (`embassy_time::Timer` / `with_timeout`)
//! pour toutes les puces supportées par `esp-hal`, exactement comme
//! `embassy-rp` le fait pour le RP2040.
//!
//! ## Exemple minimal
//! ```rust,ignore
//! use esp_hal::gpio::{Input, InputConfig, Pull};
//!
//! let pin = Input::new(peripherals.GPIO15, InputConfig::default().with_pull(Pull::Down));
//! let mut piezo = PiezoVibration::new(pin);
//!
//! piezo.wait_for_vibration().await;
//! let count = piezo.count();
//! ```
//!
//! ## Exemple avec debounce
//! ```rust,ignore
//! use esp_hal::gpio::{Input, InputConfig, Pull};
//!
//! // Chocs humains : ignorer les rebonds électriques sous 100 ms
//! let pin = Input::new(peripherals.GPIO15, InputConfig::default().with_pull(Pull::Down));
//! let mut piezo = PiezoVibration::new_with_debounce(pin, 100);
//!
//! let event = piezo.wait_for_vibration().await;
//! defmt::info!("choc #{}", event.count);
//! ```
//!
//! ## Initialisation requise côté application
//! ```rust,ignore
//! use esp_hal::timer::timg::TimerGroup;
//!
//! let peripherals = esp_hal::init(esp_hal::Config::default());
//! let timg0 = TimerGroup::new(peripherals.TIMG0);
//! esp_hal_embassy::init(timg0.timer0); // requis avant tout Timer::after / with_timeout
//! ```

#![no_std]
#![forbid(unsafe_code)]

pub mod error;
pub mod signals;

pub use error::PiezoError;
pub use esp_hal::gpio::Level;

use embassy_time::{with_timeout, Duration, Timer};
use esp_hal::gpio::Input;

/// Événement de vibration horodaté.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct VibrationEvent {
    /// Nombre de vibrations détectées depuis la création du driver.
    pub count: u32,
    /// `true` si le pin est actuellement haut (vibration en cours).
    pub active: bool,
}

/// Driver pour capteur de vibration piézoélectrique (sortie numérique DO).
///
/// Le pin DO du capteur passe à l'état haut lors d'une vibration.
/// La sensibilité est réglable via le potentiomètre du module.
///
/// # Debounce
///
/// Le paramètre `debounce_ms` filtre les rebonds électriques après un front
/// montant : si le pin est retombé avant la fin du délai, l'événement est
/// ignoré et l'attente recommence.
///
/// | `debounce_ms` | Cas d'usage                                      |
/// |---------------|--------------------------------------------------|
/// | `0`           | Désactivé tout front montant est compté        |
/// | `5`           | Filtrage électrique basique                      |
/// | `100`         | Chocs humains (frappe, impact mécanique)         |
///
/// ⚠ Ne pas dépasser la durée physique du signal : une valeur trop élevée
/// ferait manquer des événements légitimes.
pub struct PiezoVibration<'a> {
    pin: Input<'a>,
    count: u32,
    debounce_ms: u64,
}

impl<'a> PiezoVibration<'a> {
    /// Crée un driver sans debounce (`debounce_ms = 0`).
    ///
    /// Tout front montant est immédiatement comptabilisé.
    ///
    /// # Arguments
    /// * `pin` : `Input` esp-hal déjà configuré (DO du capteur).
    ///   Utiliser `Pull::Down` si le capteur ne tire pas lui-même la ligne :
    ///   `Input::new(gpio, InputConfig::default().with_pull(Pull::Down))`.
    pub fn new(pin: Input<'a>) -> Self {
        Self { pin, count: 0, debounce_ms: 0 }
    }

    /// Crée un driver avec debounce.
    ///
    /// Après chaque front montant, attend `debounce_ms` millisecondes avant
    /// de valider l'événement. Si le pin est retombé entre-temps (rebond),
    /// l'événement est ignoré silencieusement et l'attente recommence.
    ///
    /// # Arguments
    /// * `pin`         : `Input` esp-hal déjà configuré (DO du capteur).
    /// * `debounce_ms` : Délai de confirmation en millisecondes (`0` = désactivé).
    ///
    /// # Valeurs recommandées
    /// - `0`   : tout voir : haute fréquence, signaux continus, tests
    /// - `5`   : filtrage électrique basique
    /// - `100` : chocs humains discrets (frappe, impact mécanique)
    pub fn new_with_debounce(pin: Input<'a>, debounce_ms: u64) -> Self {
        Self { pin, count: 0, debounce_ms }
    }

    //  Lecture instantanée 
    /// `true` si une vibration est détectée à cet instant.
    pub fn is_vibrating(&self) -> bool {
        self.pin.is_high()
    }

    /// Nombre de vibrations comptées depuis la création du driver.
    pub fn count(&self) -> u32 {
        self.count
    }

    /// Remet le compteur à zéro.
    pub fn reset_count(&mut self) {
        self.count = 0;
    }

    /// Retourne un snapshot de l'état courant.
    pub fn state(&self) -> VibrationEvent {
        VibrationEvent {
            count: self.count,
            active: self.is_vibrating(),
        }
    }

    //  Attente asynchrone 

    /// Attend le prochain front montant (début de vibration).
    ///
    /// Ne consomme pas de CPU pendant l'attente.
    ///
    /// Si `debounce_ms > 0`, confirme que le pin est encore haut après le
    /// délai avant de valider. Les rebonds (pin retombé avant la fin du délai)
    /// sont ignorés et l'attente recommence automatiquement.
    ///
    /// Incrémente le compteur interne à chaque détection validée.
    pub async fn wait_for_vibration(&mut self) -> VibrationEvent {
        loop {
            self.pin.wait_for_high().await;

            if self.debounce_ms > 0 {
                Timer::after(Duration::from_millis(self.debounce_ms)).await;
                // Rebond : signal retombé avant la fin du délai → on ignore
                if !self.is_vibrating() {
                    continue;
                }
            }

            self.count = self.count.saturating_add(1);
            return self.state();
        }
    }

    /// Attend le prochain front montant avec un timeout.
    ///
    /// Le timeout englobe l'intégralité de la détection, délai de debounce
    /// inclus : un choc détecté à `t` ms doit laisser au moins `debounce_ms`
    /// ms avant l'expiration du timeout pour être validé.
    ///
    /// # Retour
    /// - `Ok(VibrationEvent)` : vibration détectée dans le délai
    /// - `Err(PiezoError::Timeout)` : délai expiré sans vibration confirmée
    pub async fn wait_for_vibration_timeout(
        &mut self,
        timeout: Duration,
    ) -> Result<VibrationEvent, PiezoError> {
        with_timeout(timeout, async {
            loop {
                self.pin.wait_for_high().await;

                if self.debounce_ms > 0 {
                    Timer::after(Duration::from_millis(self.debounce_ms)).await;
                    if !self.is_vibrating() {
                        continue;
                    }
                }

                return;
            }
        })
        .await
        .map_err(|_| PiezoError::Timeout)?;

        self.count = self.count.saturating_add(1);
        Ok(self.state())
    }

    /// Attend la fin d'une vibration (front descendant).
    ///
    /// Utile pour mesurer la durée d'un événement.
    pub async fn wait_for_silence(&mut self) {
        self.pin.wait_for_low().await;
    }

    /// Attend la fin d'une vibration avec un timeout.
    ///
    /// # Retour
    /// - `Ok(())` : silence atteint dans le délai
    /// - `Err(PiezoError::Timeout)` : vibration toujours active après le délai
    pub async fn wait_for_silence_timeout(
        &mut self,
        timeout: Duration,
    ) -> Result<(), PiezoError> {
        with_timeout(timeout, self.pin.wait_for_low())
            .await
            .map_err(|_| PiezoError::Timeout)
    }

    // Comptage sur fenêtre temporelle 

    /// Compte les vibrations détectées pendant une durée donnée.
    ///
    /// Utile pour mesurer une fréquence ou détecter une activité soutenue.
    ///
    /// # Exemple
    /// ```rust,ignore
    /// // Combien de chocs en 2 secondes ?
    /// let n = piezo.count_during(Duration::from_secs(2)).await;
    /// ```
    ///
    /// ⚠ Cette méthode suppose que `wait_for_vibration()` est appelée de
    /// manière concurrente (par exemple dans une autre tâche) pour incrémenter
    /// le compteur interne pendant que le délai s'écoule.
    pub async fn count_during(&mut self, window: Duration) -> u32 {
        let start = self.count;
        Timer::after(window).await;
        // Les fronts montants sont comptés par wait_for_vibration()
        // appelé dans une tâche parallèle  ici on lit juste le delta.
        self.count.saturating_sub(start)
    }
}