La lib de télémétrie (dans src/software/telemetry/) est une crate Rust qui comporte 2 éléments :
-
La lib en elle-même (point d’entrée
src/lib.rs
) -
Un outil CLI (point d’entrée
src/bin.rs
)
L’outil CLI permet actuellement :
-
de récupérer, de parser et d’afficher le protocole de télémétrie qui passe sur un port série donné : debug
-
de récupérer, de parser et d’afficher le protocole de télémétrie qui a été enregistré dans un fichier donné : play
-
de récupérer les octets qui passent sur un port série donné et de les enregistrer dans un fichier donné : record
Tip
|
Le setup le plus simple est de se placer dans src/software/telemetry/ avec un terminal et avec VS Code (de manière à avoir le support de RLS).
|
Pour lancer l’outil, le plus pratique est d’appeler scripts/run.sh :
./scripts/run.sh --help
Pour lancer rustfmt, Clippy (le linter Rust) et les tests du même coup, il faut la commande suivante :
cargo fmt && cargo clippy --all-features && cargo test
Je recommande de le faire très régulièrement, voire avant chaque essai.
On veut ajouter un 4e mode au programme en CLI : stats.
Ce mode prend le path d’un fichier en entrée (comme play) mais au lieu de le rejouer et d’afficher les messages au fur et à mesure dans stdout (avec des temporisations pour faire réaliste), on va agréger les différents messages pour récupérer le nombre d’occurences de chaque message dans le fichier.
On utilise clap (version 3, pas encore sortie) pour parser les arguments passés au lancement de l’exécutable et gérer l’affichage de leur documentation (quand on le lance avec -h
par exemple).
Il faut donc modifier ça pour gérer notre nouveau mode :
-
ajouter une struct
Stats
basée surPlay
-
modifier le enum
Mode
pour ajouter cette struct
Tip
|
À ce stade là, lancer Clippy ou une compilation devrait indiquer les endroits où on n’exploite pas le enum de manière complète. |
Ensuite, on va pouvoir :
-
ajouter une nouvelle fonction
stats(cfg: Stats)
-
mettre un
println!("whatever")
dedans -
vérifier qu’on arrive bien à afficher ce message lorsqu’on appelle l’exécutable avec les bonnes options (on a besoin que ça plante avec un message d’erreur si on n’a pas fourni le path du fichier)
Warning
|
Il y aura probablement un warning de compilation qui dit qu’on ne fait rien du paramètre cfg mais on va l’ignorer pour l’instant.
|
On va faire une 1ère implémentation sans utiliser/modifier la lib, en gérant tout dans la fonction stats
(pas grave si ça fait de la duplication de code avec le reste de la lib ou du programme).
C’est parti :
-
il faut ouvrir le fichier en utilisant la méthode
open
de File (un objet de typeFile
= un handle pour manipuler le fichier) -
on crée un BufRead du
File
(pas obligatoire mais ça permet, grâce à un buffer, de décorréler les I/O réelles du traitement qu’on va faire des données issues du fichier) -
on utilise la méthode
lines
duBufRead
pour obtenir une chaîne de caractères d’une ligne du fichier ou une erreur ; exemple d’utilisation ici (on boucle sur les lignes ; si on a autre chose d’unOk(String)
, alors on ne fait rien et probablement que la boucle s’arrêtera là vu que c’est la fin du fichier)
Tip
|
En faisant un println!("{}", &la_chaine_de_la_ligne_en_cours) dans la boucle, on devrait voir apparaitre chaque ligne du fichier dans stdout.
|
Caution
|
Je conseille de tester avec un record qui est petit pour réduire le spam ^^ |
Maintenant qu’on a chaque ligne, on peut les traiter :
-
on a déjà la crate base64 dans le projet (voir Cargo.toml)
-
pour chaque ligne, on va utiliser la méthode decode pour obtenir la liste d’octets (
Vec<u8>
) correspondants ;decode
renvoie unResult<Vec<u8>, DecodeError>
car bien sûr ça peut échouer (si on lui donne une chaîne qui n’est pas du base64), mais on va appeler.unwrap()
sur ceResult
pour extraire le résultat (ça implique que si c’est une erreur, le programme va panic, mais c’est OK ici ; c’est facile à observer si on passe un fichier texte quelconque au programme) -
on peut ensuite vérifier que ça marche en affichant les octets de chaque ligne avec
println!("{:?}", &le_vec_des_octets)
(le{:?}
dans le pattern indique qu’on veut l’affichage en mode debug duVec<u8>
; avec{}
on serait obligé de lui expliquer comment transformer leVec<u8>
enString
) -
enfin, on va agréger chaque ligne dans un seul vecteur : on le crée en dehors de la boucle avec
let mut buffer = Vec::new()
et à chaque itération, on peut ajouter les octets de la ligne avecbuffer.append(&mut le_vec_des_octets)
Tip
|
Pour tester sans spammer stdout, on peut retirer les println!() qui servaient au debug jusque là et en ajouter un après la boucle qui affiche le nombre d’éléments (d’octets ici) de notre buffer (voir Vec.len()). Normalement ça doit être plus petit que le poids du fichier lu car le base64 implique un overhead.
|
Ensuite, on va pouvoir analyser ces octets. La démarche est la suivante :
-
on donne une référence à notre buffer d’octets en appelant
crate::parsers::parse_telemetry_message
-
il y a 3 résultats possibles :
-
le parser a réussi à trouver un message au début du buffer : on obtient un
Ok
qui contient un tuple(reste_du_buffer, message)
; on va dire que le buffer est maintenant égal àreste_du_buffer
(= on retire les octets qui ont été parsés) et on peut faire ce qu’on veut avec le message -
le parser renvoie une erreur de type
Err(nom::Err::Incomplete(_))
: ça partait bien mais il manque des octets pour avoir un message complet ; ici pas besoin de traiter ce cas vu qu’on a récupéré tous les octets dans notre buffer, on n’aura rien de plus -
le parser échoue et renvoie un
Err(e)
: la stratégie ici est de retirer uniquement le 1er octet du buffer (buffer.remove(0)
) et de ré-essayer
-
-
avec ce fonctionnement, le buffer va se vider progressivement (on retire les octets parsés et les octets en erreur) ; on peut donc boucler jusqu’à ce qu’il soit complètement consommé (
while !buffer.is_empty() { … }
) -
on va pouvoir également créer un
Vec<TelemetryMessage>
avant cette boucle, et le remplir avecmon_vec.push(un_message_de_telemetry)
à chaque fois qu’on parse un message ; après la boucle, ce vecteur contiendra tous les messages qui ont été trouvés
Enfin, on va pouvoir calculer nos stats :
-
on crée une variable (initialisée à 0) pour chaque type de message
-
on boucle sur le vecteur de messages (
for message in vecteur { … }
), et grâce à un bloc de pattern matching, on va déterminer pour chaque message quelle variable incrémenter -
après la boucle, on affiche les comptes !
Lorsqu’on rejoue les messages (avec le mode play), on va faire artificiellement une pause après chaque message restitué :
-
après un
StoppedMessage
, on attend 100 ms -
après un
DataSnapshot
, on attend 10 ms -
après n’importe quel autre message, on attend 0 ms
On va donc créer une fonction compute_duration
qui prend en paramètre le Vec<TelemetryMessage>
et va renvoyer le nombre de ms nécessaires à la lecture de ce fichier.
C’est l’occasion d’écrire quelques tests unitaires pour vérifier que compute_duration
fait son job :
-
si on lui donne un vecteur vide (
vec![]
), elle renvoie0
-
si on lui donne un vecteur qui contient quelques messages, elle renvoie le résultat attendu
Ensuite, on va appeler cette fonction à la fin de stats()
pour afficher le résultats (en secondes).
Normalement à ce stade tout fonctionne bien mais il y a quelques limitations :
-
on est obligé de monter tout le fichier en mémoire
-
on a déjà une fonction
gather_telemetry_from_file
dans la lib et donc on a dupliqué du code
On va donc refactorer stats()
pour éviter ça !
La bonne nouvelle c’est que gather_telemetry_from_file
ne monte pas le fichier complet en mémoire.
Pour l’utiliser, le plus simple est d"étudier la fonction play :
-
on crée le handle du fichier à lire
-
on crée une paire de Sender/Receiver ; c’est un mécanisme qui permet la communication entre plusieurs threads (plusieurs producers, un seul consummer)
-
on crée un thread dans lequel on appelle
gather_telemetry_from_file
; on lui passe leFile
et leSender
qui sont alors moved dans le thread : impossible par la suite d’utiliser ces variables, on les a données au thread -
on boucle à l’infini et à chaque itération on va essayer de récupérer un message dans le channel grâce au
Receiver
:-
s’il y a un message, c’est cool on peut l’utiliser
-
s’il n’y a rien, on attend un peu et on reboucle (si on n’attend pas ça marche mais consomme du CPU pour rien)
-
si le channel est cassé, on stoppe le programme ; cause : le thread a terminé son exécution (le fichier a été lu ou alors le thread a panic)
-
Avec ceci en tête, on peut maintenant faire un mélange entre le code de play()
et le code qu’on a écrit pour les stats pour avoir le même résultat qu’avant mais en utilisant un thread et gather_telemetry_from_file
.