1#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum PhyControllerState {
15 #[default]
17 Idle,
18 Negotiating,
20 Active,
22 Switching,
24 Error,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum PhyUpdateResult {
31 Success {
33 tx_phy: BlePhy,
35 rx_phy: BlePhy,
37 },
38 Rejected,
40 NotSupported,
42 Timeout,
44 Failed,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum PhyControllerEvent {
51 NegotiationComplete {
53 local: PhyCapabilities,
55 peer: PhyCapabilities,
57 },
58 SwitchRecommended {
60 from: BlePhy,
62 to: BlePhy,
64 rssi: i8,
66 },
67 UpdateComplete(PhyUpdateResult),
69 RssiUpdate(i8),
71}
72
73#[derive(Debug, Clone, Default)]
75pub struct PhyStats {
76 pub switches: u64,
78 pub successful_switches: u64,
80 pub failed_switches: u64,
82 pub rssi_samples: u64,
84 pub time_in_le1m: u64,
86 pub time_in_le2m: u64,
88 pub time_in_coded: u64,
90}
91
92impl PhyStats {
93 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 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#[derive(Debug, Clone)]
114pub struct PhyControllerConfig {
115 pub strategy: PhyStrategy,
117 pub min_samples_for_switch: usize,
119 pub rssi_window_size: usize,
121 pub switch_cooldown_ms: u64,
123 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#[derive(Debug)]
143pub struct PhyController {
144 config: PhyControllerConfig,
146 state: PhyControllerState,
148 tx_phy: BlePhy,
150 rx_phy: BlePhy,
152 local_caps: PhyCapabilities,
154 peer_caps: PhyCapabilities,
156 rssi_samples: Vec<i8>,
158 last_switch_time: u64,
160 stats: PhyStats,
162}
163
164impl PhyController {
165 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 pub fn with_defaults(local_caps: PhyCapabilities) -> Self {
182 Self::new(PhyControllerConfig::default(), local_caps)
183 }
184
185 pub fn state(&self) -> PhyControllerState {
187 self.state
188 }
189
190 pub fn tx_phy(&self) -> BlePhy {
192 self.tx_phy
193 }
194
195 pub fn rx_phy(&self) -> BlePhy {
197 self.rx_phy
198 }
199
200 pub fn current_preference(&self) -> PhyPreference {
202 PhyPreference {
203 tx: self.tx_phy,
204 rx: self.rx_phy,
205 }
206 }
207
208 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 pub fn stats(&self) -> &PhyStats {
218 &self.stats
219 }
220
221 pub fn config(&self) -> &PhyControllerConfig {
223 &self.config
224 }
225
226 pub fn start_negotiation(&mut self) {
228 self.state = PhyControllerState::Negotiating;
229 self.rssi_samples.clear();
230 }
231
232 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 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 if self.rssi_samples.len() > self.config.rssi_window_size {
250 self.rssi_samples.remove(0);
251 }
252
253 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 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 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 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 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 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 pub fn set_strategy(&mut self, strategy: PhyStrategy) {
344 self.config.strategy = strategy;
345 }
346
347 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); }
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 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()); }
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); 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, ..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 for i in 0..5 {
487 let event = ctrl.record_rssi(-40, i * 100);
488 if i >= 2 {
489 if let Some(PhyControllerEvent::SwitchRecommended { to, .. }) = event {
491 assert_eq!(to, BlePhy::Le2M);
492 return; }
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 ctrl.last_switch_time = 1000;
513
514 let event = ctrl.record_rssi(-40, 2000);
516 assert!(event.is_none()); let event = ctrl.record_rssi(-40, 2100);
519 assert!(event.is_none()); }
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}