Skip to main content

wrapper_ble_esp32c3mini/
lib.rs

1//! # wrapper-ble-esp32c3mini
2//!
3//! Bibliothèque `no_std` encapsulant l'initialisation d'un périphérique BLE
4//! (rôle *GATT peripheral*) sur ESP32-C3, en s'appuyant sur :
5//!
6//! - [`esp-radio`](https://docs.rs/esp-radio) pour le contrôleur HCI matériel
7//!   ([`BleConnector`]),
8//! - [`bt-hci`](https://docs.rs/bt-hci) et son [`ExternalController`] pour
9//!   exposer ce contrôleur au format attendu par la pile hôte,
10//! - [`trouble-host`](https://docs.rs/trouble-host) pour la pile hôte BLE
11//!   (GAP, GATT, L2CAP) et l'exécution asynchrone via Embassy.
12//!
13//! ## Vue d'ensemble
14//!
15//! Le point d'entrée est [`BleSystem::init`], qui construit :
16//! 1. le contrôleur BLE matériel ([`BleController`]),
17//! 2. la pile hôte `trouble-host` configurée en rôle périphérique,
18//! 3. un serveur GATT [`BleServer`] exposant le service [`DisplayService`].
19//!
20//! Le `[`Runner`](trouble_host::Runner)` renvoyé doit ensuite être piloté en
21//! continu par la tâche [`run_ble_runner`], typiquement *spawnée* sur
22//! l'exécuteur Embassy du binaire final.
23//!
24//! ## Exemple d'utilisation (squelette)
25//!
26//! ```ignore
27//! #![no_std]
28//! #![no_main]
29//!
30//! use wrapper_ble_esp32c3mini::{BleSystem, run_ble_runner};
31//!
32//! #[esp_hal_embassy::main]
33//! async fn main(spawner: embassy_executor::Spawner) {
34//!     let peripherals = esp_hal::init(esp_hal::Config::default());
35//!
36//!     let BleSystem { runner, peripheral, server } = BleSystem::init(peripherals.BT);
37//!
38//!     spawner.spawn(ble_runner_task(runner)).unwrap();
39//!
40//!     // `peripheral` sert ensuite à publicité + acceptation de connexions,
41//!     // `server` expose les caractéristiques `command` / `status`.
42//! }
43//!
44//! #[embassy_executor::task]
45//! async fn ble_runner_task(runner: trouble_host::Runner<'static, wrapper_ble_esp32c3mini::BleController, trouble_host::DefaultPacketPool>) {
46//!     wrapper_ble_esp32c3mini::run_ble_runner(runner).await;
47//! }
48//! ```
49//!
50//! ## Compatibilité des versions
51//!
52//! Les versions figées dans `Cargo.toml` ne sont pas arbitraires : elles
53//! forment le seul jeu de versions récentes qui se lient toutes ensemble au
54//! moment de la rédaction de ce crate.
55//!
56//! - `esp-radio = "0.18.0"` exige `esp-hal` dans la plage `~1.1.0-rc.0`
57//!   (donc `esp-hal = "1.1.2"`, **pas** la branche `1.2.x`) et `bt-hci`
58//!   dans la plage `^0.8.0`.
59//! - `bt-hci = "0.8.1"` est donc la version retenue, ce qui impose à son
60//!   tour `trouble-host = "0.6.0"` (la seule branche de `trouble-host` qui
61//!   dépend elle-même de `bt-hci ^0.8`, les versions `0.7`/`0.8` étant
62//!   passées à `bt-hci ^0.9`/`^0.10`).
63//!
64//! Si, à l'avenir, `esp-radio` publie une version qui suit `esp-hal 1.2.x`
65//! et un `bt-hci` plus récent, il faudra remonter `trouble-host` en même
66//! temps que `bt-hci`, pas séparément.
67
68#![no_std]
69
70use bt_hci::controller::ExternalController;
71use embassy_time::{Duration, Timer};
72use esp_radio::ble::controller::BleConnector;
73use static_cell::StaticCell;
74use trouble_host::prelude::*;
75
76/// Taille (en octets) du buffer HCI utilisé par le contrôleur externe.
77///
78/// `20` correspond à la taille de payload HCI ACL par défaut utilisée dans
79/// les exemples `esp-radio` / `trouble-host`. Augmenter cette valeur permet
80/// de négocier un MTU L2CAP plus grand, au prix de plus de RAM statique.
81pub type BleController = ExternalController<BleConnector<'static>, 20>;
82
83/// Serveur GATT exposé par ce périphérique BLE.
84///
85/// Contient un unique service, [`DisplayService`], monté à la construction
86/// via [`BleServer::new_with_config`].
87#[gatt_server]
88pub struct BleServer {
89    /// Service GATT exposant les caractéristiques `command` et `status`.
90    pub display: DisplayService,
91}
92
93/// Service GATT « Display », de type Nordic UART Service (UUID `6e400001…`).
94///
95/// - `command` : caractéristique en écriture, utilisée par le client BLE
96///   pour envoyer des ordres à l'appareil (jusqu'à 16 octets).
97/// - `status`  : caractéristique en lecture + notification, utilisée par
98///   l'appareil pour signaler son état au client (jusqu'à 16 octets).
99#[gatt_service(uuid = "6e400001-b5a3-f393-e0a9-e50e24dcca9e")]
100pub struct DisplayService {
101    /// Commande envoyée par le client (écriture avec ou sans réponse).
102    #[characteristic(uuid = "6e400002-b5a3-f393-e0a9-e50e24dcca9e", write, write_without_response)]
103    pub command: heapless::Vec<u8, 16>,
104
105    /// État courant de l'appareil, lisible et notifiable.
106    #[characteristic(uuid = "6e400003-b5a3-f393-e0a9-e50e24dcca9e", read, notify)]
107    pub status: heapless::Vec<u8, 16>,
108}
109
110/// Regroupe les trois composants nécessaires au fonctionnement de la pile
111/// BLE : le *runner* de la pile hôte, le rôle périphérique et le serveur
112/// GATT applicatif.
113///
114/// Ces trois éléments sont volontairement scindés (plutôt que gardés dans
115/// une seule struct opaque) car `trouble-host` attend qu'ils soient
116/// utilisés indépendamment : le `runner` est piloté en tâche de fond
117/// ([`run_ble_runner`]), tandis que `peripheral` sert à publier des
118/// annonces BLE et `server` à répondre aux lectures/écritures GATT.
119pub struct BleSystem {
120    /// Boucle d'évènements de la pile hôte BLE ; doit tourner en continu.
121    pub runner: Runner<'static, BleController, DefaultPacketPool>,
122    /// Rôle périphérique : publicité BLE et acceptation de connexions.
123    pub peripheral: Peripheral<'static, BleController, DefaultPacketPool>,
124    /// Serveur GATT applicatif (voir [`BleServer`]).
125    ///
126    /// `#[gatt_server]` génère un type portant un paramètre de durée de vie
127    /// (`BleServer<'values>`), hérité de la chaîne de caractères passée à
128    /// `PeripheralConfig::name`. Comme on lui passe toujours un `&'static
129    /// str` (le nom de l'appareil est un littéral), cette durée de vie vaut
130    /// systématiquement `'static` ici.
131    pub server: BleServer<'static>,
132}
133
134impl BleSystem {
135    /// Initialise le contrôleur BLE matériel, la pile hôte `trouble-host`
136    /// et le serveur GATT, et retourne les trois composants prêts à
137    /// l'emploi dans un [`BleSystem`].
138    ///
139    /// # Mémoire statique
140    ///
141    /// `trouble-host` exige que ses ressources internes ([`HostResources`])
142    /// ainsi que la [`Stack`] elle-même vivent aussi longtemps que le
143    /// `runner`/`peripheral` retournés (borne `'static`). Comme ce ne sont
144    /// pas des variables globales au sens Rust, elles sont promues en
145    /// `'static` via [`StaticCell`], le motif standard dans l'écosystème
146    /// Embassy pour ce cas de figure.
147    ///
148    /// # Panics
149    ///
150    /// Cette fonction panique si :
151    /// - l'initialisation du [`BleConnector`] échoue (matériel radio
152    ///   indisponible ou mal configuré),
153    /// - la création du serveur GATT échoue (par exemple si le nom du
154    ///   périphérique dépasse la longueur maximale autorisée par le GAP,
155    ///   22 octets).
156    ///
157    /// Ces deux échecs sont considérés comme irrécupérables à ce stade de
158    /// l'initialisation et ne peuvent pas être corrigés à l'exécution ;
159    /// c'est pourquoi ils sont remontés par `panic!` plutôt que par un
160    /// `Result`. Si tu préfères une gestion d'erreur récupérable, remplace
161    /// les `.expect(...)` par une propagation via `Result`.
162    pub fn init(bt_peripheral: esp_hal::peripherals::BT<'static>) -> Self {
163        let transport = BleConnector::new(bt_peripheral, esp_radio::ble::Config::default())
164            .expect("Erreur init BleConnector");
165        let controller: BleController = ExternalController::new(transport);
166
167        // Ressources internes de la pile hôte (files d'attente L2CAP, etc.)
168        // Générique <PacketPool, CONNS, CHANNELS> : 1 connexion simultanée,
169        // 1 canal L2CAP. Augmente ces constantes si tu as besoin de gérer
170        // plusieurs connexions ou canaux en parallèle.
171        static RESOURCES: StaticCell<HostResources<DefaultPacketPool, 1, 1>> = StaticCell::new();
172        let resources = RESOURCES.init(HostResources::new());
173
174        // La `Stack` doit elle aussi être promue `'static` : `build()`
175        // attend `&'stack self` avec `'stack` égal au paramètre de durée de
176        // vie de la `Stack`, lui-même hérité de `resources`.
177        static STACK: StaticCell<Stack<'static, BleController, DefaultPacketPool>> =
178            StaticCell::new();
179        let stack = STACK.init(
180            trouble_host::new(controller, resources)
181                // Adresse aléatoire statique (bit de poids fort à 1) ; à
182                // remplacer par une adresse propre à chaque appareil en
183                // production (ex. dérivée de l'identifiant matériel unique).
184                .set_random_address(Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff])),
185        );
186
187        let Host {
188            peripheral,
189            runner,
190            ..
191        } = stack.build();
192
193        let server = BleServer::new_with_config(GapConfig::Peripheral(PeripheralConfig {
194            name: "ESP32C3-Mini",
195            appearance: &appearance::UNKNOWN,
196        }))
197        .expect("Erreur création serveur GATT");
198
199        Self {
200            runner,
201            peripheral,
202            server,
203        }
204    }
205}
206
207/// Fait tourner indéfiniment la boucle d'évènements de la pile hôte BLE.
208///
209/// À *spawner* sur l'exécuteur Embassy du binaire final (voir l'exemple du
210/// module). En cas d'erreur du runner (déconnexion inattendue du
211/// contrôleur, par exemple), l'erreur est journalisée via la façade
212/// [`log`] et la boucle retente après une seconde plutôt que de paniquer,
213/// afin de ne pas interrompre le reste de l'application.
214///
215/// Le binaire final doit initialiser un backend `log` (par exemple
216/// `esp-println` avec sa feature `log-04`, ou `defmt`) pour que ces
217/// messages soient réellement visibles ; sans backend initialisé, ils sont
218/// silencieusement ignorés plutôt que de provoquer une erreur.
219pub async fn run_ble_runner(mut runner: Runner<'_, BleController, DefaultPacketPool>) {
220    loop {
221        if let Err(e) = runner.run().await {
222            log::error!("Erreur du runner Bluetooth : {:?}", e);
223            Timer::after(Duration::from_secs(1)).await;
224        }
225    }
226}