1use serde::{Deserialize, Serialize};
4
5pub const CRITICAL_RELAY_BATTERY_LEVEL: u8 = 15;
15
16#[derive(Debug, Clone)]
18pub struct RelayConfig {
19 pub relay_threshold: usize,
21
22 pub min_battery_for_relay: u8,
24
25 pub allow_relay: bool,
27
28 pub relay_priority: RelayPriority,
30}
31
32impl Default for RelayConfig {
33 fn default() -> Self {
34 Self {
35 relay_threshold: 3,
36 min_battery_for_relay: 30,
37 allow_relay: true,
38 relay_priority: RelayPriority::Auto,
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "lowercase")]
46pub enum RelayPriority {
47 Never,
49 Auto,
51 Always,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum RelayRole {
59 Regular,
61 Relay,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum RelayDemotionReason {
68 LowConnections,
70 LowBattery {
72 min_required: u8,
74 },
75 RelayDisallowed,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum RelayTransition {
87 Promoted {
89 connection_count: usize,
91 battery_level: u8,
93 },
94 Demoted(RelayDemotionReason),
96}
97
98#[derive(Debug, Clone)]
100pub struct RelayInfo {
101 pub connection_count: usize,
103
104 pub battery_level: u8,
106
107 pub is_charging: bool,
109
110 pub role: RelayRole,
112
113 pub link_quality: u8,
115
116 pub queue_depth: usize,
118
119 pub congestion_level: f32,
121}
122
123pub struct RelayManager {
125 config: RelayConfig,
126 current_role: RelayRole,
127}
128
129impl RelayManager {
130 pub fn new() -> Self {
132 Self::with_config(RelayConfig::default())
133 }
134
135 pub fn with_config(config: RelayConfig) -> Self {
137 Self {
138 config,
139 current_role: RelayRole::Regular,
140 }
141 }
142
143 pub fn should_promote_to_relay(
155 &self,
156 connection_count: usize,
157 battery_level: u8,
158 is_charging: bool,
159 ) -> bool {
160 if self.relaying_disallowed() {
163 return false;
164 }
165
166 if connection_count < self.config.relay_threshold {
168 return false;
169 }
170
171 if battery_level < 15 {
173 return false;
175 }
176
177 if battery_level < self.config.min_battery_for_relay && !is_charging {
178 return matches!(self.config.relay_priority, RelayPriority::Always);
180 }
181
182 if is_charging {
184 return true;
185 }
186
187 matches!(
189 self.config.relay_priority,
190 RelayPriority::Auto | RelayPriority::Always
191 )
192 }
193
194 pub fn should_demote_from_relay(
206 &self,
207 connection_count: usize,
208 battery_level: u8,
209 is_charging: bool,
210 ) -> bool {
211 if self.relaying_disallowed() {
214 return true;
215 }
216
217 if matches!(self.config.relay_priority, RelayPriority::Always) {
219 return battery_level < CRITICAL_RELAY_BATTERY_LEVEL;
220 }
221
222 if connection_count < self.config.relay_threshold {
224 return true;
225 }
226
227 if battery_level < self.demotion_battery_floor(is_charging) {
233 return true;
234 }
235
236 false
237 }
238
239 fn relaying_disallowed(&self) -> bool {
244 !self.config.allow_relay || matches!(self.config.relay_priority, RelayPriority::Never)
245 }
246
247 fn demotion_battery_floor(&self, is_charging: bool) -> u8 {
252 if matches!(self.config.relay_priority, RelayPriority::Always) || is_charging {
253 CRITICAL_RELAY_BATTERY_LEVEL
254 } else {
255 self.config.min_battery_for_relay
256 }
257 }
258
259 pub fn evaluate_transition(
267 &mut self,
268 connection_count: usize,
269 battery_level: u8,
270 is_charging: bool,
271 ) -> Option<RelayTransition> {
272 match self.current_role {
273 RelayRole::Regular => {
274 if self.should_promote_to_relay(connection_count, battery_level, is_charging) {
275 self.set_role(RelayRole::Relay);
276 Some(RelayTransition::Promoted {
277 connection_count,
278 battery_level,
279 })
280 } else {
281 None
282 }
283 }
284 RelayRole::Relay => {
285 if self.should_demote_from_relay(connection_count, battery_level, is_charging) {
286 self.set_role(RelayRole::Regular);
287 let floor = self.demotion_battery_floor(is_charging);
295 let reason = if self.relaying_disallowed() {
296 RelayDemotionReason::RelayDisallowed
297 } else if battery_level < floor {
298 RelayDemotionReason::LowBattery {
299 min_required: floor,
300 }
301 } else {
302 RelayDemotionReason::LowConnections
303 };
304 Some(RelayTransition::Demoted(reason))
305 } else {
306 None
307 }
308 }
309 }
310 }
311
312 pub fn calculate_relay_score(&self, relay: &RelayInfo) -> f32 {
316 let mut score = 0.0;
317
318 let connection_score = (relay.connection_count as f32 / 10.0 * 30.0).min(30.0);
320 score += connection_score;
321
322 let battery_score = relay.battery_level as f32 / 100.0 * 20.0;
324 score += battery_score;
325
326 if relay.is_charging {
328 score += 20.0;
329 }
330
331 let link_score = relay.link_quality as f32 / 100.0 * 20.0;
333 score += link_score;
334
335 let congestion_penalty = relay.congestion_level * 15.0;
337 score -= congestion_penalty;
338
339 let queue_penalty = (relay.queue_depth as f32 / 50.0 * 15.0).min(15.0);
341 score -= queue_penalty;
342
343 score.max(0.0)
344 }
345
346 pub fn select_best_relays(
360 &self,
361 mut candidates: Vec<RelayInfo>,
362 count: usize,
363 max_congestion_level: f32,
364 ) -> Vec<RelayInfo> {
365 candidates.retain(|relay| relay.congestion_level < max_congestion_level);
367
368 candidates.sort_by(|a, b| {
370 let score_a = self.calculate_relay_score(a);
371 let score_b = self.calculate_relay_score(b);
372 score_b
373 .partial_cmp(&score_a)
374 .unwrap_or(std::cmp::Ordering::Equal)
375 });
376
377 candidates.into_iter().take(count).collect()
379 }
380
381 pub fn current_role(&self) -> RelayRole {
383 self.current_role
384 }
385
386 pub fn set_role(&mut self, role: RelayRole) {
388 self.current_role = role;
389 }
390
391 pub fn config(&self) -> &RelayConfig {
393 &self.config
394 }
395}
396
397impl Default for RelayManager {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_relay_promotion_basic() {
409 let manager = RelayManager::new();
410
411 assert!(manager.should_promote_to_relay(5, 80, false));
413
414 assert!(!manager.should_promote_to_relay(2, 80, false));
416
417 assert!(!manager.should_promote_to_relay(5, 10, false));
419 }
420
421 #[test]
422 fn test_relay_promotion_charging_preferred() {
423 let manager = RelayManager::new();
424
425 assert!(manager.should_promote_to_relay(3, 25, true));
427 }
428
429 #[test]
430 fn test_relay_demotion() {
431 let manager = RelayManager::new();
432
433 assert!(manager.should_demote_from_relay(2, 80, false));
435
436 assert!(manager.should_demote_from_relay(5, 25, false));
438
439 assert!(!manager.should_demote_from_relay(5, 80, false));
441 }
442
443 #[test]
444 fn test_should_demote_charging_exempt_from_soft_floor() {
445 let manager = RelayManager::new(); assert!(manager.should_demote_from_relay(5, 25, false));
449
450 assert!(!manager.should_demote_from_relay(5, 25, true));
453 assert!(manager.should_demote_from_relay(5, 10, true));
454 }
455
456 #[test]
457 fn test_evaluate_transition_promote_then_idempotent() {
458 let mut manager = RelayManager::new();
459 assert_eq!(manager.current_role(), RelayRole::Regular);
460
461 assert_eq!(
463 manager.evaluate_transition(5, 80, false),
464 Some(RelayTransition::Promoted {
465 connection_count: 5,
466 battery_level: 80,
467 })
468 );
469 assert_eq!(manager.current_role(), RelayRole::Relay);
470
471 assert_eq!(manager.evaluate_transition(5, 80, false), None);
473 assert_eq!(manager.current_role(), RelayRole::Relay);
474 }
475
476 #[test]
477 fn test_evaluate_transition_no_promote_stays_regular() {
478 let mut manager = RelayManager::new();
479 assert_eq!(manager.evaluate_transition(1, 80, false), None);
481 assert_eq!(manager.current_role(), RelayRole::Regular);
482 }
483
484 #[test]
485 fn test_evaluate_transition_demote_low_connections() {
486 let mut manager = RelayManager::new();
487 manager.set_role(RelayRole::Relay);
488
489 assert_eq!(
491 manager.evaluate_transition(1, 80, false),
492 Some(RelayTransition::Demoted(
493 RelayDemotionReason::LowConnections
494 ))
495 );
496 assert_eq!(manager.current_role(), RelayRole::Regular);
497 }
498
499 #[test]
500 fn test_evaluate_transition_demote_low_battery() {
501 let mut manager = RelayManager::new(); manager.set_role(RelayRole::Relay);
503
504 assert_eq!(
507 manager.evaluate_transition(5, 25, false),
508 Some(RelayTransition::Demoted(RelayDemotionReason::LowBattery {
509 min_required: 30,
510 }))
511 );
512 assert_eq!(manager.current_role(), RelayRole::Regular);
513 }
514
515 #[test]
516 fn test_evaluate_transition_battery_precedence_over_connections() {
517 let mut manager = RelayManager::new();
518 manager.set_role(RelayRole::Relay);
519
520 assert_eq!(
522 manager.evaluate_transition(1, 10, false),
523 Some(RelayTransition::Demoted(RelayDemotionReason::LowBattery {
524 min_required: 30,
525 }))
526 );
527 }
528
529 #[test]
530 fn test_evaluate_transition_always_critical_battery_floor() {
531 let config = RelayConfig {
532 relay_priority: RelayPriority::Always,
533 ..Default::default()
534 };
535 let mut manager = RelayManager::with_config(config);
536 manager.set_role(RelayRole::Relay);
537
538 assert_eq!(
540 manager.evaluate_transition(1, 10, false),
541 Some(RelayTransition::Demoted(RelayDemotionReason::LowBattery {
542 min_required: 15,
543 }))
544 );
545 }
546
547 #[test]
548 fn test_evaluate_transition_no_flap_while_charging() {
549 let mut manager = RelayManager::new(); assert!(matches!(
554 manager.evaluate_transition(5, 25, true),
555 Some(RelayTransition::Promoted { .. })
556 ));
557 assert_eq!(manager.evaluate_transition(5, 25, true), None);
558 assert_eq!(manager.current_role(), RelayRole::Relay);
559 }
560
561 #[test]
562 fn test_relay_scoring() {
563 let manager = RelayManager::new();
564
565 let good_relay = RelayInfo {
566 connection_count: 8,
567 battery_level: 80,
568 is_charging: true,
569 role: RelayRole::Relay,
570 link_quality: 90,
571 queue_depth: 5,
572 congestion_level: 0.1,
573 };
574
575 let poor_relay = RelayInfo {
576 connection_count: 3,
577 battery_level: 25,
578 is_charging: false,
579 role: RelayRole::Regular,
580 link_quality: 40,
581 queue_depth: 60,
582 congestion_level: 0.8,
583 };
584
585 let good_score = manager.calculate_relay_score(&good_relay);
586 let poor_score = manager.calculate_relay_score(&poor_relay);
587
588 assert!(good_score > poor_score);
589 assert!(good_score > 70.0); }
591
592 #[test]
593 fn test_select_best_relays() {
594 let manager = RelayManager::new();
595
596 let relays = vec![
597 RelayInfo {
598 connection_count: 8,
599 battery_level: 80,
600 is_charging: true,
601 role: RelayRole::Relay,
602 link_quality: 90,
603 queue_depth: 5,
604 congestion_level: 0.1,
605 },
606 RelayInfo {
607 connection_count: 5,
608 battery_level: 60,
609 is_charging: false,
610 role: RelayRole::Relay,
611 link_quality: 70,
612 queue_depth: 15,
613 congestion_level: 0.3,
614 },
615 RelayInfo {
616 connection_count: 3,
617 battery_level: 40,
618 is_charging: false,
619 role: RelayRole::Regular,
620 link_quality: 50,
621 queue_depth: 40,
622 congestion_level: 0.6,
623 },
624 ];
625
626 let best = manager.select_best_relays(relays, 2, 0.7);
627 assert_eq!(best.len(), 2);
628
629 assert_eq!(best[0].connection_count, 8);
631 }
632
633 #[test]
634 fn test_select_best_relays_honors_congestion_threshold() {
635 let manager = RelayManager::new();
636 let candidate = |congestion_level: f32| RelayInfo {
637 connection_count: 5,
638 battery_level: 60,
639 is_charging: false,
640 role: RelayRole::Relay,
641 link_quality: 70,
642 queue_depth: 10,
643 congestion_level,
644 };
645
646 let best = manager.select_best_relays(vec![candidate(0.6)], 1, 0.7);
649 assert_eq!(best.len(), 1);
650
651 let best = manager.select_best_relays(vec![candidate(0.6)], 1, 0.5);
652 assert!(best.is_empty());
653 }
654
655 #[test]
656 fn test_relay_priority_never() {
657 let config = RelayConfig {
658 relay_priority: RelayPriority::Never,
659 ..Default::default()
660 };
661 let manager = RelayManager::with_config(config);
662
663 assert!(!manager.should_promote_to_relay(10, 100, true));
665
666 assert!(manager.should_demote_from_relay(10, 100, true));
668 }
669
670 #[test]
671 fn test_allow_relay_false_blocks_auto_promotion() {
672 let config = RelayConfig {
673 allow_relay: false,
674 ..Default::default() };
676 let manager = RelayManager::with_config(config);
677
678 assert!(!manager.should_promote_to_relay(10, 100, true));
681 assert!(!manager.should_promote_to_relay(10, 100, false));
682 }
683
684 #[test]
685 fn test_allow_relay_false_forces_demotion() {
686 let config = RelayConfig {
687 allow_relay: false,
688 ..Default::default()
689 };
690 let manager = RelayManager::with_config(config);
691
692 assert!(manager.should_demote_from_relay(10, 100, false));
694 }
695
696 #[test]
697 fn test_allow_relay_false_blocks_always_priority() {
698 let config = RelayConfig {
699 allow_relay: false,
700 relay_priority: RelayPriority::Always,
701 ..Default::default()
702 };
703 let manager = RelayManager::with_config(config);
704
705 assert!(!manager.should_promote_to_relay(10, 100, true));
706 assert!(manager.should_demote_from_relay(10, 100, true));
707 }
708
709 #[test]
710 fn test_evaluate_transition_demote_relay_disallowed() {
711 let config = RelayConfig {
712 allow_relay: false,
713 ..Default::default()
714 };
715 let mut manager = RelayManager::with_config(config);
716 manager.set_role(RelayRole::Relay);
717
718 assert_eq!(
721 manager.evaluate_transition(10, 100, false),
722 Some(RelayTransition::Demoted(
723 RelayDemotionReason::RelayDisallowed
724 ))
725 );
726 assert_eq!(manager.current_role(), RelayRole::Regular);
727 }
728
729 #[test]
730 fn test_relay_priority_always() {
731 let config = RelayConfig {
732 relay_priority: RelayPriority::Always,
733 ..Default::default()
734 };
735 let manager = RelayManager::with_config(config);
736
737 assert!(manager.should_promote_to_relay(3, 20, false));
739
740 assert!(!manager.should_promote_to_relay(3, 10, false));
742 }
743}