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