embassy_piezo/lib.rs
1// Copyright (C) 2026 Jorge Andre Castro
2// GPL-2.0-or-later
3
4//! # embassy-piezo
5//!
6//! Driver asynchrone `no_std` pour capteur de vibration piézoélectrique (DO).
7//!
8//! Portage **esp-hal** (ESP32 / ESP32-Cx / ESP32-Sx). La logique métier est
9//! inchangée par rapport à la version `embassy-rp` : seul le type `Input`
10//! provient désormais de `esp_hal::gpio` au lieu de `embassy_rp::gpio`.
11//!
12//! ## Caractéristiques
13//! - Async natif via GPIO `esp-hal` (trait `embedded-hal-async` `Wait`)
14//! - Détection front montant sans polling (zéro CPU en attente)
15//! - Comptage d'événements avec timeout configurable
16//! - Debounce configurable pour filtrer les rebonds électriques
17//! - Compatible signaux globaux inter-tâches
18//! - Zéro allocation, zéro `unsafe`
19//!
20//! ## Pré-requis (Cargo.toml)
21//! ```toml
22//! [dependencies]
23//! esp-hal = { version = "1", features = ["esp32"] } # adapter la cible
24//! esp-hal-embassy = "0.99" # fournit l'executor + le time-driver embassy
25//! embassy-executor = "0.7"
26//! embassy-time = "0.4"
27//! ```
28//! `embassy-time` reste utilisé tel quel : `esp-hal-embassy` fournit
29//! l'implémentation de l'horloge (`embassy_time::Timer` / `with_timeout`)
30//! pour toutes les puces supportées par `esp-hal`, exactement comme
31//! `embassy-rp` le fait pour le RP2040.
32//!
33//! ## Exemple minimal
34//! ```rust,ignore
35//! use esp_hal::gpio::{Input, InputConfig, Pull};
36//!
37//! let pin = Input::new(peripherals.GPIO15, InputConfig::default().with_pull(Pull::Down));
38//! let mut piezo = PiezoVibration::new(pin);
39//!
40//! piezo.wait_for_vibration().await;
41//! let count = piezo.count();
42//! ```
43//!
44//! ## Exemple avec debounce
45//! ```rust,ignore
46//! use esp_hal::gpio::{Input, InputConfig, Pull};
47//!
48//! // Chocs humains : ignorer les rebonds électriques sous 100 ms
49//! let pin = Input::new(peripherals.GPIO15, InputConfig::default().with_pull(Pull::Down));
50//! let mut piezo = PiezoVibration::new_with_debounce(pin, 100);
51//!
52//! let event = piezo.wait_for_vibration().await;
53//! defmt::info!("choc #{}", event.count);
54//! ```
55//!
56//! ## Initialisation requise côté application
57//! ```rust,ignore
58//! use esp_hal::timer::timg::TimerGroup;
59//!
60//! let peripherals = esp_hal::init(esp_hal::Config::default());
61//! let timg0 = TimerGroup::new(peripherals.TIMG0);
62//! esp_hal_embassy::init(timg0.timer0); // requis avant tout Timer::after / with_timeout
63//! ```
64
65#![no_std]
66#![forbid(unsafe_code)]
67
68pub mod error;
69pub mod signals;
70
71pub use error::PiezoError;
72pub use esp_hal::gpio::Level;
73
74use embassy_time::{with_timeout, Duration, Timer};
75use esp_hal::gpio::Input;
76
77/// Événement de vibration horodaté.
78#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
79pub struct VibrationEvent {
80 /// Nombre de vibrations détectées depuis la création du driver.
81 pub count: u32,
82 /// `true` si le pin est actuellement haut (vibration en cours).
83 pub active: bool,
84}
85
86/// Driver pour capteur de vibration piézoélectrique (sortie numérique DO).
87///
88/// Le pin DO du capteur passe à l'état haut lors d'une vibration.
89/// La sensibilité est réglable via le potentiomètre du module.
90///
91/// # Debounce
92///
93/// Le paramètre `debounce_ms` filtre les rebonds électriques après un front
94/// montant : si le pin est retombé avant la fin du délai, l'événement est
95/// ignoré et l'attente recommence.
96///
97/// | `debounce_ms` | Cas d'usage |
98/// |---------------|--------------------------------------------------|
99/// | `0` | Désactivé tout front montant est compté |
100/// | `5` | Filtrage électrique basique |
101/// | `100` | Chocs humains (frappe, impact mécanique) |
102///
103/// ⚠ Ne pas dépasser la durée physique du signal : une valeur trop élevée
104/// ferait manquer des événements légitimes.
105pub struct PiezoVibration<'a> {
106 pin: Input<'a>,
107 count: u32,
108 debounce_ms: u64,
109}
110
111impl<'a> PiezoVibration<'a> {
112 /// Crée un driver sans debounce (`debounce_ms = 0`).
113 ///
114 /// Tout front montant est immédiatement comptabilisé.
115 ///
116 /// # Arguments
117 /// * `pin` : `Input` esp-hal déjà configuré (DO du capteur).
118 /// Utiliser `Pull::Down` si le capteur ne tire pas lui-même la ligne :
119 /// `Input::new(gpio, InputConfig::default().with_pull(Pull::Down))`.
120 pub fn new(pin: Input<'a>) -> Self {
121 Self { pin, count: 0, debounce_ms: 0 }
122 }
123
124 /// Crée un driver avec debounce.
125 ///
126 /// Après chaque front montant, attend `debounce_ms` millisecondes avant
127 /// de valider l'événement. Si le pin est retombé entre-temps (rebond),
128 /// l'événement est ignoré silencieusement et l'attente recommence.
129 ///
130 /// # Arguments
131 /// * `pin` : `Input` esp-hal déjà configuré (DO du capteur).
132 /// * `debounce_ms` : Délai de confirmation en millisecondes (`0` = désactivé).
133 ///
134 /// # Valeurs recommandées
135 /// - `0` : tout voir : haute fréquence, signaux continus, tests
136 /// - `5` : filtrage électrique basique
137 /// - `100` : chocs humains discrets (frappe, impact mécanique)
138 pub fn new_with_debounce(pin: Input<'a>, debounce_ms: u64) -> Self {
139 Self { pin, count: 0, debounce_ms }
140 }
141
142 // Lecture instantanée
143 /// `true` si une vibration est détectée à cet instant.
144 pub fn is_vibrating(&self) -> bool {
145 self.pin.is_high()
146 }
147
148 /// Nombre de vibrations comptées depuis la création du driver.
149 pub fn count(&self) -> u32 {
150 self.count
151 }
152
153 /// Remet le compteur à zéro.
154 pub fn reset_count(&mut self) {
155 self.count = 0;
156 }
157
158 /// Retourne un snapshot de l'état courant.
159 pub fn state(&self) -> VibrationEvent {
160 VibrationEvent {
161 count: self.count,
162 active: self.is_vibrating(),
163 }
164 }
165
166 // Attente asynchrone
167
168 /// Attend le prochain front montant (début de vibration).
169 ///
170 /// Ne consomme pas de CPU pendant l'attente.
171 ///
172 /// Si `debounce_ms > 0`, confirme que le pin est encore haut après le
173 /// délai avant de valider. Les rebonds (pin retombé avant la fin du délai)
174 /// sont ignorés et l'attente recommence automatiquement.
175 ///
176 /// Incrémente le compteur interne à chaque détection validée.
177 pub async fn wait_for_vibration(&mut self) -> VibrationEvent {
178 loop {
179 self.pin.wait_for_high().await;
180
181 if self.debounce_ms > 0 {
182 Timer::after(Duration::from_millis(self.debounce_ms)).await;
183 // Rebond : signal retombé avant la fin du délai → on ignore
184 if !self.is_vibrating() {
185 continue;
186 }
187 }
188
189 self.count = self.count.saturating_add(1);
190 return self.state();
191 }
192 }
193
194 /// Attend le prochain front montant avec un timeout.
195 ///
196 /// Le timeout englobe l'intégralité de la détection, délai de debounce
197 /// inclus : un choc détecté à `t` ms doit laisser au moins `debounce_ms`
198 /// ms avant l'expiration du timeout pour être validé.
199 ///
200 /// # Retour
201 /// - `Ok(VibrationEvent)` : vibration détectée dans le délai
202 /// - `Err(PiezoError::Timeout)` : délai expiré sans vibration confirmée
203 pub async fn wait_for_vibration_timeout(
204 &mut self,
205 timeout: Duration,
206 ) -> Result<VibrationEvent, PiezoError> {
207 with_timeout(timeout, async {
208 loop {
209 self.pin.wait_for_high().await;
210
211 if self.debounce_ms > 0 {
212 Timer::after(Duration::from_millis(self.debounce_ms)).await;
213 if !self.is_vibrating() {
214 continue;
215 }
216 }
217
218 return;
219 }
220 })
221 .await
222 .map_err(|_| PiezoError::Timeout)?;
223
224 self.count = self.count.saturating_add(1);
225 Ok(self.state())
226 }
227
228 /// Attend la fin d'une vibration (front descendant).
229 ///
230 /// Utile pour mesurer la durée d'un événement.
231 pub async fn wait_for_silence(&mut self) {
232 self.pin.wait_for_low().await;
233 }
234
235 /// Attend la fin d'une vibration avec un timeout.
236 ///
237 /// # Retour
238 /// - `Ok(())` : silence atteint dans le délai
239 /// - `Err(PiezoError::Timeout)` : vibration toujours active après le délai
240 pub async fn wait_for_silence_timeout(
241 &mut self,
242 timeout: Duration,
243 ) -> Result<(), PiezoError> {
244 with_timeout(timeout, self.pin.wait_for_low())
245 .await
246 .map_err(|_| PiezoError::Timeout)
247 }
248
249 // Comptage sur fenêtre temporelle
250
251 /// Compte les vibrations détectées pendant une durée donnée.
252 ///
253 /// Utile pour mesurer une fréquence ou détecter une activité soutenue.
254 ///
255 /// # Exemple
256 /// ```rust,ignore
257 /// // Combien de chocs en 2 secondes ?
258 /// let n = piezo.count_during(Duration::from_secs(2)).await;
259 /// ```
260 ///
261 /// ⚠ Cette méthode suppose que `wait_for_vibration()` est appelée de
262 /// manière concurrente (par exemple dans une autre tâche) pour incrémenter
263 /// le compteur interne pendant que le délai s'écoule.
264 pub async fn count_during(&mut self, window: Duration) -> u32 {
265 let start = self.count;
266 Timer::after(window).await;
267 // Les fronts montants sont comptés par wait_for_vibration()
268 // appelé dans une tâche parallèle ici on lit juste le delta.
269 self.count.saturating_sub(start)
270 }
271}