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}