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