use super::types::{BlePhy, PhyCapabilities};
#[derive(Debug, Clone, PartialEq)]
pub enum PhyStrategy {
Fixed(BlePhy),
Adaptive {
rssi_threshold_high: i8,
rssi_threshold_low: i8,
hysteresis_db: u8,
coded_phy: BlePhy,
},
MaxRange,
MaxThroughput,
PowerOptimized {
rssi_threshold: i8,
},
}
impl Default for PhyStrategy {
fn default() -> Self {
PhyStrategy::Adaptive {
rssi_threshold_high: -50,
rssi_threshold_low: -75,
hysteresis_db: 5,
coded_phy: BlePhy::LeCodedS2,
}
}
}
impl PhyStrategy {
pub fn fixed(phy: BlePhy) -> Self {
PhyStrategy::Fixed(phy)
}
pub fn adaptive(high_threshold: i8, low_threshold: i8, hysteresis: u8) -> Self {
PhyStrategy::Adaptive {
rssi_threshold_high: high_threshold,
rssi_threshold_low: low_threshold,
hysteresis_db: hysteresis,
coded_phy: BlePhy::LeCodedS2,
}
}
pub fn adaptive_max_range() -> Self {
PhyStrategy::Adaptive {
rssi_threshold_high: -50,
rssi_threshold_low: -70,
hysteresis_db: 5,
coded_phy: BlePhy::LeCodedS8,
}
}
pub fn select_phy(
&self,
current_phy: BlePhy,
rssi: i8,
capabilities: &PhyCapabilities,
) -> BlePhy {
let selected = match self {
PhyStrategy::Fixed(phy) => *phy,
PhyStrategy::Adaptive {
rssi_threshold_high,
rssi_threshold_low,
hysteresis_db,
coded_phy,
} => {
let (high_thresh, low_thresh) = if current_phy == BlePhy::Le2M {
(
*rssi_threshold_high - *hysteresis_db as i8,
*rssi_threshold_low,
)
} else if current_phy.is_coded() {
(
*rssi_threshold_high,
*rssi_threshold_low + *hysteresis_db as i8,
)
} else {
(*rssi_threshold_high, *rssi_threshold_low)
};
if rssi > high_thresh {
BlePhy::Le2M
} else if rssi < low_thresh {
*coded_phy
} else {
BlePhy::Le1M
}
}
PhyStrategy::MaxRange => {
if capabilities.le_coded {
BlePhy::LeCodedS8
} else {
BlePhy::Le1M
}
}
PhyStrategy::MaxThroughput => {
if capabilities.le_2m {
BlePhy::Le2M
} else {
BlePhy::Le1M
}
}
PhyStrategy::PowerOptimized { rssi_threshold } => {
if rssi > *rssi_threshold && capabilities.le_2m {
BlePhy::Le2M } else {
BlePhy::Le1M
}
}
};
if capabilities.supports(selected) {
selected
} else {
BlePhy::Le1M }
}
pub fn name(&self) -> &'static str {
match self {
PhyStrategy::Fixed(_) => "fixed",
PhyStrategy::Adaptive { .. } => "adaptive",
PhyStrategy::MaxRange => "max_range",
PhyStrategy::MaxThroughput => "max_throughput",
PhyStrategy::PowerOptimized { .. } => "power_optimized",
}
}
pub fn requires_capability_check(&self) -> bool {
!matches!(self, PhyStrategy::Fixed(BlePhy::Le1M))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhySwitchDecision {
Keep,
Switch(BlePhy),
}
impl PhySwitchDecision {
pub fn should_switch(&self) -> bool {
matches!(self, PhySwitchDecision::Switch(_))
}
pub fn target(&self) -> Option<BlePhy> {
match self {
PhySwitchDecision::Keep => None,
PhySwitchDecision::Switch(phy) => Some(*phy),
}
}
}
pub fn evaluate_phy_switch(
strategy: &PhyStrategy,
current_phy: BlePhy,
rssi: i8,
capabilities: &PhyCapabilities,
) -> PhySwitchDecision {
let recommended = strategy.select_phy(current_phy, rssi, capabilities);
if recommended != current_phy {
PhySwitchDecision::Switch(recommended)
} else {
PhySwitchDecision::Keep
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strategy_default() {
let strategy = PhyStrategy::default();
assert_eq!(strategy.name(), "adaptive");
}
#[test]
fn test_fixed_strategy() {
let strategy = PhyStrategy::fixed(BlePhy::LeCodedS8);
let caps = PhyCapabilities::ble5_full();
assert_eq!(
strategy.select_phy(BlePhy::Le1M, -30, &caps),
BlePhy::LeCodedS8
);
assert_eq!(
strategy.select_phy(BlePhy::Le1M, -90, &caps),
BlePhy::LeCodedS8
);
}
#[test]
fn test_fixed_strategy_capability_fallback() {
let strategy = PhyStrategy::fixed(BlePhy::LeCodedS8);
let caps = PhyCapabilities::le_1m_only();
assert_eq!(strategy.select_phy(BlePhy::Le1M, -50, &caps), BlePhy::Le1M);
}
#[test]
fn test_adaptive_strong_signal() {
let strategy = PhyStrategy::default();
let caps = PhyCapabilities::ble5_full();
assert_eq!(strategy.select_phy(BlePhy::Le1M, -40, &caps), BlePhy::Le2M);
}
#[test]
fn test_adaptive_medium_signal() {
let strategy = PhyStrategy::default();
let caps = PhyCapabilities::ble5_full();
assert_eq!(strategy.select_phy(BlePhy::Le1M, -60, &caps), BlePhy::Le1M);
}
#[test]
fn test_adaptive_weak_signal() {
let strategy = PhyStrategy::default();
let caps = PhyCapabilities::ble5_full();
assert!(strategy.select_phy(BlePhy::Le1M, -80, &caps).is_coded());
}
#[test]
fn test_adaptive_hysteresis() {
let strategy = PhyStrategy::Adaptive {
rssi_threshold_high: -50,
rssi_threshold_low: -75,
hysteresis_db: 5,
coded_phy: BlePhy::LeCodedS2,
};
let caps = PhyCapabilities::ble5_full();
let from_1m = strategy.select_phy(BlePhy::Le1M, -48, &caps);
let from_2m = strategy.select_phy(BlePhy::Le2M, -48, &caps);
assert_eq!(from_1m, BlePhy::Le2M);
assert_eq!(from_2m, BlePhy::Le2M);
let at_52_from_1m = strategy.select_phy(BlePhy::Le1M, -52, &caps);
let at_52_from_2m = strategy.select_phy(BlePhy::Le2M, -52, &caps);
assert_eq!(at_52_from_1m, BlePhy::Le1M);
assert_eq!(at_52_from_2m, BlePhy::Le2M);
}
#[test]
fn test_max_range() {
let strategy = PhyStrategy::MaxRange;
let caps = PhyCapabilities::ble5_full();
assert_eq!(
strategy.select_phy(BlePhy::Le1M, -30, &caps),
BlePhy::LeCodedS8
);
}
#[test]
fn test_max_range_no_coded() {
let strategy = PhyStrategy::MaxRange;
let caps = PhyCapabilities::ble5_no_coded();
assert_eq!(strategy.select_phy(BlePhy::Le1M, -30, &caps), BlePhy::Le1M);
}
#[test]
fn test_max_throughput() {
let strategy = PhyStrategy::MaxThroughput;
let caps = PhyCapabilities::ble5_full();
assert_eq!(strategy.select_phy(BlePhy::Le1M, -80, &caps), BlePhy::Le2M);
}
#[test]
fn test_power_optimized_strong() {
let strategy = PhyStrategy::PowerOptimized {
rssi_threshold: -55,
};
let caps = PhyCapabilities::ble5_full();
assert_eq!(strategy.select_phy(BlePhy::Le1M, -40, &caps), BlePhy::Le2M);
}
#[test]
fn test_power_optimized_weak() {
let strategy = PhyStrategy::PowerOptimized {
rssi_threshold: -55,
};
let caps = PhyCapabilities::ble5_full();
assert_eq!(strategy.select_phy(BlePhy::Le1M, -70, &caps), BlePhy::Le1M);
}
#[test]
fn test_switch_decision_keep() {
let strategy = PhyStrategy::fixed(BlePhy::Le1M);
let caps = PhyCapabilities::ble5_full();
let decision = evaluate_phy_switch(&strategy, BlePhy::Le1M, -50, &caps);
assert_eq!(decision, PhySwitchDecision::Keep);
assert!(!decision.should_switch());
assert!(decision.target().is_none());
}
#[test]
fn test_switch_decision_switch() {
let strategy = PhyStrategy::MaxThroughput;
let caps = PhyCapabilities::ble5_full();
let decision = evaluate_phy_switch(&strategy, BlePhy::Le1M, -50, &caps);
assert_eq!(decision, PhySwitchDecision::Switch(BlePhy::Le2M));
assert!(decision.should_switch());
assert_eq!(decision.target(), Some(BlePhy::Le2M));
}
#[test]
fn test_strategy_names() {
assert_eq!(PhyStrategy::fixed(BlePhy::Le1M).name(), "fixed");
assert_eq!(PhyStrategy::MaxRange.name(), "max_range");
assert_eq!(PhyStrategy::MaxThroughput.name(), "max_throughput");
}
#[test]
fn test_requires_capability_check() {
assert!(!PhyStrategy::fixed(BlePhy::Le1M).requires_capability_check());
assert!(PhyStrategy::fixed(BlePhy::Le2M).requires_capability_check());
assert!(PhyStrategy::MaxRange.requires_capability_check());
}
}