hive_btle/phy/
controller.rs

1//! PHY Controller
2//!
3//! Manages PHY selection and switching for BLE connections,
4//! handling the state machine and platform-specific operations.
5
6#[cfg(not(feature = "std"))]
7use alloc::vec::Vec;
8
9use super::strategy::{evaluate_phy_switch, PhyStrategy, PhySwitchDecision};
10use super::types::{BlePhy, PhyCapabilities, PhyPreference};
11
12/// PHY controller state
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum PhyControllerState {
15    /// Not initialized or no connection
16    #[default]
17    Idle,
18    /// Negotiating PHY capabilities
19    Negotiating,
20    /// Operating with current PHY
21    Active,
22    /// Switching to a new PHY
23    Switching,
24    /// Error state
25    Error,
26}
27
28/// PHY update result
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum PhyUpdateResult {
31    /// PHY update succeeded
32    Success {
33        /// New TX PHY
34        tx_phy: BlePhy,
35        /// New RX PHY
36        rx_phy: BlePhy,
37    },
38    /// PHY update rejected by peer
39    Rejected,
40    /// PHY update not supported
41    NotSupported,
42    /// PHY update timed out
43    Timeout,
44    /// PHY update failed
45    Failed,
46}
47
48/// Event from the PHY controller
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum PhyControllerEvent {
51    /// PHY negotiation complete
52    NegotiationComplete {
53        /// Local capabilities
54        local: PhyCapabilities,
55        /// Peer capabilities
56        peer: PhyCapabilities,
57    },
58    /// PHY switch recommended
59    SwitchRecommended {
60        /// Current PHY
61        from: BlePhy,
62        /// Recommended PHY
63        to: BlePhy,
64        /// Current RSSI that triggered recommendation
65        rssi: i8,
66    },
67    /// PHY update completed
68    UpdateComplete(PhyUpdateResult),
69    /// RSSI measurement received
70    RssiUpdate(i8),
71}
72
73/// PHY controller statistics
74#[derive(Debug, Clone, Default)]
75pub struct PhyStats {
76    /// Number of PHY switches
77    pub switches: u64,
78    /// Successful switches
79    pub successful_switches: u64,
80    /// Failed switches
81    pub failed_switches: u64,
82    /// RSSI samples collected
83    pub rssi_samples: u64,
84    /// Time spent in each PHY (arbitrary units)
85    pub time_in_le1m: u64,
86    /// Time in LE 2M
87    pub time_in_le2m: u64,
88    /// Time in LE Coded
89    pub time_in_coded: u64,
90}
91
92impl PhyStats {
93    /// Get switch success rate
94    pub fn success_rate(&self) -> f32 {
95        if self.switches == 0 {
96            1.0
97        } else {
98            self.successful_switches as f32 / self.switches as f32
99        }
100    }
101
102    /// Record time in current PHY
103    pub fn record_time(&mut self, phy: BlePhy, time_units: u64) {
104        match phy {
105            BlePhy::Le1M => self.time_in_le1m += time_units,
106            BlePhy::Le2M => self.time_in_le2m += time_units,
107            BlePhy::LeCodedS2 | BlePhy::LeCodedS8 => self.time_in_coded += time_units,
108        }
109    }
110}
111
112/// PHY controller configuration
113#[derive(Debug, Clone)]
114pub struct PhyControllerConfig {
115    /// PHY selection strategy
116    pub strategy: PhyStrategy,
117    /// Minimum RSSI samples before considering switch
118    pub min_samples_for_switch: usize,
119    /// RSSI averaging window size
120    pub rssi_window_size: usize,
121    /// Minimum time between switches (milliseconds)
122    pub switch_cooldown_ms: u64,
123    /// Enable automatic PHY switching
124    pub auto_switch: bool,
125}
126
127impl Default for PhyControllerConfig {
128    fn default() -> Self {
129        Self {
130            strategy: PhyStrategy::default(),
131            min_samples_for_switch: 5,
132            rssi_window_size: 10,
133            switch_cooldown_ms: 5000,
134            auto_switch: true,
135        }
136    }
137}
138
139/// PHY Controller
140///
141/// Manages PHY selection, switching, and monitoring for a BLE connection.
142#[derive(Debug)]
143pub struct PhyController {
144    /// Configuration
145    config: PhyControllerConfig,
146    /// Current state
147    state: PhyControllerState,
148    /// Current TX PHY
149    tx_phy: BlePhy,
150    /// Current RX PHY
151    rx_phy: BlePhy,
152    /// Local PHY capabilities
153    local_caps: PhyCapabilities,
154    /// Peer PHY capabilities
155    peer_caps: PhyCapabilities,
156    /// RSSI samples
157    rssi_samples: Vec<i8>,
158    /// Last switch time (ms timestamp)
159    last_switch_time: u64,
160    /// Statistics
161    stats: PhyStats,
162}
163
164impl PhyController {
165    /// Create a new PHY controller
166    pub fn new(config: PhyControllerConfig, local_caps: PhyCapabilities) -> Self {
167        Self {
168            config,
169            state: PhyControllerState::Idle,
170            tx_phy: BlePhy::Le1M,
171            rx_phy: BlePhy::Le1M,
172            local_caps,
173            peer_caps: PhyCapabilities::default(),
174            rssi_samples: Vec::new(),
175            last_switch_time: 0,
176            stats: PhyStats::default(),
177        }
178    }
179
180    /// Create with default config
181    pub fn with_defaults(local_caps: PhyCapabilities) -> Self {
182        Self::new(PhyControllerConfig::default(), local_caps)
183    }
184
185    /// Get current state
186    pub fn state(&self) -> PhyControllerState {
187        self.state
188    }
189
190    /// Get current TX PHY
191    pub fn tx_phy(&self) -> BlePhy {
192        self.tx_phy
193    }
194
195    /// Get current RX PHY
196    pub fn rx_phy(&self) -> BlePhy {
197        self.rx_phy
198    }
199
200    /// Get current PHY preference
201    pub fn current_preference(&self) -> PhyPreference {
202        PhyPreference {
203            tx: self.tx_phy,
204            rx: self.rx_phy,
205        }
206    }
207
208    /// Get effective capabilities (intersection of local and peer)
209    pub fn effective_capabilities(&self) -> PhyCapabilities {
210        PhyCapabilities {
211            le_2m: self.local_caps.le_2m && self.peer_caps.le_2m,
212            le_coded: self.local_caps.le_coded && self.peer_caps.le_coded,
213        }
214    }
215
216    /// Get statistics
217    pub fn stats(&self) -> &PhyStats {
218        &self.stats
219    }
220
221    /// Get config
222    pub fn config(&self) -> &PhyControllerConfig {
223        &self.config
224    }
225
226    /// Start PHY negotiation for a new connection
227    pub fn start_negotiation(&mut self) {
228        self.state = PhyControllerState::Negotiating;
229        self.rssi_samples.clear();
230    }
231
232    /// Complete negotiation with peer capabilities
233    pub fn complete_negotiation(&mut self, peer_caps: PhyCapabilities) -> PhyControllerEvent {
234        self.peer_caps = peer_caps;
235        self.state = PhyControllerState::Active;
236
237        PhyControllerEvent::NegotiationComplete {
238            local: self.local_caps,
239            peer: peer_caps,
240        }
241    }
242
243    /// Record an RSSI measurement
244    pub fn record_rssi(&mut self, rssi: i8, current_time: u64) -> Option<PhyControllerEvent> {
245        self.rssi_samples.push(rssi);
246        self.stats.rssi_samples += 1;
247
248        // Keep only recent samples
249        if self.rssi_samples.len() > self.config.rssi_window_size {
250            self.rssi_samples.remove(0);
251        }
252
253        // Check for PHY switch if enabled and have enough samples
254        if self.config.auto_switch
255            && self.state == PhyControllerState::Active
256            && self.rssi_samples.len() >= self.config.min_samples_for_switch
257            && current_time >= self.last_switch_time + self.config.switch_cooldown_ms
258        {
259            let avg_rssi = self.average_rssi();
260            let decision = self.evaluate_switch(avg_rssi);
261
262            if let PhySwitchDecision::Switch(to_phy) = decision {
263                return Some(PhyControllerEvent::SwitchRecommended {
264                    from: self.tx_phy,
265                    to: to_phy,
266                    rssi: avg_rssi,
267                });
268            }
269        }
270
271        None
272    }
273
274    /// Get average RSSI from samples
275    pub fn average_rssi(&self) -> i8 {
276        if self.rssi_samples.is_empty() {
277            return -100;
278        }
279        let sum: i32 = self.rssi_samples.iter().map(|&r| r as i32).sum();
280        (sum / self.rssi_samples.len() as i32) as i8
281    }
282
283    /// Evaluate whether to switch PHY
284    pub fn evaluate_switch(&self, rssi: i8) -> PhySwitchDecision {
285        let effective_caps = self.effective_capabilities();
286        evaluate_phy_switch(&self.config.strategy, self.tx_phy, rssi, &effective_caps)
287    }
288
289    /// Request a PHY update
290    pub fn request_switch(&mut self, to_phy: BlePhy) -> Option<PhyPreference> {
291        if self.state != PhyControllerState::Active {
292            return None;
293        }
294
295        let effective_caps = self.effective_capabilities();
296        if !effective_caps.supports(to_phy) {
297            return None;
298        }
299
300        self.state = PhyControllerState::Switching;
301        self.stats.switches += 1;
302
303        Some(PhyPreference::symmetric(to_phy))
304    }
305
306    /// Handle PHY update result from stack
307    pub fn handle_update_result(
308        &mut self,
309        result: PhyUpdateResult,
310        current_time: u64,
311    ) -> PhyControllerEvent {
312        match result {
313            PhyUpdateResult::Success { tx_phy, rx_phy } => {
314                self.tx_phy = tx_phy;
315                self.rx_phy = rx_phy;
316                self.last_switch_time = current_time;
317                self.state = PhyControllerState::Active;
318                self.stats.successful_switches += 1;
319            }
320            PhyUpdateResult::Rejected
321            | PhyUpdateResult::NotSupported
322            | PhyUpdateResult::Timeout
323            | PhyUpdateResult::Failed => {
324                self.state = PhyControllerState::Active;
325                self.stats.failed_switches += 1;
326            }
327        }
328
329        PhyControllerEvent::UpdateComplete(result)
330    }
331
332    /// Reset controller state
333    pub fn reset(&mut self) {
334        self.state = PhyControllerState::Idle;
335        self.tx_phy = BlePhy::Le1M;
336        self.rx_phy = BlePhy::Le1M;
337        self.peer_caps = PhyCapabilities::default();
338        self.rssi_samples.clear();
339        self.last_switch_time = 0;
340    }
341
342    /// Set PHY strategy
343    pub fn set_strategy(&mut self, strategy: PhyStrategy) {
344        self.config.strategy = strategy;
345    }
346
347    /// Enable/disable auto switching
348    pub fn set_auto_switch(&mut self, enabled: bool) {
349        self.config.auto_switch = enabled;
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    fn make_controller() -> PhyController {
358        let caps = PhyCapabilities::ble5_full();
359        PhyController::with_defaults(caps)
360    }
361
362    #[test]
363    fn test_controller_creation() {
364        let ctrl = make_controller();
365        assert_eq!(ctrl.state(), PhyControllerState::Idle);
366        assert_eq!(ctrl.tx_phy(), BlePhy::Le1M);
367        assert_eq!(ctrl.rx_phy(), BlePhy::Le1M);
368    }
369
370    #[test]
371    fn test_negotiation_flow() {
372        let mut ctrl = make_controller();
373
374        ctrl.start_negotiation();
375        assert_eq!(ctrl.state(), PhyControllerState::Negotiating);
376
377        let event = ctrl.complete_negotiation(PhyCapabilities::ble5_full());
378        assert_eq!(ctrl.state(), PhyControllerState::Active);
379
380        if let PhyControllerEvent::NegotiationComplete { local, peer } = event {
381            assert!(local.le_2m);
382            assert!(peer.le_coded);
383        } else {
384            panic!("Expected NegotiationComplete event");
385        }
386    }
387
388    #[test]
389    fn test_effective_capabilities() {
390        let mut ctrl = make_controller();
391        ctrl.complete_negotiation(PhyCapabilities::ble5_no_coded());
392
393        let effective = ctrl.effective_capabilities();
394        assert!(effective.le_2m);
395        assert!(!effective.le_coded); // Peer doesn't support
396    }
397
398    #[test]
399    fn test_rssi_recording() {
400        let mut ctrl = make_controller();
401        ctrl.complete_negotiation(PhyCapabilities::ble5_full());
402
403        for i in 0..5 {
404            ctrl.record_rssi(-50 - i, 1000 + i as u64 * 100);
405        }
406
407        let avg = ctrl.average_rssi();
408        assert!((-55..=-50).contains(&avg));
409    }
410
411    #[test]
412    fn test_rssi_window_limit() {
413        let mut ctrl = make_controller();
414        ctrl.complete_negotiation(PhyCapabilities::ble5_full());
415
416        // Add more samples than window size
417        for i in 0..20 {
418            ctrl.record_rssi(-50, i * 100);
419        }
420
421        assert_eq!(ctrl.rssi_samples.len(), ctrl.config.rssi_window_size);
422    }
423
424    #[test]
425    fn test_switch_request() {
426        let mut ctrl = make_controller();
427        ctrl.complete_negotiation(PhyCapabilities::ble5_full());
428
429        let pref = ctrl.request_switch(BlePhy::Le2M);
430        assert!(pref.is_some());
431        assert_eq!(ctrl.state(), PhyControllerState::Switching);
432    }
433
434    #[test]
435    fn test_switch_request_unsupported() {
436        let mut ctrl = make_controller();
437        ctrl.complete_negotiation(PhyCapabilities::le_1m_only());
438
439        let pref = ctrl.request_switch(BlePhy::LeCodedS8);
440        assert!(pref.is_none()); // Peer doesn't support
441    }
442
443    #[test]
444    fn test_update_result_success() {
445        let mut ctrl = make_controller();
446        ctrl.complete_negotiation(PhyCapabilities::ble5_full());
447        ctrl.request_switch(BlePhy::Le2M);
448
449        let result = PhyUpdateResult::Success {
450            tx_phy: BlePhy::Le2M,
451            rx_phy: BlePhy::Le2M,
452        };
453        ctrl.handle_update_result(result, 5000);
454
455        assert_eq!(ctrl.state(), PhyControllerState::Active);
456        assert_eq!(ctrl.tx_phy(), BlePhy::Le2M);
457        assert_eq!(ctrl.rx_phy(), BlePhy::Le2M);
458        assert_eq!(ctrl.stats().successful_switches, 1);
459    }
460
461    #[test]
462    fn test_update_result_rejected() {
463        let mut ctrl = make_controller();
464        ctrl.complete_negotiation(PhyCapabilities::ble5_full());
465        ctrl.request_switch(BlePhy::Le2M);
466
467        ctrl.handle_update_result(PhyUpdateResult::Rejected, 5000);
468
469        assert_eq!(ctrl.state(), PhyControllerState::Active);
470        assert_eq!(ctrl.tx_phy(), BlePhy::Le1M); // Unchanged
471        assert_eq!(ctrl.stats().failed_switches, 1);
472    }
473
474    #[test]
475    fn test_auto_switch_recommendation() {
476        let config = PhyControllerConfig {
477            min_samples_for_switch: 3,
478            switch_cooldown_ms: 0, // No cooldown for test
479            ..Default::default()
480        };
481        let caps = PhyCapabilities::ble5_full();
482        let mut ctrl = PhyController::new(config, caps);
483        ctrl.complete_negotiation(PhyCapabilities::ble5_full());
484
485        // Record strong RSSI samples
486        for i in 0..5 {
487            let event = ctrl.record_rssi(-40, i * 100);
488            if i >= 2 {
489                // After min_samples_for_switch
490                if let Some(PhyControllerEvent::SwitchRecommended { to, .. }) = event {
491                    assert_eq!(to, BlePhy::Le2M);
492                    return; // Test passed
493                }
494            }
495        }
496
497        panic!("Expected switch recommendation for strong signal");
498    }
499
500    #[test]
501    fn test_switch_cooldown() {
502        let config = PhyControllerConfig {
503            min_samples_for_switch: 2,
504            switch_cooldown_ms: 5000,
505            ..Default::default()
506        };
507        let caps = PhyCapabilities::ble5_full();
508        let mut ctrl = PhyController::new(config, caps);
509        ctrl.complete_negotiation(PhyCapabilities::ble5_full());
510
511        // Simulate a recent switch
512        ctrl.last_switch_time = 1000;
513
514        // Record samples at time 2000 (within cooldown)
515        let event = ctrl.record_rssi(-40, 2000);
516        assert!(event.is_none()); // Cooldown not expired
517
518        let event = ctrl.record_rssi(-40, 2100);
519        assert!(event.is_none()); // Still in cooldown
520    }
521
522    #[test]
523    fn test_reset() {
524        let mut ctrl = make_controller();
525        ctrl.complete_negotiation(PhyCapabilities::ble5_full());
526        ctrl.record_rssi(-50, 1000);
527
528        ctrl.reset();
529
530        assert_eq!(ctrl.state(), PhyControllerState::Idle);
531        assert_eq!(ctrl.tx_phy(), BlePhy::Le1M);
532        assert!(ctrl.rssi_samples.is_empty());
533    }
534
535    #[test]
536    fn test_stats_success_rate() {
537        let mut stats = PhyStats::default();
538        assert_eq!(stats.success_rate(), 1.0);
539
540        stats.switches = 10;
541        stats.successful_switches = 8;
542        stats.failed_switches = 2;
543        assert!((stats.success_rate() - 0.8).abs() < 0.01);
544    }
545
546    #[test]
547    fn test_stats_record_time() {
548        let mut stats = PhyStats::default();
549
550        stats.record_time(BlePhy::Le1M, 100);
551        stats.record_time(BlePhy::Le2M, 50);
552        stats.record_time(BlePhy::LeCodedS8, 200);
553
554        assert_eq!(stats.time_in_le1m, 100);
555        assert_eq!(stats.time_in_le2m, 50);
556        assert_eq!(stats.time_in_coded, 200);
557    }
558}