Conventions de code — workspace fleet/¶
Ce document fixe les règles d'écriture pour les crates fleet-mavlink, fleet-base, fleet-companion. Trois priorités, dans l'ordre :
- Stabilité — un véhicule en mission ne peut pas crasher
- Performance — économe en CPU/RAM, viable de Mac M1 jusqu'à Pi Zero 2W (et ESP32 plus tard)
- Embedded-readiness — ne pas peindre le code partagé dans un coin std
1. Lints (config centralisée)¶
Cargo.toml workspace :
[workspace.lints.rust]
unsafe_code = "deny"
rust_2018_idioms = "warn"
unused_must_use = "warn"
missing_debug_implementations = "warn"
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
# ... allow list pour les lints noisy (voir Cargo.toml)
Chaque crate importe avec :
CI : cargo clippy --workspace --all-targets -- -D warnings et cargo fmt --check.
2. Stabilité — règles dures¶
Pas de panic en production¶
| Forbidden | Use instead |
|---|---|
x.unwrap() |
x? ou x.context("…")? |
x.expect("…") |
idem |
assert!, panic!, unreachable! |
anyhow::bail! ou Err(…) typé |
[i] indexing sur slices |
.get(i).context("…")? |
Arithmétique débordante (a + b) |
.checked_add(b).context("…")? ou .saturating_add(b) |
Exception : unwrap() est tolérable dans :
- main() au démarrage (config invalide = abort acceptable)
- Tests
- Une expression qui ne peut pas faillir par construction (avec un commentaire)
Erreurs typées dans les libs, anyhow dans les binaires¶
// fleet-mavlink (lib) → thiserror, types stables
#[derive(Debug, thiserror::Error)]
pub enum FleetMavError {
#[error("unsupported vehicle type: {0:?}")]
UnsupportedType(MavType),
}
// fleet-base / fleet-companion (bin) → anyhow + Context
fn open_serial(path: &str) -> anyhow::Result<File> {
File::open(path).with_context(|| format!("opening {path}"))
}
Les libs DOIVENT retourner des types d'erreur explicites. Les binaires peuvent collapser en anyhow::Error.
Timeout sur tout I/O externe¶
// Bon
let img = tokio::time::timeout(
Duration::from_secs(3),
camera::capture(&path, w, h),
).await??;
// Mauvais — un libcamera-jpeg qui hang fait planter le sentinel
let img = camera::capture(&path, w, h).await?;
S'applique : caméra, série, réseau, FS, GPIO blocking.
Graceful shutdown¶
let shutdown = CancellationToken::new();
tokio::select! {
res = work() => { /* terminer */ }
() = shutdown.cancelled() => { /* arrêt propre */ }
}
Toujours select! au plus haut niveau de chaque task, jamais process::exit() hors main finalisé.
Bounded queues + backpressure¶
let (tx, rx) = mpsc::channel::<PathBuf>(16); // borné
// vs
let (tx, rx) = mpsc::unbounded_channel(); // → OOM si l'aval lag, INTERDIT
Restart-on-failure côté OS¶
Sur la Pi : systemd unit avec Restart=always, RestartSec=5s. Le daemon peut crasher (rare avec Rust) ; il repart. Documenter le path de recovery dans le README de chaque crate.
3. Performance — patterns à connaître¶
Locks : choisir selon le pattern¶
| Pattern | Choix | Pourquoi |
|---|---|---|
| Read >> write, état dense | parking_lot::RwLock |
Lock court, pas d'await, plus rapide que tokio::sync::RwLock |
| Read >>> write, snapshot atomique | arc_swap::ArcSwap<T> |
Lecture lock-free, écriture clone-on-write |
| Mut court partagé async | tokio::sync::Mutex SI on doit tenir le lock à travers un await |
|
| Single producer | tokio::sync::watch |
Diffusion d'état (latest-wins) |
| Multi producer / multi consumer | tokio::sync::mpsc ou tokio::sync::broadcast |
Ne pas réinventer |
Règle d'or : ne JAMAIS tenir un parking_lot lock à travers un .await. Toujours libérer le guard avant d'awaiter (le drop happens à la fin de l'expression).
Allocation¶
&str>Stringquand le contenu ne change pasBox<str>>Stringquand immuable mais ownedVec::with_capacity(n)>Vec::new()quand on connaît la taillebytes::BytesMut>Vec<u8>pour buffers réutilisés (ref-counted slicing)- Pas d'allocation dans les hot paths (boucle MAVLink read, dispatch coordinator)
Profile release¶
Déjà configuré dans Cargo.toml workspace :
LTO réduit le binaire de 10–30% et gagne 5–15% CPU. À ne PAS désactiver.
4. Embedded-readiness — discipline pour fleet-mavlink¶
Le crate partagé doit pouvoir être consommé un jour par un module ESP32 no_std. Règles :
| À éviter | Préférer |
|---|---|
std::time::Instant |
Pas de timing dans la lib — c'est runtime, ça vit chez le consumer (LiveVehicle dans fleet-base) |
std::collections::HashMap |
BTreeMap (no_std) ou heapless::FnvIndexMap |
String, Vec |
&str, slices, heapless::String<N>, heapless::Vec<T,N> |
std::println!, std::eprintln! |
tracing::info! etc. (le subscriber décide) |
Le crate déclare #![cfg_attr(not(feature = "std"), no_std)] au top du lib.rs. Aujourd'hui la feature std est activée par défaut, mais le code ne dépend que de core::.
Quand un module ESP32 arrive :
Pas de refactor à faire à ce moment-là, juste activer.
5. Async / Tokio¶
#[tokio::main]uniquement dans lemain.rsspawn_blockingpour bridger les libs sync (mavlink crate)tokio::select!pour multiplexer cancellation + work- Une task par responsabilité — pas de god-loop
JoinHandle::abort()pour kill une task qui hang
6. Logging avec tracing¶
use tracing::{info, warn, debug, error};
info!(vehicle = %name, sysid = state.sysid, mode = state.mode_name(), "heartbeat");
warn!(error = %e, "fallback path");
debug!(?packet, "raw packet"); // {:?}
info!: événements opérationnels (connection, mode change)warn!: dégradation, retryerror!: échec d'opération qui survitdebug!: inspectable avecRUST_LOG=fleet_base=debug
Pas de println! / eprintln! hors d'un binaire de test.
7. Documentation¶
///sur tout itempubdans les libs//!au top de chaque module pour l'overviewcargo doc --workspace --no-depsdoit compiler sans warning- Liens internes :
[ItemName]→ résolution Rustdoc auto
8. Tests¶
- Tests unitaires dans
#[cfg(test)] mod testsau bas du fichier - Tests d'intégration dans
tests/au niveau crate - Smoke tests CLI dans
docs/dev/sitl.md(déjà fait pour le scripted takeoff) - Pas besoin de 100% coverage, viser les invariants : parsing MAVLink, mode mapping, edge cases d'erreur
9. Liste des libs validées¶
| Crate | Usage |
|---|---|
mavlink |
Protocole MAVLink (ardupilotmega) |
tokio |
Async runtime |
tokio-util |
CancellationToken pour shutdown |
tracing + tracing-subscriber |
Logging structuré |
clap |
Parsing CLI |
anyhow |
Errors dans les binaires |
thiserror |
Errors typées dans les libs |
parking_lot |
Locks sync rapides |
arc-swap |
State read-mostly lock-free |
bytes |
Buffers réutilisés |
rppal |
GPIO Pi (Linux uniquement) |
À éviter sauf justification écrite : lazy_static (utiliser std::sync::LazyLock), once_cell (idem), des forks bizarres de crates standards, *-async quand il existe une variante tokio.