1#[cfg(not(feature = "std"))]
22use alloc::vec::Vec;
23
24use super::strategy::{evaluate_phy_switch, PhyStrategy, PhySwitchDecision};
25use super::types::{BlePhy, PhyCapabilities, PhyPreference};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum PhyControllerState {
30 #[default]
32 Idle,
33 Negotiating,
35 Active,
37 Switching,
39 Error,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum PhyUpdateResult {
46 Success {
48 tx_phy: BlePhy,
50 rx_phy: BlePhy,
52 },
53 Rejected,
55 NotSupported,
57 Timeout,
59 Failed,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum PhyControllerEvent {
66 NegotiationComplete {
68 local: PhyCapabilities,
70 peer: PhyCapabilities,
72 },
73 SwitchRecommended {
75 from: BlePhy,
77 to: BlePhy,
79 rssi: i8,
81 },
82 UpdateComplete(PhyUpdateResult),
84 RssiUpdate(i8),
86}
87
88#[derive(Debug, Clone, Default)]
90pub struct PhyStats {
91 pub switches: u64,
93 pub successful_switches: u64,
95 pub failed_switches: u64,
97 pub rssi_samples: u64,
99 pub time_in_le1m: u64,
101 pub time_in_le2m: u64,
103 pub time_in_coded: u64,
105}
106
107impl PhyStats {
108 pub fn success_rate(&self) -> f32 {
110 if self.switches == 0 {
111 1.0
112 } else {
113 self.successful_switches as f32 / self.switches as f32
114 }
115 }
116
117 pub fn record_time(&mut self, phy: BlePhy, time_units: u64) {
119 match phy {
120 BlePhy::Le1M => self.time_in_le1m += time_units,
121 BlePhy::Le2M => self.time_in_le2m += time_units,
122 BlePhy::LeCodedS2 | BlePhy::LeCodedS8 => self.time_in_coded += time_units,
123 }
124 }
125}
126
127#[derive(Debug, Clone)]
129pub struct PhyControllerConfig {
130 pub strategy: PhyStrategy,
132 pub min_samples_for_switch: usize,
134 pub rssi_window_size: usize,
136 pub switch_cooldown_ms: u64,
138 pub auto_switch: bool,
140}
141
142impl Default for PhyControllerConfig {
143 fn default() -> Self {
144 Self {
145 strategy: PhyStrategy::default(),
146 min_samples_for_switch: 5,
147 rssi_window_size: 10,
148 switch_cooldown_ms: 5000,
149 auto_switch: true,
150 }
151 }
152}
153
154#[derive(Debug)]
158pub struct PhyController {
159 config: PhyControllerConfig,
161 state: PhyControllerState,
163 tx_phy: BlePhy,
165 rx_phy: BlePhy,
167 local_caps: PhyCapabilities,
169 peer_caps: PhyCapabilities,
171 rssi_samples: Vec<i8>,
173 last_switch_time: u64,
175 stats: PhyStats,
177}
178
179impl PhyController {
180 pub fn new(config: PhyControllerConfig, local_caps: PhyCapabilities) -> Self {
182 Self {
183 config,
184 state: PhyControllerState::Idle,
185 tx_phy: BlePhy::Le1M,
186 rx_phy: BlePhy::Le1M,
187 local_caps,
188 peer_caps: PhyCapabilities::default(),
189 rssi_samples: Vec::new(),
190 last_switch_time: 0,
191 stats: PhyStats::default(),
192 }
193 }
194
195 pub fn with_defaults(local_caps: PhyCapabilities) -> Self {
197 Self::new(PhyControllerConfig::default(), local_caps)
198 }
199
200 pub fn state(&self) -> PhyControllerState {
202 self.state
203 }
204
205 pub fn tx_phy(&self) -> BlePhy {
207 self.tx_phy
208 }
209
210 pub fn rx_phy(&self) -> BlePhy {
212 self.rx_phy
213 }
214
215 pub fn current_preference(&self) -> PhyPreference {
217 PhyPreference {
218 tx: self.tx_phy,
219 rx: self.rx_phy,
220 }
221 }
222
223 pub fn effective_capabilities(&self) -> PhyCapabilities {
225 PhyCapabilities {
226 le_2m: self.local_caps.le_2m && self.peer_caps.le_2m,
227 le_coded: self.local_caps.le_coded && self.peer_caps.le_coded,
228 }
229 }
230
231 pub fn stats(&self) -> &PhyStats {
233 &self.stats
234 }
235
236 pub fn config(&self) -> &PhyControllerConfig {
238 &self.config
239 }
240
241 pub fn start_negotiation(&mut self) {
243 self.state = PhyControllerState::Negotiating;
244 self.rssi_samples.clear();
245 }
246
247 pub fn complete_negotiation(&mut self, peer_caps: PhyCapabilities) -> PhyControllerEvent {
249 self.peer_caps = peer_caps;
250 self.state = PhyControllerState::Active;
251
252 PhyControllerEvent::NegotiationComplete {
253 local: self.local_caps,
254 peer: peer_caps,
255 }
256 }
257
258 pub fn record_rssi(&mut self, rssi: i8, current_time: u64) -> Option<PhyControllerEvent> {
260 self.rssi_samples.push(rssi);
261 self.stats.rssi_samples += 1;
262
263 if self.rssi_samples.len() > self.config.rssi_window_size {
265 self.rssi_samples.remove(0);
266 }
267
268 if self.config.auto_switch
270 && self.state == PhyControllerState::Active
271 && self.rssi_samples.len() >= self.config.min_samples_for_switch
272 && current_time >= self.last_switch_time + self.config.switch_cooldown_ms
273 {
274 let avg_rssi = self.average_rssi();
275 let decision = self.evaluate_switch(avg_rssi);
276
277 if let PhySwitchDecision::Switch(to_phy) = decision {
278 return Some(PhyControllerEvent::SwitchRecommended {
279 from: self.tx_phy,
280 to: to_phy,
281 rssi: avg_rssi,
282 });
283 }
284 }
285
286 None
287 }
288
289 pub fn average_rssi(&self) -> i8 {
291 if self.rssi_samples.is_empty() {
292 return -100;
293 }
294 let sum: i32 = self.rssi_samples.iter().map(|&r| r as i32).sum();
295 (sum / self.rssi_samples.len() as i32) as i8
296 }
297
298 pub fn evaluate_switch(&self, rssi: i8) -> PhySwitchDecision {
300 let effective_caps = self.effective_capabilities();
301 evaluate_phy_switch(&self.config.strategy, self.tx_phy, rssi, &effective_caps)
302 }
303
304 pub fn request_switch(&mut self, to_phy: BlePhy) -> Option<PhyPreference> {
306 if self.state != PhyControllerState::Active {
307 return None;
308 }
309
310 let effective_caps = self.effective_capabilities();
311 if !effective_caps.supports(to_phy) {
312 return None;
313 }
314
315 self.state = PhyControllerState::Switching;
316 self.stats.switches += 1;
317
318 Some(PhyPreference::symmetric(to_phy))
319 }
320
321 pub fn handle_update_result(
323 &mut self,
324 result: PhyUpdateResult,
325 current_time: u64,
326 ) -> PhyControllerEvent {
327 match result {
328 PhyUpdateResult::Success { tx_phy, rx_phy } => {
329 self.tx_phy = tx_phy;
330 self.rx_phy = rx_phy;
331 self.last_switch_time = current_time;
332 self.state = PhyControllerState::Active;
333 self.stats.successful_switches += 1;
334 }
335 PhyUpdateResult::Rejected
336 | PhyUpdateResult::NotSupported
337 | PhyUpdateResult::Timeout
338 | PhyUpdateResult::Failed => {
339 self.state = PhyControllerState::Active;
340 self.stats.failed_switches += 1;
341 }
342 }
343
344 PhyControllerEvent::UpdateComplete(result)
345 }
346
347 pub fn reset(&mut self) {
349 self.state = PhyControllerState::Idle;
350 self.tx_phy = BlePhy::Le1M;
351 self.rx_phy = BlePhy::Le1M;
352 self.peer_caps = PhyCapabilities::default();
353 self.rssi_samples.clear();
354 self.last_switch_time = 0;
355 }
356
357 pub fn set_strategy(&mut self, strategy: PhyStrategy) {
359 self.config.strategy = strategy;
360 }
361
362 pub fn set_auto_switch(&mut self, enabled: bool) {
364 self.config.auto_switch = enabled;
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 fn make_controller() -> PhyController {
373 let caps = PhyCapabilities::ble5_full();
374 PhyController::with_defaults(caps)
375 }
376
377 #[test]
378 fn test_controller_creation() {
379 let ctrl = make_controller();
380 assert_eq!(ctrl.state(), PhyControllerState::Idle);
381 assert_eq!(ctrl.tx_phy(), BlePhy::Le1M);
382 assert_eq!(ctrl.rx_phy(), BlePhy::Le1M);
383 }
384
385 #[test]
386 fn test_negotiation_flow() {
387 let mut ctrl = make_controller();
388
389 ctrl.start_negotiation();
390 assert_eq!(ctrl.state(), PhyControllerState::Negotiating);
391
392 let event = ctrl.complete_negotiation(PhyCapabilities::ble5_full());
393 assert_eq!(ctrl.state(), PhyControllerState::Active);
394
395 if let PhyControllerEvent::NegotiationComplete { local, peer } = event {
396 assert!(local.le_2m);
397 assert!(peer.le_coded);
398 } else {
399 panic!("Expected NegotiationComplete event");
400 }
401 }
402
403 #[test]
404 fn test_effective_capabilities() {
405 let mut ctrl = make_controller();
406 ctrl.complete_negotiation(PhyCapabilities::ble5_no_coded());
407
408 let effective = ctrl.effective_capabilities();
409 assert!(effective.le_2m);
410 assert!(!effective.le_coded); }
412
413 #[test]
414 fn test_rssi_recording() {
415 let mut ctrl = make_controller();
416 ctrl.complete_negotiation(PhyCapabilities::ble5_full());
417
418 for i in 0..5 {
419 ctrl.record_rssi(-50 - i, 1000 + i as u64 * 100);
420 }
421
422 let avg = ctrl.average_rssi();
423 assert!((-55..=-50).contains(&avg));
424 }
425
426 #[test]
427 fn test_rssi_window_limit() {
428 let mut ctrl = make_controller();
429 ctrl.complete_negotiation(PhyCapabilities::ble5_full());
430
431 for i in 0..20 {
433 ctrl.record_rssi(-50, i * 100);
434 }
435
436 assert_eq!(ctrl.rssi_samples.len(), ctrl.config.rssi_window_size);
437 }
438
439 #[test]
440 fn test_switch_request() {
441 let mut ctrl = make_controller();
442 ctrl.complete_negotiation(PhyCapabilities::ble5_full());
443
444 let pref = ctrl.request_switch(BlePhy::Le2M);
445 assert!(pref.is_some());
446 assert_eq!(ctrl.state(), PhyControllerState::Switching);
447 }
448
449 #[test]
450 fn test_switch_request_unsupported() {
451 let mut ctrl = make_controller();
452 ctrl.complete_negotiation(PhyCapabilities::le_1m_only());
453
454 let pref = ctrl.request_switch(BlePhy::LeCodedS8);
455 assert!(pref.is_none()); }
457
458 #[test]
459 fn test_update_result_success() {
460 let mut ctrl = make_controller();
461 ctrl.complete_negotiation(PhyCapabilities::ble5_full());
462 ctrl.request_switch(BlePhy::Le2M);
463
464 let result = PhyUpdateResult::Success {
465 tx_phy: BlePhy::Le2M,
466 rx_phy: BlePhy::Le2M,
467 };
468 ctrl.handle_update_result(result, 5000);
469
470 assert_eq!(ctrl.state(), PhyControllerState::Active);
471 assert_eq!(ctrl.tx_phy(), BlePhy::Le2M);
472 assert_eq!(ctrl.rx_phy(), BlePhy::Le2M);
473 assert_eq!(ctrl.stats().successful_switches, 1);
474 }
475
476 #[test]
477 fn test_update_result_rejected() {
478 let mut ctrl = make_controller();
479 ctrl.complete_negotiation(PhyCapabilities::ble5_full());
480 ctrl.request_switch(BlePhy::Le2M);
481
482 ctrl.handle_update_result(PhyUpdateResult::Rejected, 5000);
483
484 assert_eq!(ctrl.state(), PhyControllerState::Active);
485 assert_eq!(ctrl.tx_phy(), BlePhy::Le1M); assert_eq!(ctrl.stats().failed_switches, 1);
487 }
488
489 #[test]
490 fn test_auto_switch_recommendation() {
491 let config = PhyControllerConfig {
492 min_samples_for_switch: 3,
493 switch_cooldown_ms: 0, ..Default::default()
495 };
496 let caps = PhyCapabilities::ble5_full();
497 let mut ctrl = PhyController::new(config, caps);
498 ctrl.complete_negotiation(PhyCapabilities::ble5_full());
499
500 for i in 0..5 {
502 let event = ctrl.record_rssi(-40, i * 100);
503 if i >= 2 {
504 if let Some(PhyControllerEvent::SwitchRecommended { to, .. }) = event {
506 assert_eq!(to, BlePhy::Le2M);
507 return; }
509 }
510 }
511
512 panic!("Expected switch recommendation for strong signal");
513 }
514
515 #[test]
516 fn test_switch_cooldown() {
517 let config = PhyControllerConfig {
518 min_samples_for_switch: 2,
519 switch_cooldown_ms: 5000,
520 ..Default::default()
521 };
522 let caps = PhyCapabilities::ble5_full();
523 let mut ctrl = PhyController::new(config, caps);
524 ctrl.complete_negotiation(PhyCapabilities::ble5_full());
525
526 ctrl.last_switch_time = 1000;
528
529 let event = ctrl.record_rssi(-40, 2000);
531 assert!(event.is_none()); let event = ctrl.record_rssi(-40, 2100);
534 assert!(event.is_none()); }
536
537 #[test]
538 fn test_reset() {
539 let mut ctrl = make_controller();
540 ctrl.complete_negotiation(PhyCapabilities::ble5_full());
541 ctrl.record_rssi(-50, 1000);
542
543 ctrl.reset();
544
545 assert_eq!(ctrl.state(), PhyControllerState::Idle);
546 assert_eq!(ctrl.tx_phy(), BlePhy::Le1M);
547 assert!(ctrl.rssi_samples.is_empty());
548 }
549
550 #[test]
551 fn test_stats_success_rate() {
552 let mut stats = PhyStats::default();
553 assert_eq!(stats.success_rate(), 1.0);
554
555 stats.switches = 10;
556 stats.successful_switches = 8;
557 stats.failed_switches = 2;
558 assert!((stats.success_rate() - 0.8).abs() < 0.01);
559 }
560
561 #[test]
562 fn test_stats_record_time() {
563 let mut stats = PhyStats::default();
564
565 stats.record_time(BlePhy::Le1M, 100);
566 stats.record_time(BlePhy::Le2M, 50);
567 stats.record_time(BlePhy::LeCodedS8, 200);
568
569 assert_eq!(stats.time_in_le1m, 100);
570 assert_eq!(stats.time_in_le2m, 50);
571 assert_eq!(stats.time_in_coded, 200);
572 }
573}