#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use super::strategy::{evaluate_phy_switch, PhyStrategy, PhySwitchDecision};
use super::types::{BlePhy, PhyCapabilities, PhyPreference};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PhyControllerState {
#[default]
Idle,
Negotiating,
Active,
Switching,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhyUpdateResult {
Success {
tx_phy: BlePhy,
rx_phy: BlePhy,
},
Rejected,
NotSupported,
Timeout,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhyControllerEvent {
NegotiationComplete {
local: PhyCapabilities,
peer: PhyCapabilities,
},
SwitchRecommended {
from: BlePhy,
to: BlePhy,
rssi: i8,
},
UpdateComplete(PhyUpdateResult),
RssiUpdate(i8),
}
#[derive(Debug, Clone, Default)]
pub struct PhyStats {
pub switches: u64,
pub successful_switches: u64,
pub failed_switches: u64,
pub rssi_samples: u64,
pub time_in_le1m: u64,
pub time_in_le2m: u64,
pub time_in_coded: u64,
}
impl PhyStats {
pub fn success_rate(&self) -> f32 {
if self.switches == 0 {
1.0
} else {
self.successful_switches as f32 / self.switches as f32
}
}
pub fn record_time(&mut self, phy: BlePhy, time_units: u64) {
match phy {
BlePhy::Le1M => self.time_in_le1m += time_units,
BlePhy::Le2M => self.time_in_le2m += time_units,
BlePhy::LeCodedS2 | BlePhy::LeCodedS8 => self.time_in_coded += time_units,
}
}
}
#[derive(Debug, Clone)]
pub struct PhyControllerConfig {
pub strategy: PhyStrategy,
pub min_samples_for_switch: usize,
pub rssi_window_size: usize,
pub switch_cooldown_ms: u64,
pub auto_switch: bool,
}
impl Default for PhyControllerConfig {
fn default() -> Self {
Self {
strategy: PhyStrategy::default(),
min_samples_for_switch: 5,
rssi_window_size: 10,
switch_cooldown_ms: 5000,
auto_switch: true,
}
}
}
#[derive(Debug)]
pub struct PhyController {
config: PhyControllerConfig,
state: PhyControllerState,
tx_phy: BlePhy,
rx_phy: BlePhy,
local_caps: PhyCapabilities,
peer_caps: PhyCapabilities,
rssi_samples: Vec<i8>,
last_switch_time: u64,
stats: PhyStats,
}
impl PhyController {
pub fn new(config: PhyControllerConfig, local_caps: PhyCapabilities) -> Self {
Self {
config,
state: PhyControllerState::Idle,
tx_phy: BlePhy::Le1M,
rx_phy: BlePhy::Le1M,
local_caps,
peer_caps: PhyCapabilities::default(),
rssi_samples: Vec::new(),
last_switch_time: 0,
stats: PhyStats::default(),
}
}
pub fn with_defaults(local_caps: PhyCapabilities) -> Self {
Self::new(PhyControllerConfig::default(), local_caps)
}
pub fn state(&self) -> PhyControllerState {
self.state
}
pub fn tx_phy(&self) -> BlePhy {
self.tx_phy
}
pub fn rx_phy(&self) -> BlePhy {
self.rx_phy
}
pub fn current_preference(&self) -> PhyPreference {
PhyPreference {
tx: self.tx_phy,
rx: self.rx_phy,
}
}
pub fn effective_capabilities(&self) -> PhyCapabilities {
PhyCapabilities {
le_2m: self.local_caps.le_2m && self.peer_caps.le_2m,
le_coded: self.local_caps.le_coded && self.peer_caps.le_coded,
}
}
pub fn stats(&self) -> &PhyStats {
&self.stats
}
pub fn config(&self) -> &PhyControllerConfig {
&self.config
}
pub fn start_negotiation(&mut self) {
self.state = PhyControllerState::Negotiating;
self.rssi_samples.clear();
}
pub fn complete_negotiation(&mut self, peer_caps: PhyCapabilities) -> PhyControllerEvent {
self.peer_caps = peer_caps;
self.state = PhyControllerState::Active;
PhyControllerEvent::NegotiationComplete {
local: self.local_caps,
peer: peer_caps,
}
}
pub fn record_rssi(&mut self, rssi: i8, current_time: u64) -> Option<PhyControllerEvent> {
self.rssi_samples.push(rssi);
self.stats.rssi_samples += 1;
if self.rssi_samples.len() > self.config.rssi_window_size {
self.rssi_samples.remove(0);
}
if self.config.auto_switch
&& self.state == PhyControllerState::Active
&& self.rssi_samples.len() >= self.config.min_samples_for_switch
&& current_time >= self.last_switch_time + self.config.switch_cooldown_ms
{
let avg_rssi = self.average_rssi();
let decision = self.evaluate_switch(avg_rssi);
if let PhySwitchDecision::Switch(to_phy) = decision {
return Some(PhyControllerEvent::SwitchRecommended {
from: self.tx_phy,
to: to_phy,
rssi: avg_rssi,
});
}
}
None
}
pub fn average_rssi(&self) -> i8 {
if self.rssi_samples.is_empty() {
return -100;
}
let sum: i32 = self.rssi_samples.iter().map(|&r| r as i32).sum();
(sum / self.rssi_samples.len() as i32) as i8
}
pub fn evaluate_switch(&self, rssi: i8) -> PhySwitchDecision {
let effective_caps = self.effective_capabilities();
evaluate_phy_switch(&self.config.strategy, self.tx_phy, rssi, &effective_caps)
}
pub fn request_switch(&mut self, to_phy: BlePhy) -> Option<PhyPreference> {
if self.state != PhyControllerState::Active {
return None;
}
let effective_caps = self.effective_capabilities();
if !effective_caps.supports(to_phy) {
return None;
}
self.state = PhyControllerState::Switching;
self.stats.switches += 1;
Some(PhyPreference::symmetric(to_phy))
}
pub fn handle_update_result(
&mut self,
result: PhyUpdateResult,
current_time: u64,
) -> PhyControllerEvent {
match result {
PhyUpdateResult::Success { tx_phy, rx_phy } => {
self.tx_phy = tx_phy;
self.rx_phy = rx_phy;
self.last_switch_time = current_time;
self.state = PhyControllerState::Active;
self.stats.successful_switches += 1;
}
PhyUpdateResult::Rejected
| PhyUpdateResult::NotSupported
| PhyUpdateResult::Timeout
| PhyUpdateResult::Failed => {
self.state = PhyControllerState::Active;
self.stats.failed_switches += 1;
}
}
PhyControllerEvent::UpdateComplete(result)
}
pub fn reset(&mut self) {
self.state = PhyControllerState::Idle;
self.tx_phy = BlePhy::Le1M;
self.rx_phy = BlePhy::Le1M;
self.peer_caps = PhyCapabilities::default();
self.rssi_samples.clear();
self.last_switch_time = 0;
}
pub fn set_strategy(&mut self, strategy: PhyStrategy) {
self.config.strategy = strategy;
}
pub fn set_auto_switch(&mut self, enabled: bool) {
self.config.auto_switch = enabled;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_controller() -> PhyController {
let caps = PhyCapabilities::ble5_full();
PhyController::with_defaults(caps)
}
#[test]
fn test_controller_creation() {
let ctrl = make_controller();
assert_eq!(ctrl.state(), PhyControllerState::Idle);
assert_eq!(ctrl.tx_phy(), BlePhy::Le1M);
assert_eq!(ctrl.rx_phy(), BlePhy::Le1M);
}
#[test]
fn test_negotiation_flow() {
let mut ctrl = make_controller();
ctrl.start_negotiation();
assert_eq!(ctrl.state(), PhyControllerState::Negotiating);
let event = ctrl.complete_negotiation(PhyCapabilities::ble5_full());
assert_eq!(ctrl.state(), PhyControllerState::Active);
if let PhyControllerEvent::NegotiationComplete { local, peer } = event {
assert!(local.le_2m);
assert!(peer.le_coded);
} else {
panic!("Expected NegotiationComplete event");
}
}
#[test]
fn test_effective_capabilities() {
let mut ctrl = make_controller();
ctrl.complete_negotiation(PhyCapabilities::ble5_no_coded());
let effective = ctrl.effective_capabilities();
assert!(effective.le_2m);
assert!(!effective.le_coded); }
#[test]
fn test_rssi_recording() {
let mut ctrl = make_controller();
ctrl.complete_negotiation(PhyCapabilities::ble5_full());
for i in 0..5 {
ctrl.record_rssi(-50 - i, 1000 + i as u64 * 100);
}
let avg = ctrl.average_rssi();
assert!((-55..=-50).contains(&avg));
}
#[test]
fn test_rssi_window_limit() {
let mut ctrl = make_controller();
ctrl.complete_negotiation(PhyCapabilities::ble5_full());
for i in 0..20 {
ctrl.record_rssi(-50, i * 100);
}
assert_eq!(ctrl.rssi_samples.len(), ctrl.config.rssi_window_size);
}
#[test]
fn test_switch_request() {
let mut ctrl = make_controller();
ctrl.complete_negotiation(PhyCapabilities::ble5_full());
let pref = ctrl.request_switch(BlePhy::Le2M);
assert!(pref.is_some());
assert_eq!(ctrl.state(), PhyControllerState::Switching);
}
#[test]
fn test_switch_request_unsupported() {
let mut ctrl = make_controller();
ctrl.complete_negotiation(PhyCapabilities::le_1m_only());
let pref = ctrl.request_switch(BlePhy::LeCodedS8);
assert!(pref.is_none()); }
#[test]
fn test_update_result_success() {
let mut ctrl = make_controller();
ctrl.complete_negotiation(PhyCapabilities::ble5_full());
ctrl.request_switch(BlePhy::Le2M);
let result = PhyUpdateResult::Success {
tx_phy: BlePhy::Le2M,
rx_phy: BlePhy::Le2M,
};
ctrl.handle_update_result(result, 5000);
assert_eq!(ctrl.state(), PhyControllerState::Active);
assert_eq!(ctrl.tx_phy(), BlePhy::Le2M);
assert_eq!(ctrl.rx_phy(), BlePhy::Le2M);
assert_eq!(ctrl.stats().successful_switches, 1);
}
#[test]
fn test_update_result_rejected() {
let mut ctrl = make_controller();
ctrl.complete_negotiation(PhyCapabilities::ble5_full());
ctrl.request_switch(BlePhy::Le2M);
ctrl.handle_update_result(PhyUpdateResult::Rejected, 5000);
assert_eq!(ctrl.state(), PhyControllerState::Active);
assert_eq!(ctrl.tx_phy(), BlePhy::Le1M); assert_eq!(ctrl.stats().failed_switches, 1);
}
#[test]
fn test_auto_switch_recommendation() {
let config = PhyControllerConfig {
min_samples_for_switch: 3,
switch_cooldown_ms: 0, ..Default::default()
};
let caps = PhyCapabilities::ble5_full();
let mut ctrl = PhyController::new(config, caps);
ctrl.complete_negotiation(PhyCapabilities::ble5_full());
for i in 0..5 {
let event = ctrl.record_rssi(-40, i * 100);
if i >= 2 {
if let Some(PhyControllerEvent::SwitchRecommended { to, .. }) = event {
assert_eq!(to, BlePhy::Le2M);
return; }
}
}
panic!("Expected switch recommendation for strong signal");
}
#[test]
fn test_switch_cooldown() {
let config = PhyControllerConfig {
min_samples_for_switch: 2,
switch_cooldown_ms: 5000,
..Default::default()
};
let caps = PhyCapabilities::ble5_full();
let mut ctrl = PhyController::new(config, caps);
ctrl.complete_negotiation(PhyCapabilities::ble5_full());
ctrl.last_switch_time = 1000;
let event = ctrl.record_rssi(-40, 2000);
assert!(event.is_none());
let event = ctrl.record_rssi(-40, 2100);
assert!(event.is_none()); }
#[test]
fn test_reset() {
let mut ctrl = make_controller();
ctrl.complete_negotiation(PhyCapabilities::ble5_full());
ctrl.record_rssi(-50, 1000);
ctrl.reset();
assert_eq!(ctrl.state(), PhyControllerState::Idle);
assert_eq!(ctrl.tx_phy(), BlePhy::Le1M);
assert!(ctrl.rssi_samples.is_empty());
}
#[test]
fn test_stats_success_rate() {
let mut stats = PhyStats::default();
assert_eq!(stats.success_rate(), 1.0);
stats.switches = 10;
stats.successful_switches = 8;
stats.failed_switches = 2;
assert!((stats.success_rate() - 0.8).abs() < 0.01);
}
#[test]
fn test_stats_record_time() {
let mut stats = PhyStats::default();
stats.record_time(BlePhy::Le1M, 100);
stats.record_time(BlePhy::Le2M, 50);
stats.record_time(BlePhy::LeCodedS8, 200);
assert_eq!(stats.time_in_le1m, 100);
assert_eq!(stats.time_in_le2m, 50);
assert_eq!(stats.time_in_coded, 200);
}
}