use serde::{Deserialize, Serialize};
pub const CRITICAL_RELAY_BATTERY_LEVEL: u8 = 15;
#[derive(Debug, Clone)]
pub struct RelayConfig {
pub relay_threshold: usize,
pub min_battery_for_relay: u8,
pub allow_relay: bool,
pub relay_priority: RelayPriority,
}
impl Default for RelayConfig {
fn default() -> Self {
Self {
relay_threshold: 3,
min_battery_for_relay: 30,
allow_relay: true,
relay_priority: RelayPriority::Auto,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RelayPriority {
Never,
Auto,
Always,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RelayRole {
Regular,
Relay,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelayDemotionReason {
LowConnections,
LowBattery {
min_required: u8,
},
RelayDisallowed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelayTransition {
Promoted {
connection_count: usize,
battery_level: u8,
},
Demoted(RelayDemotionReason),
}
#[derive(Debug, Clone)]
pub struct RelayInfo {
pub connection_count: usize,
pub battery_level: u8,
pub is_charging: bool,
pub role: RelayRole,
pub link_quality: u8,
pub queue_depth: usize,
pub congestion_level: f32,
}
pub struct RelayManager {
config: RelayConfig,
current_role: RelayRole,
}
impl RelayManager {
pub fn new() -> Self {
Self::with_config(RelayConfig::default())
}
pub fn with_config(config: RelayConfig) -> Self {
Self {
config,
current_role: RelayRole::Regular,
}
}
pub fn should_promote_to_relay(
&self,
connection_count: usize,
battery_level: u8,
is_charging: bool,
) -> bool {
if self.relaying_disallowed() {
return false;
}
if connection_count < self.config.relay_threshold {
return false;
}
if battery_level < 15 {
return false;
}
if battery_level < self.config.min_battery_for_relay && !is_charging {
return matches!(self.config.relay_priority, RelayPriority::Always);
}
if is_charging {
return true;
}
matches!(
self.config.relay_priority,
RelayPriority::Auto | RelayPriority::Always
)
}
pub fn should_demote_from_relay(
&self,
connection_count: usize,
battery_level: u8,
is_charging: bool,
) -> bool {
if self.relaying_disallowed() {
return true;
}
if matches!(self.config.relay_priority, RelayPriority::Always) {
return battery_level < CRITICAL_RELAY_BATTERY_LEVEL;
}
if connection_count < self.config.relay_threshold {
return true;
}
if battery_level < self.demotion_battery_floor(is_charging) {
return true;
}
false
}
fn relaying_disallowed(&self) -> bool {
!self.config.allow_relay || matches!(self.config.relay_priority, RelayPriority::Never)
}
fn demotion_battery_floor(&self, is_charging: bool) -> u8 {
if matches!(self.config.relay_priority, RelayPriority::Always) || is_charging {
CRITICAL_RELAY_BATTERY_LEVEL
} else {
self.config.min_battery_for_relay
}
}
pub fn evaluate_transition(
&mut self,
connection_count: usize,
battery_level: u8,
is_charging: bool,
) -> Option<RelayTransition> {
match self.current_role {
RelayRole::Regular => {
if self.should_promote_to_relay(connection_count, battery_level, is_charging) {
self.set_role(RelayRole::Relay);
Some(RelayTransition::Promoted {
connection_count,
battery_level,
})
} else {
None
}
}
RelayRole::Relay => {
if self.should_demote_from_relay(connection_count, battery_level, is_charging) {
self.set_role(RelayRole::Regular);
let floor = self.demotion_battery_floor(is_charging);
let reason = if self.relaying_disallowed() {
RelayDemotionReason::RelayDisallowed
} else if battery_level < floor {
RelayDemotionReason::LowBattery {
min_required: floor,
}
} else {
RelayDemotionReason::LowConnections
};
Some(RelayTransition::Demoted(reason))
} else {
None
}
}
}
}
pub fn calculate_relay_score(&self, relay: &RelayInfo) -> f32 {
let mut score = 0.0;
let connection_score = (relay.connection_count as f32 / 10.0 * 30.0).min(30.0);
score += connection_score;
let battery_score = relay.battery_level as f32 / 100.0 * 20.0;
score += battery_score;
if relay.is_charging {
score += 20.0;
}
let link_score = relay.link_quality as f32 / 100.0 * 20.0;
score += link_score;
let congestion_penalty = relay.congestion_level * 15.0;
score -= congestion_penalty;
let queue_penalty = (relay.queue_depth as f32 / 50.0 * 15.0).min(15.0);
score -= queue_penalty;
score.max(0.0)
}
pub fn select_best_relays(
&self,
mut candidates: Vec<RelayInfo>,
count: usize,
max_congestion_level: f32,
) -> Vec<RelayInfo> {
candidates.retain(|relay| relay.congestion_level < max_congestion_level);
candidates.sort_by(|a, b| {
let score_a = self.calculate_relay_score(a);
let score_b = self.calculate_relay_score(b);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
candidates.into_iter().take(count).collect()
}
pub fn current_role(&self) -> RelayRole {
self.current_role
}
pub fn set_role(&mut self, role: RelayRole) {
self.current_role = role;
}
pub fn config(&self) -> &RelayConfig {
&self.config
}
}
impl Default for RelayManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relay_promotion_basic() {
let manager = RelayManager::new();
assert!(manager.should_promote_to_relay(5, 80, false));
assert!(!manager.should_promote_to_relay(2, 80, false));
assert!(!manager.should_promote_to_relay(5, 10, false));
}
#[test]
fn test_relay_promotion_charging_preferred() {
let manager = RelayManager::new();
assert!(manager.should_promote_to_relay(3, 25, true));
}
#[test]
fn test_relay_demotion() {
let manager = RelayManager::new();
assert!(manager.should_demote_from_relay(2, 80, false));
assert!(manager.should_demote_from_relay(5, 25, false));
assert!(!manager.should_demote_from_relay(5, 80, false));
}
#[test]
fn test_should_demote_charging_exempt_from_soft_floor() {
let manager = RelayManager::new();
assert!(manager.should_demote_from_relay(5, 25, false));
assert!(!manager.should_demote_from_relay(5, 25, true));
assert!(manager.should_demote_from_relay(5, 10, true));
}
#[test]
fn test_evaluate_transition_promote_then_idempotent() {
let mut manager = RelayManager::new();
assert_eq!(manager.current_role(), RelayRole::Regular);
assert_eq!(
manager.evaluate_transition(5, 80, false),
Some(RelayTransition::Promoted {
connection_count: 5,
battery_level: 80,
})
);
assert_eq!(manager.current_role(), RelayRole::Relay);
assert_eq!(manager.evaluate_transition(5, 80, false), None);
assert_eq!(manager.current_role(), RelayRole::Relay);
}
#[test]
fn test_evaluate_transition_no_promote_stays_regular() {
let mut manager = RelayManager::new();
assert_eq!(manager.evaluate_transition(1, 80, false), None);
assert_eq!(manager.current_role(), RelayRole::Regular);
}
#[test]
fn test_evaluate_transition_demote_low_connections() {
let mut manager = RelayManager::new();
manager.set_role(RelayRole::Relay);
assert_eq!(
manager.evaluate_transition(1, 80, false),
Some(RelayTransition::Demoted(
RelayDemotionReason::LowConnections
))
);
assert_eq!(manager.current_role(), RelayRole::Regular);
}
#[test]
fn test_evaluate_transition_demote_low_battery() {
let mut manager = RelayManager::new(); manager.set_role(RelayRole::Relay);
assert_eq!(
manager.evaluate_transition(5, 25, false),
Some(RelayTransition::Demoted(RelayDemotionReason::LowBattery {
min_required: 30,
}))
);
assert_eq!(manager.current_role(), RelayRole::Regular);
}
#[test]
fn test_evaluate_transition_battery_precedence_over_connections() {
let mut manager = RelayManager::new();
manager.set_role(RelayRole::Relay);
assert_eq!(
manager.evaluate_transition(1, 10, false),
Some(RelayTransition::Demoted(RelayDemotionReason::LowBattery {
min_required: 30,
}))
);
}
#[test]
fn test_evaluate_transition_always_critical_battery_floor() {
let config = RelayConfig {
relay_priority: RelayPriority::Always,
..Default::default()
};
let mut manager = RelayManager::with_config(config);
manager.set_role(RelayRole::Relay);
assert_eq!(
manager.evaluate_transition(1, 10, false),
Some(RelayTransition::Demoted(RelayDemotionReason::LowBattery {
min_required: 15,
}))
);
}
#[test]
fn test_evaluate_transition_no_flap_while_charging() {
let mut manager = RelayManager::new();
assert!(matches!(
manager.evaluate_transition(5, 25, true),
Some(RelayTransition::Promoted { .. })
));
assert_eq!(manager.evaluate_transition(5, 25, true), None);
assert_eq!(manager.current_role(), RelayRole::Relay);
}
#[test]
fn test_relay_scoring() {
let manager = RelayManager::new();
let good_relay = RelayInfo {
connection_count: 8,
battery_level: 80,
is_charging: true,
role: RelayRole::Relay,
link_quality: 90,
queue_depth: 5,
congestion_level: 0.1,
};
let poor_relay = RelayInfo {
connection_count: 3,
battery_level: 25,
is_charging: false,
role: RelayRole::Regular,
link_quality: 40,
queue_depth: 60,
congestion_level: 0.8,
};
let good_score = manager.calculate_relay_score(&good_relay);
let poor_score = manager.calculate_relay_score(&poor_relay);
assert!(good_score > poor_score);
assert!(good_score > 70.0); }
#[test]
fn test_select_best_relays() {
let manager = RelayManager::new();
let relays = vec![
RelayInfo {
connection_count: 8,
battery_level: 80,
is_charging: true,
role: RelayRole::Relay,
link_quality: 90,
queue_depth: 5,
congestion_level: 0.1,
},
RelayInfo {
connection_count: 5,
battery_level: 60,
is_charging: false,
role: RelayRole::Relay,
link_quality: 70,
queue_depth: 15,
congestion_level: 0.3,
},
RelayInfo {
connection_count: 3,
battery_level: 40,
is_charging: false,
role: RelayRole::Regular,
link_quality: 50,
queue_depth: 40,
congestion_level: 0.6,
},
];
let best = manager.select_best_relays(relays, 2, 0.7);
assert_eq!(best.len(), 2);
assert_eq!(best[0].connection_count, 8);
}
#[test]
fn test_select_best_relays_honors_congestion_threshold() {
let manager = RelayManager::new();
let candidate = |congestion_level: f32| RelayInfo {
connection_count: 5,
battery_level: 60,
is_charging: false,
role: RelayRole::Relay,
link_quality: 70,
queue_depth: 10,
congestion_level,
};
let best = manager.select_best_relays(vec![candidate(0.6)], 1, 0.7);
assert_eq!(best.len(), 1);
let best = manager.select_best_relays(vec![candidate(0.6)], 1, 0.5);
assert!(best.is_empty());
}
#[test]
fn test_relay_priority_never() {
let config = RelayConfig {
relay_priority: RelayPriority::Never,
..Default::default()
};
let manager = RelayManager::with_config(config);
assert!(!manager.should_promote_to_relay(10, 100, true));
assert!(manager.should_demote_from_relay(10, 100, true));
}
#[test]
fn test_allow_relay_false_blocks_auto_promotion() {
let config = RelayConfig {
allow_relay: false,
..Default::default() };
let manager = RelayManager::with_config(config);
assert!(!manager.should_promote_to_relay(10, 100, true));
assert!(!manager.should_promote_to_relay(10, 100, false));
}
#[test]
fn test_allow_relay_false_forces_demotion() {
let config = RelayConfig {
allow_relay: false,
..Default::default()
};
let manager = RelayManager::with_config(config);
assert!(manager.should_demote_from_relay(10, 100, false));
}
#[test]
fn test_allow_relay_false_blocks_always_priority() {
let config = RelayConfig {
allow_relay: false,
relay_priority: RelayPriority::Always,
..Default::default()
};
let manager = RelayManager::with_config(config);
assert!(!manager.should_promote_to_relay(10, 100, true));
assert!(manager.should_demote_from_relay(10, 100, true));
}
#[test]
fn test_evaluate_transition_demote_relay_disallowed() {
let config = RelayConfig {
allow_relay: false,
..Default::default()
};
let mut manager = RelayManager::with_config(config);
manager.set_role(RelayRole::Relay);
assert_eq!(
manager.evaluate_transition(10, 100, false),
Some(RelayTransition::Demoted(
RelayDemotionReason::RelayDisallowed
))
);
assert_eq!(manager.current_role(), RelayRole::Regular);
}
#[test]
fn test_relay_priority_always() {
let config = RelayConfig {
relay_priority: RelayPriority::Always,
..Default::default()
};
let manager = RelayManager::with_config(config);
assert!(manager.should_promote_to_relay(3, 20, false));
assert!(!manager.should_promote_to_relay(3, 10, false));
}
}