1use super::types::{BlePhy, PhyCapabilities};
7
8#[derive(Debug, Clone, PartialEq)]
10pub enum PhyStrategy {
11 Fixed(BlePhy),
13
14 Adaptive {
16 rssi_threshold_high: i8,
18 rssi_threshold_low: i8,
20 hysteresis_db: u8,
22 coded_phy: BlePhy,
24 },
25
26 MaxRange,
28
29 MaxThroughput,
31
32 PowerOptimized {
34 rssi_threshold: i8,
36 },
37}
38
39impl Default for PhyStrategy {
40 fn default() -> Self {
41 PhyStrategy::Adaptive {
42 rssi_threshold_high: -50,
43 rssi_threshold_low: -75,
44 hysteresis_db: 5,
45 coded_phy: BlePhy::LeCodedS2,
46 }
47 }
48}
49
50impl PhyStrategy {
51 pub fn fixed(phy: BlePhy) -> Self {
53 PhyStrategy::Fixed(phy)
54 }
55
56 pub fn adaptive(high_threshold: i8, low_threshold: i8, hysteresis: u8) -> Self {
58 PhyStrategy::Adaptive {
59 rssi_threshold_high: high_threshold,
60 rssi_threshold_low: low_threshold,
61 hysteresis_db: hysteresis,
62 coded_phy: BlePhy::LeCodedS2,
63 }
64 }
65
66 pub fn adaptive_max_range() -> Self {
68 PhyStrategy::Adaptive {
69 rssi_threshold_high: -50,
70 rssi_threshold_low: -70,
71 hysteresis_db: 5,
72 coded_phy: BlePhy::LeCodedS8,
73 }
74 }
75
76 pub fn select_phy(
78 &self,
79 current_phy: BlePhy,
80 rssi: i8,
81 capabilities: &PhyCapabilities,
82 ) -> BlePhy {
83 let selected = match self {
84 PhyStrategy::Fixed(phy) => *phy,
85 PhyStrategy::Adaptive {
86 rssi_threshold_high,
87 rssi_threshold_low,
88 hysteresis_db,
89 coded_phy,
90 } => {
91 let (high_thresh, low_thresh) = if current_phy == BlePhy::Le2M {
93 (
95 *rssi_threshold_high - *hysteresis_db as i8,
96 *rssi_threshold_low,
97 )
98 } else if current_phy.is_coded() {
99 (
101 *rssi_threshold_high,
102 *rssi_threshold_low + *hysteresis_db as i8,
103 )
104 } else {
105 (*rssi_threshold_high, *rssi_threshold_low)
106 };
107
108 if rssi > high_thresh {
109 BlePhy::Le2M
110 } else if rssi < low_thresh {
111 *coded_phy
112 } else {
113 BlePhy::Le1M
114 }
115 }
116 PhyStrategy::MaxRange => {
117 if capabilities.le_coded {
118 BlePhy::LeCodedS8
119 } else {
120 BlePhy::Le1M
121 }
122 }
123 PhyStrategy::MaxThroughput => {
124 if capabilities.le_2m {
125 BlePhy::Le2M
126 } else {
127 BlePhy::Le1M
128 }
129 }
130 PhyStrategy::PowerOptimized { rssi_threshold } => {
131 if rssi > *rssi_threshold && capabilities.le_2m {
132 BlePhy::Le2M } else {
134 BlePhy::Le1M
135 }
136 }
137 };
138
139 if capabilities.supports(selected) {
141 selected
142 } else {
143 BlePhy::Le1M }
145 }
146
147 pub fn name(&self) -> &'static str {
149 match self {
150 PhyStrategy::Fixed(_) => "fixed",
151 PhyStrategy::Adaptive { .. } => "adaptive",
152 PhyStrategy::MaxRange => "max_range",
153 PhyStrategy::MaxThroughput => "max_throughput",
154 PhyStrategy::PowerOptimized { .. } => "power_optimized",
155 }
156 }
157
158 pub fn requires_capability_check(&self) -> bool {
160 !matches!(self, PhyStrategy::Fixed(BlePhy::Le1M))
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum PhySwitchDecision {
167 Keep,
169 Switch(BlePhy),
171}
172
173impl PhySwitchDecision {
174 pub fn should_switch(&self) -> bool {
176 matches!(self, PhySwitchDecision::Switch(_))
177 }
178
179 pub fn target(&self) -> Option<BlePhy> {
181 match self {
182 PhySwitchDecision::Keep => None,
183 PhySwitchDecision::Switch(phy) => Some(*phy),
184 }
185 }
186}
187
188pub fn evaluate_phy_switch(
190 strategy: &PhyStrategy,
191 current_phy: BlePhy,
192 rssi: i8,
193 capabilities: &PhyCapabilities,
194) -> PhySwitchDecision {
195 let recommended = strategy.select_phy(current_phy, rssi, capabilities);
196 if recommended != current_phy {
197 PhySwitchDecision::Switch(recommended)
198 } else {
199 PhySwitchDecision::Keep
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_strategy_default() {
209 let strategy = PhyStrategy::default();
210 assert_eq!(strategy.name(), "adaptive");
211 }
212
213 #[test]
214 fn test_fixed_strategy() {
215 let strategy = PhyStrategy::fixed(BlePhy::LeCodedS8);
216 let caps = PhyCapabilities::ble5_full();
217
218 assert_eq!(
220 strategy.select_phy(BlePhy::Le1M, -30, &caps),
221 BlePhy::LeCodedS8
222 );
223 assert_eq!(
224 strategy.select_phy(BlePhy::Le1M, -90, &caps),
225 BlePhy::LeCodedS8
226 );
227 }
228
229 #[test]
230 fn test_fixed_strategy_capability_fallback() {
231 let strategy = PhyStrategy::fixed(BlePhy::LeCodedS8);
232 let caps = PhyCapabilities::le_1m_only();
233
234 assert_eq!(strategy.select_phy(BlePhy::Le1M, -50, &caps), BlePhy::Le1M);
236 }
237
238 #[test]
239 fn test_adaptive_strong_signal() {
240 let strategy = PhyStrategy::default();
241 let caps = PhyCapabilities::ble5_full();
242
243 assert_eq!(strategy.select_phy(BlePhy::Le1M, -40, &caps), BlePhy::Le2M);
245 }
246
247 #[test]
248 fn test_adaptive_medium_signal() {
249 let strategy = PhyStrategy::default();
250 let caps = PhyCapabilities::ble5_full();
251
252 assert_eq!(strategy.select_phy(BlePhy::Le1M, -60, &caps), BlePhy::Le1M);
254 }
255
256 #[test]
257 fn test_adaptive_weak_signal() {
258 let strategy = PhyStrategy::default();
259 let caps = PhyCapabilities::ble5_full();
260
261 assert!(strategy.select_phy(BlePhy::Le1M, -80, &caps).is_coded());
263 }
264
265 #[test]
266 fn test_adaptive_hysteresis() {
267 let strategy = PhyStrategy::Adaptive {
268 rssi_threshold_high: -50,
269 rssi_threshold_low: -75,
270 hysteresis_db: 5,
271 coded_phy: BlePhy::LeCodedS2,
272 };
273 let caps = PhyCapabilities::ble5_full();
274
275 let from_1m = strategy.select_phy(BlePhy::Le1M, -48, &caps);
279 let from_2m = strategy.select_phy(BlePhy::Le2M, -48, &caps);
280
281 assert_eq!(from_1m, BlePhy::Le2M);
282 assert_eq!(from_2m, BlePhy::Le2M); let at_52_from_1m = strategy.select_phy(BlePhy::Le1M, -52, &caps);
288 let at_52_from_2m = strategy.select_phy(BlePhy::Le2M, -52, &caps);
289
290 assert_eq!(at_52_from_1m, BlePhy::Le1M);
291 assert_eq!(at_52_from_2m, BlePhy::Le2M);
292 }
293
294 #[test]
295 fn test_max_range() {
296 let strategy = PhyStrategy::MaxRange;
297 let caps = PhyCapabilities::ble5_full();
298
299 assert_eq!(
300 strategy.select_phy(BlePhy::Le1M, -30, &caps),
301 BlePhy::LeCodedS8
302 );
303 }
304
305 #[test]
306 fn test_max_range_no_coded() {
307 let strategy = PhyStrategy::MaxRange;
308 let caps = PhyCapabilities::ble5_no_coded();
309
310 assert_eq!(strategy.select_phy(BlePhy::Le1M, -30, &caps), BlePhy::Le1M);
311 }
312
313 #[test]
314 fn test_max_throughput() {
315 let strategy = PhyStrategy::MaxThroughput;
316 let caps = PhyCapabilities::ble5_full();
317
318 assert_eq!(strategy.select_phy(BlePhy::Le1M, -80, &caps), BlePhy::Le2M);
319 }
320
321 #[test]
322 fn test_power_optimized_strong() {
323 let strategy = PhyStrategy::PowerOptimized {
324 rssi_threshold: -55,
325 };
326 let caps = PhyCapabilities::ble5_full();
327
328 assert_eq!(strategy.select_phy(BlePhy::Le1M, -40, &caps), BlePhy::Le2M);
330 }
331
332 #[test]
333 fn test_power_optimized_weak() {
334 let strategy = PhyStrategy::PowerOptimized {
335 rssi_threshold: -55,
336 };
337 let caps = PhyCapabilities::ble5_full();
338
339 assert_eq!(strategy.select_phy(BlePhy::Le1M, -70, &caps), BlePhy::Le1M);
341 }
342
343 #[test]
344 fn test_switch_decision_keep() {
345 let strategy = PhyStrategy::fixed(BlePhy::Le1M);
346 let caps = PhyCapabilities::ble5_full();
347
348 let decision = evaluate_phy_switch(&strategy, BlePhy::Le1M, -50, &caps);
349 assert_eq!(decision, PhySwitchDecision::Keep);
350 assert!(!decision.should_switch());
351 assert!(decision.target().is_none());
352 }
353
354 #[test]
355 fn test_switch_decision_switch() {
356 let strategy = PhyStrategy::MaxThroughput;
357 let caps = PhyCapabilities::ble5_full();
358
359 let decision = evaluate_phy_switch(&strategy, BlePhy::Le1M, -50, &caps);
360 assert_eq!(decision, PhySwitchDecision::Switch(BlePhy::Le2M));
361 assert!(decision.should_switch());
362 assert_eq!(decision.target(), Some(BlePhy::Le2M));
363 }
364
365 #[test]
366 fn test_strategy_names() {
367 assert_eq!(PhyStrategy::fixed(BlePhy::Le1M).name(), "fixed");
368 assert_eq!(PhyStrategy::MaxRange.name(), "max_range");
369 assert_eq!(PhyStrategy::MaxThroughput.name(), "max_throughput");
370 }
371
372 #[test]
373 fn test_requires_capability_check() {
374 assert!(!PhyStrategy::fixed(BlePhy::Le1M).requires_capability_check());
375 assert!(PhyStrategy::fixed(BlePhy::Le2M).requires_capability_check());
376 assert!(PhyStrategy::MaxRange.requires_capability_check());
377 }
378}