Skip to main content

peat_btle/
transport.rs

1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Transport trait implementation for PEAT-BTLE
17//!
18//! Implements the pluggable transport abstraction (ADR-032) for Bluetooth LE,
19//! providing the `BluetoothLETransport` struct that can be registered with
20//! the `TransportManager`.
21
22#[cfg(not(feature = "std"))]
23use alloc::{boxed::Box, vec::Vec};
24
25use async_trait::async_trait;
26use core::time::Duration;
27
28use crate::config::{BleConfig, BlePhy};
29use crate::error::Result;
30use crate::platform::BleAdapter;
31use crate::NodeId;
32
33/// Transport capabilities for Bluetooth LE
34///
35/// Advertises what this transport can do, allowing the TransportManager
36/// to select the best transport for each message.
37#[derive(Debug, Clone)]
38pub struct TransportCapabilities {
39    /// Maximum bandwidth in bytes/second
40    pub max_bandwidth_bps: u64,
41    /// Typical latency in milliseconds
42    pub typical_latency_ms: u32,
43    /// Maximum practical range in meters
44    pub max_range_meters: u32,
45    /// Supports bidirectional communication
46    pub bidirectional: bool,
47    /// Supports reliable delivery
48    pub reliable: bool,
49    /// Battery impact score (0-100, higher = more power)
50    pub battery_impact: u8,
51    /// Supports broadcast/advertising
52    pub supports_broadcast: bool,
53    /// Requires pairing before use
54    pub requires_pairing: bool,
55    /// Maximum message size in bytes
56    pub max_message_size: usize,
57}
58
59impl TransportCapabilities {
60    /// Create default BLE capabilities
61    pub fn bluetooth_le() -> Self {
62        Self {
63            max_bandwidth_bps: 250_000, // ~250 KB/s practical throughput
64            typical_latency_ms: 30,
65            max_range_meters: 100,
66            bidirectional: true,
67            reliable: true,
68            battery_impact: 15,
69            supports_broadcast: true,
70            requires_pairing: false,
71            max_message_size: 512,
72        }
73    }
74
75    /// Create capabilities for Coded PHY (long range)
76    pub fn bluetooth_le_coded() -> Self {
77        Self {
78            max_bandwidth_bps: 125_000, // Coded S=8
79            typical_latency_ms: 100,
80            max_range_meters: 400,
81            bidirectional: true,
82            reliable: true,
83            battery_impact: 20, // Slightly higher due to longer TX time
84            supports_broadcast: true,
85            requires_pairing: false,
86            max_message_size: 512,
87        }
88    }
89
90    /// Update capabilities based on PHY
91    pub fn for_phy(phy: BlePhy) -> Self {
92        match phy {
93            BlePhy::Le1M => Self::bluetooth_le(),
94            BlePhy::Le2M => Self {
95                max_bandwidth_bps: 500_000,
96                typical_latency_ms: 20,
97                max_range_meters: 50,
98                ..Self::bluetooth_le()
99            },
100            BlePhy::LeCodedS2 => Self {
101                max_bandwidth_bps: 250_000,
102                typical_latency_ms: 50,
103                max_range_meters: 200,
104                ..Self::bluetooth_le()
105            },
106            BlePhy::LeCodedS8 => Self::bluetooth_le_coded(),
107        }
108    }
109}
110
111impl Default for TransportCapabilities {
112    fn default() -> Self {
113        Self::bluetooth_le()
114    }
115}
116
117/// Connection to a BLE peer
118///
119/// Represents an active GATT connection to a remote device.
120pub trait BleConnection: Send + Sync {
121    /// Get the remote peer's node ID
122    fn peer_id(&self) -> &NodeId;
123
124    /// Check if connection is still alive
125    fn is_alive(&self) -> bool;
126
127    /// Get the negotiated MTU
128    fn mtu(&self) -> u16;
129
130    /// Get the current PHY
131    fn phy(&self) -> BlePhy;
132
133    /// Get RSSI (signal strength) in dBm
134    fn rssi(&self) -> Option<i8>;
135
136    /// Get connection duration
137    fn connected_duration(&self) -> Duration;
138}
139
140/// Bluetooth LE mesh transport
141///
142/// Implements the transport abstraction for BLE, providing:
143/// - Peer discovery via advertising/scanning
144/// - GATT-based data exchange
145/// - Connection management
146/// - PHY selection
147///
148/// # Example
149///
150/// ```ignore
151/// use peat_btle::{BluetoothLETransport, BleConfig, NodeId};
152///
153/// let config = BleConfig::peat_lite(NodeId::new(0x12345678));
154/// let transport = BluetoothLETransport::new(config)?;
155///
156/// transport.start().await?;
157/// let conn = transport.connect(&peer_id).await?;
158/// ```
159pub struct BluetoothLETransport<A: BleAdapter> {
160    /// Configuration
161    config: BleConfig,
162    /// Platform-specific adapter
163    adapter: A,
164    /// Current capabilities (may change with PHY)
165    capabilities: TransportCapabilities,
166}
167
168impl<A: BleAdapter> BluetoothLETransport<A> {
169    /// Create a new BLE transport with the given adapter
170    pub fn new(config: BleConfig, adapter: A) -> Self {
171        let capabilities = TransportCapabilities::for_phy(config.phy.preferred_phy);
172        Self {
173            config,
174            adapter,
175            capabilities,
176        }
177    }
178
179    /// Get the current configuration
180    pub fn config(&self) -> &BleConfig {
181        &self.config
182    }
183
184    /// Get the current capabilities
185    pub fn capabilities(&self) -> &TransportCapabilities {
186        &self.capabilities
187    }
188
189    /// Get the node ID
190    pub fn node_id(&self) -> &NodeId {
191        &self.config.node_id
192    }
193
194    /// Per-peer link info for ADR-032 §Amendment A consumers.
195    ///
196    /// Delegates to the underlying platform adapter. The default
197    /// `BleAdapter::peer_link_info` returns `None`; adapters that
198    /// track per-peer state surface it through this pass-through,
199    /// where peat-mesh's `PeatBleTransport::peer_link_state` reads it
200    /// and synthesises a unified `LinkState`.
201    pub fn peer_link_info(&self, peer_id: &NodeId) -> Option<crate::peer::BlePeerLinkInfo> {
202        self.adapter.peer_link_info(peer_id)
203    }
204}
205
206/// Async transport operations
207///
208/// These are the core transport operations that integrate with
209/// the Peat protocol's transport abstraction (ADR-032).
210#[async_trait]
211pub trait MeshTransport: Send + Sync {
212    /// Start the transport layer
213    async fn start(&self) -> Result<()>;
214
215    /// Stop the transport layer
216    async fn stop(&self) -> Result<()>;
217
218    /// Connect to a peer by node ID
219    async fn connect(&self, peer_id: &NodeId) -> Result<Box<dyn BleConnection>>;
220
221    /// Disconnect from a peer
222    async fn disconnect(&self, peer_id: &NodeId) -> Result<()>;
223
224    /// Get an existing connection
225    fn get_connection(&self, peer_id: &NodeId) -> Option<Box<dyn BleConnection>>;
226
227    /// Get the number of connected peers
228    fn peer_count(&self) -> usize;
229
230    /// Get list of connected peer IDs
231    fn connected_peers(&self) -> Vec<NodeId>;
232
233    /// Check if connected to a specific peer
234    fn is_connected(&self, peer_id: &NodeId) -> bool {
235        self.get_connection(peer_id).is_some()
236    }
237
238    /// Send data to a connected peer
239    ///
240    /// Fragments the payload based on the connection's negotiated MTU
241    /// and writes each fragment to the peer's sync data characteristic.
242    ///
243    /// Returns the number of application bytes sent (original payload size).
244    async fn send_to(&self, peer_id: &NodeId, data: &[u8]) -> Result<usize> {
245        let _ = (peer_id, data);
246        Err(crate::error::BleError::NotSupported(
247            "send_to not implemented".into(),
248        ))
249    }
250
251    /// Get transport capabilities
252    fn capabilities(&self) -> &TransportCapabilities;
253}
254
255/// Construct a full 128-bit UUID from a BLE 16-bit short UUID
256///
257/// Uses the Bluetooth Base UUID: `0000xxxx-0000-1000-8000-00805F9B34FB`
258fn ble_uuid_from_u16(short: u16) -> uuid::Uuid {
259    uuid::Uuid::from_fields(
260        short as u32,
261        0x0000,
262        0x1000,
263        &[0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB],
264    )
265}
266
267#[async_trait]
268impl<A: BleAdapter + Send + Sync> MeshTransport for BluetoothLETransport<A> {
269    async fn start(&self) -> Result<()> {
270        // Start advertising and scanning via adapter
271        self.adapter.start().await
272    }
273
274    async fn stop(&self) -> Result<()> {
275        self.adapter.stop().await
276    }
277
278    async fn connect(&self, peer_id: &NodeId) -> Result<Box<dyn BleConnection>> {
279        self.adapter.connect(peer_id).await
280    }
281
282    async fn disconnect(&self, peer_id: &NodeId) -> Result<()> {
283        self.adapter.disconnect(peer_id).await
284    }
285
286    fn get_connection(&self, peer_id: &NodeId) -> Option<Box<dyn BleConnection>> {
287        self.adapter.get_connection(peer_id)
288    }
289
290    fn peer_count(&self) -> usize {
291        self.adapter.peer_count()
292    }
293
294    fn connected_peers(&self) -> Vec<NodeId> {
295        self.adapter.connected_peers()
296    }
297
298    async fn send_to(&self, peer_id: &NodeId, data: &[u8]) -> Result<usize> {
299        use crate::sync::protocol::chunk_data;
300
301        // Get connection for MTU
302        let conn = self.get_connection(peer_id).ok_or_else(|| {
303            crate::error::BleError::ConnectionFailed(format!("No connection to {}", peer_id))
304        })?;
305        let mtu = conn.mtu() as usize;
306
307        // Fragment data into MTU-sized chunks with reassembly headers
308        let chunks = chunk_data(data, mtu, 0);
309
310        // Write each chunk to the sync data characteristic
311        let char_uuid = ble_uuid_from_u16(crate::CHAR_SYNC_DATA_UUID);
312        for chunk in &chunks {
313            self.adapter
314                .write_to_peer(peer_id, char_uuid, &chunk.encode())
315                .await?;
316        }
317
318        Ok(data.len())
319    }
320
321    fn capabilities(&self) -> &TransportCapabilities {
322        &self.capabilities
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_capabilities_for_phy() {
332        let caps = TransportCapabilities::for_phy(BlePhy::LeCodedS8);
333        assert_eq!(caps.max_range_meters, 400);
334        assert_eq!(caps.max_bandwidth_bps, 125_000);
335    }
336
337    #[test]
338    fn test_capabilities_le2m() {
339        let caps = TransportCapabilities::for_phy(BlePhy::Le2M);
340        assert_eq!(caps.max_range_meters, 50);
341        assert_eq!(caps.max_bandwidth_bps, 500_000);
342    }
343
344    #[test]
345    fn test_ble_uuid_from_u16() {
346        let uuid = ble_uuid_from_u16(0x0003);
347        assert_eq!(uuid.to_string(), "00000003-0000-1000-8000-00805f9b34fb");
348    }
349
350    #[test]
351    fn test_send_to_default_returns_error() {
352        // Verify the default trait impl returns NotSupported
353        use crate::platform::StubAdapter;
354
355        let config = BleConfig::default();
356        let adapter = StubAdapter::default();
357        let transport = BluetoothLETransport::new(config, adapter);
358
359        // send_to without a connection should fail
360        let rt = tokio::runtime::Builder::new_current_thread()
361            .enable_all()
362            .build()
363            .unwrap();
364        let result = rt.block_on(transport.send_to(&NodeId::new(0x222), b"hello"));
365        assert!(result.is_err());
366    }
367
368    #[tokio::test]
369    async fn test_mock_send_to() {
370        use crate::platform::mock::{MockBleAdapter, MockNetwork};
371
372        let network = MockNetwork::new();
373        let mut adapter1 = MockBleAdapter::new(NodeId::new(0x111), network.clone());
374        let mut adapter2 = MockBleAdapter::new(NodeId::new(0x222), network.clone());
375
376        adapter1.init(&BleConfig::default()).await.unwrap();
377        adapter2.init(&BleConfig::default()).await.unwrap();
378        adapter2
379            .start_advertising(&crate::config::DiscoveryConfig::default())
380            .await
381            .unwrap();
382
383        // Connect
384        let _conn = adapter1.connect(&NodeId::new(0x222)).await.unwrap();
385
386        // Create transport and send data
387        let transport = BluetoothLETransport::new(BleConfig::default(), adapter1);
388        let result = transport.send_to(&NodeId::new(0x222), b"hello mesh").await;
389        assert!(result.is_ok());
390        assert_eq!(result.unwrap(), 10);
391
392        // Verify data was queued in the network
393        let packets = network.receive_data(&NodeId::new(0x222));
394        assert!(!packets.is_empty());
395    }
396
397    #[tokio::test]
398    async fn test_send_to_disconnected_peer() {
399        use crate::platform::mock::{MockBleAdapter, MockNetwork};
400
401        let network = MockNetwork::new();
402        let adapter = MockBleAdapter::new(NodeId::new(0x111), network);
403        let transport = BluetoothLETransport::new(BleConfig::default(), adapter);
404
405        // Sending to a peer we're not connected to should fail
406        let result = transport.send_to(&NodeId::new(0x999), b"hello").await;
407        assert!(result.is_err());
408    }
409}