uart_proto_detector/lib.rs
1// Copyright (C) 2026 Jorge Andre Castro <georgeandrec@gmail.com>
2// SPDX-License-Identifier: GPL-2.0-or-later
3//
4// This program is free software; you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation; either version 2 of the License, or
7// (at your option) any later version.
8
9//! # uart-proto-detector
10//!
11//! Bibliothèque universelle `no_std` pour la détection et le découpage de trames
12//! UART sur systèmes embarqués.
13//!
14//! ## Fonctionnement
15//!
16//! Le parser accumule les octets reçus un à un, valide l'en-tête, détermine la
17//! longueur attendue selon la stratégie choisie, puis retourne la trame complète.
18//! La validation du CRC est volontairement **exclue** de cette bibliothèque : chaque
19//! composant implémentant son propre algorithme, cette responsabilité revient au code
20//! appelant.
21//!
22//! ## Exemple minimal
23//!
24//! ```rust
25//! use uart_proto_detector::{GenericParser, ParserConfig, PacketLengthStrategy};
26//!
27//! // Protocole avec en-tête [0xAA, 0x55] et longueur fixe de 8 octets
28//! let config = ParserConfig {
29//! header: &[0xAA, 0x55],
30//! length_strategy: PacketLengthStrategy::Fixed(8),
31//! };
32//!
33//! let mut parser = GenericParser::new(config);
34//!
35//! let stream: &[u8] = &[0xAA, 0x55, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
36//! for &byte in stream {
37//! match parser.parse_byte(byte) {
38//! Ok(Some(packet)) => { /* traiter le paquet complet */ }
39//! Ok(None) => { /* trame en cours d'accumulation */ }
40//! Err(e) => { /* gérer l'erreur */ }
41//! }
42//! }
43//! ```
44
45#![no_std]
46#![forbid(unsafe_code)]
47#![warn(missing_docs)]
48
49// ─── Taille du buffer interne ───────────────────────────────────────────────
50
51/// Taille du buffer d'accumulation en octets.
52///
53/// Peut être surchargée à la compilation via la feature `large-buffer` ou en
54/// modifiant directement cette constante selon les contraintes mémoire de la
55/// cible.
56pub const BUFFER_SIZE: usize = 128;
57
58// ─── Erreurs ─────────────────────────────────────────────────────────────────
59
60/// Erreurs pouvant survenir lors du parsing d'une trame UART.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum ParserError {
63 /// Le buffer interne a été dépassé avant la fin d'une trame valide.
64 ///
65 /// Cela indique généralement un protocole mal configuré (longueur trop
66 /// grande) ou un flux de données corrompu.
67 BufferOverflow,
68
69 /// La longueur calculée à partir de l'octet de longueur dépasse
70 /// [`BUFFER_SIZE`], ce qui rendrait la trame impossible à stocker.
71 ///
72 /// Contient la longueur calculée.
73 PacketTooLarge(usize),
74
75 /// L'index de l'octet de longueur (champ `index` de
76 /// [`PacketLengthStrategy::LengthByte`]) est supérieur ou égal à
77 /// [`BUFFER_SIZE`], ce qui constitue une configuration invalide.
78 InvalidLengthByteIndex,
79}
80
81// ─── Stratégie de longueur ───────────────────────────────────────────────────
82
83/// Stratégie utilisée pour déterminer la longueur totale d'une trame.
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum PacketLengthStrategy {
86 /// La trame a une longueur totale fixe (en-tête inclus).
87 ///
88 /// # Exemple
89 ///
90 /// Pour un protocole dont chaque trame fait toujours 8 octets :
91 ///
92 /// ```rust
93 /// # use uart_proto_detector::PacketLengthStrategy;
94 /// let strategy = PacketLengthStrategy::Fixed(8);
95 /// ```
96 Fixed(usize),
97
98 /// Un octet situé à la position `index` dans le buffer indique le nombre
99 /// d'octets restants après lui-même. `offset` est ajouté à cette valeur
100 /// pour obtenir la longueur totale de la trame.
101 ///
102 /// **Longueur totale** = `buffer[index] as usize + offset`
103 ///
104 /// # Exemple
105 ///
106 /// Pour un protocole dont l'octet à l'index 2 indique la taille du payload,
107 /// et où la trame totale inclut 3 octets de structure supplémentaires
108 /// (en-tête + octet de longueur + CRC) :
109 ///
110 /// ```rust
111 /// # use uart_proto_detector::PacketLengthStrategy;
112 /// let strategy = PacketLengthStrategy::LengthByte { index: 2, offset: 3 };
113 /// ```
114 LengthByte {
115 /// Position de l'octet de longueur dans le buffer (base 0).
116 index: usize,
117 /// Valeur ajoutée à l'octet de longueur pour obtenir la longueur totale.
118 offset: usize,
119 },
120}
121
122// ─── Configuration ───────────────────────────────────────────────────────────
123
124/// Configuration complète d'un protocole UART.
125///
126/// Cette structure est `Copy` afin de pouvoir être stockée statiquement ou
127/// dupliquée sans coût mémoire significatif.
128///
129/// # Exemple
130///
131/// ```rust
132/// use uart_proto_detector::{ParserConfig, PacketLengthStrategy};
133///
134/// const MY_PROTOCOL: ParserConfig = ParserConfig {
135/// header: &[0xFF, 0xAA],
136/// length_strategy: PacketLengthStrategy::LengthByte { index: 2, offset: 4 },
137/// };
138/// ```
139#[derive(Clone, Copy, Debug)]
140pub struct ParserConfig {
141 /// Séquence d'octets d'en-tête identifiant le début d'une trame valide.
142 ///
143 /// Tous les octets reçus avant que l'en-tête complet soit reconnu sont
144 /// ignorés.
145 pub header: &'static [u8],
146
147 /// Méthode utilisée pour déterminer la longueur totale de la trame.
148 pub length_strategy: PacketLengthStrategy,
149}
150
151// ─── Parser ───────────────────────────────────────────────────────────────────
152
153/// Parser générique pour protocoles UART.
154///
155/// Accumule les octets reçus, valide l'en-tête et retourne une référence vers
156/// la trame complète dès qu'elle est disponible.
157///
158/// **Note :** La vérification du CRC n'est pas effectuée par ce parser. Elle
159/// doit être réalisée par le code appelant après réception d'une trame complète,
160/// car chaque protocole définit son propre algorithme.
161///
162/// # Exemple
163///
164/// ```rust
165/// use uart_proto_detector::{GenericParser, ParserConfig, PacketLengthStrategy};
166///
167/// let config = ParserConfig {
168/// header: &[0xAA, 0x55],
169/// length_strategy: PacketLengthStrategy::Fixed(6),
170/// };
171/// let mut parser = GenericParser::new(config);
172///
173/// // Simuler un flux d'octets
174/// let bytes: &[u8] = &[0xAA, 0x55, 0x01, 0x02, 0x03, 0x04];
175/// for &b in bytes {
176/// if let Ok(Some(packet)) = parser.parse_byte(b) {
177/// assert_eq!(packet.len(), 6);
178/// }
179/// }
180/// ```
181pub struct GenericParser {
182 config: ParserConfig,
183 buffer: [u8; BUFFER_SIZE],
184 bytes_read: usize,
185 expected_length: Option<usize>,
186}
187
188impl GenericParser {
189 /// Crée un nouveau parser à partir d'une [`ParserConfig`].
190 #[inline]
191 pub fn new(config: ParserConfig) -> Self {
192 Self {
193 config,
194 buffer: [0u8; BUFFER_SIZE],
195 bytes_read: 0,
196 expected_length: None,
197 }
198 }
199
200 /// Réinitialise l'état interne du parser.
201 ///
202 /// Appelé automatiquement après chaque trame complète ou en cas d'erreur.
203 /// Peut aussi être appelé manuellement en cas de timeout ou de flush du bus.
204 #[inline]
205 pub fn reset(&mut self) {
206 self.bytes_read = 0;
207 self.expected_length = None;
208 }
209
210 /// Soumet un octet au parser.
211 ///
212 /// # Valeur de retour
213 ///
214 /// | Résultat | Signification |
215 /// |----------------|------------------------------------------------------------|
216 /// | `Ok(None)` | Trame en cours d'accumulation, aucune action requise. |
217 /// | `Ok(Some(…))` | Trame complète disponible dans la slice retournée. |
218 /// | `Err(…)` | Erreur de parsing ; le parser a été réinitialisé. |
219 ///
220 /// La slice retournée par `Ok(Some(…))` pointe sur le buffer interne et
221 /// n'est valide que jusqu'au prochain appel à [`parse_byte`](Self::parse_byte)
222 /// ou [`reset`](Self::reset).
223 ///
224 /// # Erreurs
225 ///
226 /// Retourne [`ParserError::BufferOverflow`] si le buffer est plein avant
227 /// qu'une trame valide ne soit reçue, ou [`ParserError::PacketTooLarge`] si
228 /// la longueur calculée dépasse [`BUFFER_SIZE`].
229 pub fn parse_byte(&mut self, byte: u8) -> Result<Option<&[u8]>, ParserError> {
230 // Sécurité anti-débordement : ne devrait pas se produire en usage normal
231 if self.bytes_read >= BUFFER_SIZE {
232 self.reset();
233 return Err(ParserError::BufferOverflow);
234 }
235
236 self.buffer[self.bytes_read] = byte;
237 self.bytes_read += 1;
238
239 // ── 1. Validation de l'en-tête ────────────────────────────────────────
240 let header_len = self.config.header.len();
241 if self.bytes_read <= header_len {
242 let pos = self.bytes_read - 1;
243 if self.buffer[pos] != self.config.header[pos] {
244 // Mauvais en-tête : réinitialisation et attente du prochain octet
245 self.reset();
246 }
247 return Ok(None);
248 }
249
250 // ── 2. Détermination de la longueur attendue ──────────────────────────
251 if self.expected_length.is_none() {
252 match self.config.length_strategy {
253 PacketLengthStrategy::Fixed(len) => {
254 if len > BUFFER_SIZE {
255 self.reset();
256 return Err(ParserError::PacketTooLarge(len));
257 }
258 self.expected_length = Some(len);
259 }
260
261 PacketLengthStrategy::LengthByte { index, offset } => {
262 if index >= BUFFER_SIZE {
263 self.reset();
264 return Err(ParserError::InvalidLengthByteIndex);
265 }
266 if self.bytes_read > index {
267 let total = self.buffer[index] as usize + offset;
268 if total > BUFFER_SIZE {
269 self.reset();
270 return Err(ParserError::PacketTooLarge(total));
271 }
272 self.expected_length = Some(total);
273 }
274 }
275 }
276 }
277
278 // ── 3. Vérification de complétude ─────────────────────────────────────
279 if let Some(target_len) = self.expected_length {
280 if self.bytes_read == target_len {
281 let final_len = self.bytes_read;
282 self.reset();
283 // SAFETY: final_len <= BUFFER_SIZE (garanti par les vérifications ci-dessus)
284 return Ok(Some(&self.buffer[..final_len]));
285 }
286 }
287
288 Ok(None)
289 }
290
291 /// Retourne le nombre d'octets actuellement accumulés dans le buffer.
292 #[inline]
293 pub fn bytes_accumulated(&self) -> usize {
294 self.bytes_read
295 }
296
297 /// Retourne la longueur de trame attendue, si elle a déjà été déterminée.
298 #[inline]
299 pub fn expected_length(&self) -> Option<usize> {
300 self.expected_length
301 }
302}