Aller au contenu

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 :

  1. Stabilité — un véhicule en mission ne peut pas crasher
  2. Performance — économe en CPU/RAM, viable de Mac M1 jusqu'à Pi Zero 2W (et ESP32 plus tard)
  3. 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 :

[lints]
workspace = true

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 > String quand le contenu ne change pas
  • Box<str> > String quand immuable mais owned
  • Vec::with_capacity(n) > Vec::new() quand on connaît la taille
  • bytes::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 :

[profile.release]
lto = "thin"
codegen-units = 1
strip = true

LTO réduit le binaire de 10–30% et gagne 5–15% CPU. À ne PAS désactiver.

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 :

[dependencies]
fleet-mavlink = { path = "../fleet-mavlink", default-features = false }

Pas de refactor à faire à ce moment-là, juste activer.

5. Async / Tokio

  • #[tokio::main] uniquement dans le main.rs
  • spawn_blocking pour 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, retry
  • error! : échec d'opération qui survit
  • debug! : inspectable avec RUST_LOG=fleet_base=debug

Pas de println! / eprintln! hors d'un binaire de test.

7. Documentation

  • /// sur tout item pub dans les libs
  • //! au top de chaque module pour l'overview
  • cargo doc --workspace --no-deps doit compiler sans warning
  • Liens internes : [ItemName] → résolution Rustdoc auto

8. Tests

  • Tests unitaires dans #[cfg(test)] mod tests au 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.

10. CI minimal (à mettre en place)

# .github/workflows/ci.yml — squelette
- cargo fmt --check
- cargo clippy --workspace --all-targets -- -D warnings
- cargo test --workspace
- cargo build --release --workspace
- cargo build --release -p fleet-companion --target aarch64-unknown-linux-gnu  # cross-compile sanity