hive_btle/phy/
types.rs

1//! BLE PHY Types
2//!
3//! Defines the available Bluetooth Low Energy physical layer configurations
4//! for different range/throughput tradeoffs.
5
6/// BLE Physical Layer (PHY) options
7///
8/// BLE 5.0 introduced multiple PHY options for different use cases:
9/// - LE 1M: Standard 1 Mbps rate, good range
10/// - LE 2M: High speed 2 Mbps, reduced range
11/// - LE Coded: Long range mode with error correction
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13pub enum BlePhy {
14    /// LE 1M PHY - 1 Mbps, ~100m range
15    ///
16    /// The standard BLE PHY, compatible with all BLE devices.
17    /// Good balance of speed and range.
18    #[default]
19    Le1M,
20
21    /// LE 2M PHY - 2 Mbps, ~50m range
22    ///
23    /// Double the data rate but reduced range.
24    /// Use for high-throughput short-range links.
25    Le2M,
26
27    /// LE Coded S=2 PHY - 500 kbps, ~200m range
28    ///
29    /// Coded PHY with 2x redundancy.
30    /// Good balance of range and throughput.
31    LeCodedS2,
32
33    /// LE Coded S=8 PHY - 125 kbps, ~400m range
34    ///
35    /// Coded PHY with 8x redundancy.
36    /// Maximum range but lowest throughput.
37    LeCodedS8,
38}
39
40impl BlePhy {
41    /// Get the data rate in bits per second
42    pub fn data_rate_bps(&self) -> u32 {
43        match self {
44            BlePhy::Le1M => 1_000_000,
45            BlePhy::Le2M => 2_000_000,
46            BlePhy::LeCodedS2 => 500_000,
47            BlePhy::LeCodedS8 => 125_000,
48        }
49    }
50
51    /// Get the data rate in kilobits per second
52    pub fn data_rate_kbps(&self) -> u32 {
53        self.data_rate_bps() / 1000
54    }
55
56    /// Get typical maximum range in meters (line of sight)
57    pub fn typical_range_m(&self) -> u16 {
58        match self {
59            BlePhy::Le1M => 100,
60            BlePhy::Le2M => 50,
61            BlePhy::LeCodedS2 => 200,
62            BlePhy::LeCodedS8 => 400,
63        }
64    }
65
66    /// Get typical latency in milliseconds for a connection event
67    pub fn typical_latency_ms(&self) -> u16 {
68        match self {
69            BlePhy::Le1M => 30,
70            BlePhy::Le2M => 20,
71            BlePhy::LeCodedS2 => 50,
72            BlePhy::LeCodedS8 => 100,
73        }
74    }
75
76    /// Check if this is a coded PHY (long range)
77    pub fn is_coded(&self) -> bool {
78        matches!(self, BlePhy::LeCodedS2 | BlePhy::LeCodedS8)
79    }
80
81    /// Check if this requires BLE 5.0
82    pub fn requires_ble5(&self) -> bool {
83        !matches!(self, BlePhy::Le1M)
84    }
85
86    /// Get the coding scheme (S value) for coded PHYs
87    pub fn coding_scheme(&self) -> Option<u8> {
88        match self {
89            BlePhy::LeCodedS2 => Some(2),
90            BlePhy::LeCodedS8 => Some(8),
91            _ => None,
92        }
93    }
94
95    /// Get PHY name as string
96    pub fn name(&self) -> &'static str {
97        match self {
98            BlePhy::Le1M => "LE 1M",
99            BlePhy::Le2M => "LE 2M",
100            BlePhy::LeCodedS2 => "LE Coded S=2",
101            BlePhy::LeCodedS8 => "LE Coded S=8",
102        }
103    }
104
105    /// Calculate approximate time to transmit data
106    pub fn transmit_time_us(&self, bytes: usize) -> u64 {
107        // Bits to transmit (including overhead)
108        let bits = (bytes + 10) * 8; // +10 for BLE overhead
109        let rate = self.data_rate_bps() as u64;
110        (bits as u64 * 1_000_000) / rate
111    }
112
113    /// Estimate power consumption relative to LE 1M (1.0 = baseline)
114    pub fn relative_power(&self) -> f32 {
115        match self {
116            BlePhy::Le1M => 1.0,
117            BlePhy::Le2M => 0.8,      // Shorter airtime
118            BlePhy::LeCodedS2 => 1.5, // More processing
119            BlePhy::LeCodedS8 => 2.0, // Much more processing
120        }
121    }
122}
123
124impl core::fmt::Display for BlePhy {
125    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126        write!(f, "{}", self.name())
127    }
128}
129
130/// PHY capabilities of a device
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
132pub struct PhyCapabilities {
133    /// Supports LE 2M PHY
134    pub le_2m: bool,
135    /// Supports LE Coded PHY
136    pub le_coded: bool,
137}
138
139impl PhyCapabilities {
140    /// Device supports only LE 1M (BLE 4.x)
141    pub fn le_1m_only() -> Self {
142        Self {
143            le_2m: false,
144            le_coded: false,
145        }
146    }
147
148    /// Device supports all BLE 5.0 PHYs
149    pub fn ble5_full() -> Self {
150        Self {
151            le_2m: true,
152            le_coded: true,
153        }
154    }
155
156    /// Device supports LE 1M and LE 2M only
157    pub fn ble5_no_coded() -> Self {
158        Self {
159            le_2m: true,
160            le_coded: false,
161        }
162    }
163
164    /// Check if a specific PHY is supported
165    pub fn supports(&self, phy: BlePhy) -> bool {
166        match phy {
167            BlePhy::Le1M => true, // Always supported
168            BlePhy::Le2M => self.le_2m,
169            BlePhy::LeCodedS2 | BlePhy::LeCodedS8 => self.le_coded,
170        }
171    }
172
173    /// Get the best supported PHY for range
174    pub fn best_for_range(&self) -> BlePhy {
175        if self.le_coded {
176            BlePhy::LeCodedS8
177        } else {
178            BlePhy::Le1M
179        }
180    }
181
182    /// Get the best supported PHY for throughput
183    pub fn best_for_throughput(&self) -> BlePhy {
184        if self.le_2m {
185            BlePhy::Le2M
186        } else {
187            BlePhy::Le1M
188        }
189    }
190}
191
192/// PHY preference for connection
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct PhyPreference {
195    /// Preferred TX PHY
196    pub tx: BlePhy,
197    /// Preferred RX PHY
198    pub rx: BlePhy,
199}
200
201impl Default for PhyPreference {
202    fn default() -> Self {
203        Self {
204            tx: BlePhy::Le1M,
205            rx: BlePhy::Le1M,
206        }
207    }
208}
209
210impl PhyPreference {
211    /// Create symmetric preference (same PHY for TX and RX)
212    pub fn symmetric(phy: BlePhy) -> Self {
213        Self { tx: phy, rx: phy }
214    }
215
216    /// Check if TX and RX PHYs match
217    pub fn is_symmetric(&self) -> bool {
218        self.tx == self.rx
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_phy_default() {
228        assert_eq!(BlePhy::default(), BlePhy::Le1M);
229    }
230
231    #[test]
232    fn test_phy_data_rates() {
233        assert_eq!(BlePhy::Le1M.data_rate_kbps(), 1000);
234        assert_eq!(BlePhy::Le2M.data_rate_kbps(), 2000);
235        assert_eq!(BlePhy::LeCodedS2.data_rate_kbps(), 500);
236        assert_eq!(BlePhy::LeCodedS8.data_rate_kbps(), 125);
237    }
238
239    #[test]
240    fn test_phy_ranges() {
241        assert_eq!(BlePhy::Le1M.typical_range_m(), 100);
242        assert_eq!(BlePhy::Le2M.typical_range_m(), 50);
243        assert_eq!(BlePhy::LeCodedS2.typical_range_m(), 200);
244        assert_eq!(BlePhy::LeCodedS8.typical_range_m(), 400);
245    }
246
247    #[test]
248    fn test_phy_is_coded() {
249        assert!(!BlePhy::Le1M.is_coded());
250        assert!(!BlePhy::Le2M.is_coded());
251        assert!(BlePhy::LeCodedS2.is_coded());
252        assert!(BlePhy::LeCodedS8.is_coded());
253    }
254
255    #[test]
256    fn test_phy_requires_ble5() {
257        assert!(!BlePhy::Le1M.requires_ble5());
258        assert!(BlePhy::Le2M.requires_ble5());
259        assert!(BlePhy::LeCodedS2.requires_ble5());
260        assert!(BlePhy::LeCodedS8.requires_ble5());
261    }
262
263    #[test]
264    fn test_phy_coding_scheme() {
265        assert_eq!(BlePhy::Le1M.coding_scheme(), None);
266        assert_eq!(BlePhy::Le2M.coding_scheme(), None);
267        assert_eq!(BlePhy::LeCodedS2.coding_scheme(), Some(2));
268        assert_eq!(BlePhy::LeCodedS8.coding_scheme(), Some(8));
269    }
270
271    #[test]
272    fn test_phy_display() {
273        assert_eq!(format!("{}", BlePhy::Le1M), "LE 1M");
274        assert_eq!(format!("{}", BlePhy::LeCodedS8), "LE Coded S=8");
275    }
276
277    #[test]
278    fn test_phy_transmit_time() {
279        // 100 bytes at 1 Mbps = ~880 bits / 1_000_000 = ~880 µs
280        let time_1m = BlePhy::Le1M.transmit_time_us(100);
281        let time_2m = BlePhy::Le2M.transmit_time_us(100);
282
283        // 2M should be faster
284        assert!(time_2m < time_1m);
285    }
286
287    #[test]
288    fn test_phy_capabilities_default() {
289        let caps = PhyCapabilities::default();
290        assert!(!caps.le_2m);
291        assert!(!caps.le_coded);
292        assert!(caps.supports(BlePhy::Le1M));
293        assert!(!caps.supports(BlePhy::Le2M));
294    }
295
296    #[test]
297    fn test_phy_capabilities_ble5() {
298        let caps = PhyCapabilities::ble5_full();
299        assert!(caps.supports(BlePhy::Le1M));
300        assert!(caps.supports(BlePhy::Le2M));
301        assert!(caps.supports(BlePhy::LeCodedS2));
302        assert!(caps.supports(BlePhy::LeCodedS8));
303    }
304
305    #[test]
306    fn test_phy_capabilities_best_for_range() {
307        let caps = PhyCapabilities::ble5_full();
308        assert_eq!(caps.best_for_range(), BlePhy::LeCodedS8);
309
310        let caps_no_coded = PhyCapabilities::ble5_no_coded();
311        assert_eq!(caps_no_coded.best_for_range(), BlePhy::Le1M);
312    }
313
314    #[test]
315    fn test_phy_capabilities_best_for_throughput() {
316        let caps = PhyCapabilities::ble5_full();
317        assert_eq!(caps.best_for_throughput(), BlePhy::Le2M);
318
319        let caps_basic = PhyCapabilities::le_1m_only();
320        assert_eq!(caps_basic.best_for_throughput(), BlePhy::Le1M);
321    }
322
323    #[test]
324    fn test_phy_preference_symmetric() {
325        let pref = PhyPreference::symmetric(BlePhy::LeCodedS8);
326        assert_eq!(pref.tx, BlePhy::LeCodedS8);
327        assert_eq!(pref.rx, BlePhy::LeCodedS8);
328        assert!(pref.is_symmetric());
329    }
330
331    #[test]
332    fn test_phy_preference_asymmetric() {
333        let pref = PhyPreference {
334            tx: BlePhy::Le2M,
335            rx: BlePhy::LeCodedS2,
336        };
337        assert!(!pref.is_symmetric());
338    }
339
340    #[test]
341    fn test_relative_power() {
342        assert!(BlePhy::Le2M.relative_power() < BlePhy::Le1M.relative_power());
343        assert!(BlePhy::LeCodedS8.relative_power() > BlePhy::Le1M.relative_power());
344    }
345}