Skip to main content

offline_protocol_router/
relay.rs

1//! Relay management for promotion and demotion logic.
2
3use serde::{Deserialize, Serialize};
4
5/// Battery level below which a device stops doing anything for other people,
6/// however willing its configuration is.
7///
8/// This is the hard floor beneath the soft [`RelayConfig::min_battery_for_relay`]:
9/// a charging device, or one configured [`RelayPriority::Always`], is excused
10/// the soft minimum but never this. It is public because the protocol crate
11/// applies the same floor to message forwarding — a device must not keep
12/// carrying traffic at a level that would have stripped it of the relay role,
13/// and two copies of the number would eventually disagree.
14pub const CRITICAL_RELAY_BATTERY_LEVEL: u8 = 15;
15
16/// Configuration for relay behavior.
17#[derive(Debug, Clone)]
18pub struct RelayConfig {
19    /// Minimum battery level to act as relay (percentage).
20    pub min_battery_for_relay: u8,
21
22    /// Whether this device allows acting as a relay.
23    pub allow_relay: bool,
24
25    /// Priority for relay selection (higher = more preferred).
26    pub relay_priority: RelayPriority,
27}
28
29impl Default for RelayConfig {
30    fn default() -> Self {
31        Self {
32            min_battery_for_relay: 30,
33            allow_relay: true,
34            relay_priority: RelayPriority::Auto,
35        }
36    }
37}
38
39/// Relay priority levels.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "lowercase")]
42pub enum RelayPriority {
43    /// Never act as relay.
44    Never,
45    /// Automatically decide based on conditions.
46    Auto,
47    /// Always try to act as relay (if conditions met).
48    Always,
49}
50
51/// Relay role of a device.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum RelayRole {
55    /// Regular device (not a relay).
56    Regular,
57    /// Acting as a relay.
58    Relay,
59}
60
61/// Information about a potential relay.
62#[derive(Debug, Clone)]
63pub struct RelayInfo {
64    /// Number of connections this device has.
65    pub connection_count: usize,
66
67    /// Battery level (0-100).
68    pub battery_level: u8,
69
70    /// Whether the device is charging.
71    pub is_charging: bool,
72
73    /// Current relay role.
74    pub role: RelayRole,
75
76    /// Link quality to this relay (0-100).
77    pub link_quality: u8,
78
79    /// Current queue depth at this relay.
80    pub queue_depth: usize,
81
82    /// Congestion level at this relay (0.0-1.0).
83    pub congestion_level: f32,
84}
85
86/// Scores relay candidates.
87///
88/// Holds no role of its own: whether *this* device is acting as a relay is
89/// answered by the forwarding governor from what the device has actually
90/// carried, not predicted here from thresholds. What remains is the
91/// comparison of *other* devices as onward candidates.
92pub struct RelayManager;
93
94impl RelayManager {
95    /// Creates a relay manager.
96    pub fn new() -> Self {
97        Self
98    }
99
100    /// Calculates a score for a potential relay.
101    ///
102    /// Higher score = better relay candidate.
103    pub fn calculate_relay_score(&self, relay: &RelayInfo) -> f32 {
104        let mut score = 0.0;
105
106        // Connection count factor (0-30 points)
107        let connection_score = (relay.connection_count as f32 / 10.0 * 30.0).min(30.0);
108        score += connection_score;
109
110        // Battery level factor (0-20 points)
111        let battery_score = relay.battery_level as f32 / 100.0 * 20.0;
112        score += battery_score;
113
114        // Charging bonus (20 points)
115        if relay.is_charging {
116            score += 20.0;
117        }
118
119        // Link quality factor (0-20 points)
120        let link_score = relay.link_quality as f32 / 100.0 * 20.0;
121        score += link_score;
122
123        // Congestion penalty (0-15 points deduction)
124        let congestion_penalty = relay.congestion_level * 15.0;
125        score -= congestion_penalty;
126
127        // Queue depth penalty (0-15 points deduction)
128        let queue_penalty = (relay.queue_depth as f32 / 50.0 * 15.0).min(15.0);
129        score -= queue_penalty;
130
131        score.max(0.0)
132    }
133
134    /// Selects the best relays from a list of candidates.
135    ///
136    /// # Arguments
137    ///
138    /// * `candidates` - List of potential relays
139    /// * `count` - Number of relays to select
140    /// * `max_congestion_level` - Congestion level (0.0-1.0) at or above
141    ///   which a candidate is considered overloaded and excluded; pass the
142    ///   caller's path policy (e.g. `PathConfig::max_congestion_level`)
143    ///
144    /// # Returns
145    ///
146    /// Returns up to `count` relay candidates, sorted by score (best first).
147    pub fn select_best_relays(
148        &self,
149        mut candidates: Vec<RelayInfo>,
150        count: usize,
151        max_congestion_level: f32,
152    ) -> Vec<RelayInfo> {
153        // Filter out overloaded relays
154        candidates.retain(|relay| relay.congestion_level < max_congestion_level);
155
156        // Calculate scores and sort
157        candidates.sort_by(|a, b| {
158            let score_a = self.calculate_relay_score(a);
159            let score_b = self.calculate_relay_score(b);
160            score_b
161                .partial_cmp(&score_a)
162                .unwrap_or(std::cmp::Ordering::Equal)
163        });
164
165        // Take top N
166        candidates.into_iter().take(count).collect()
167    }
168}
169
170impl Default for RelayManager {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_relay_scoring() {
182        let manager = RelayManager::new();
183
184        let good_relay = RelayInfo {
185            connection_count: 8,
186            battery_level: 80,
187            is_charging: true,
188            role: RelayRole::Relay,
189            link_quality: 90,
190            queue_depth: 5,
191            congestion_level: 0.1,
192        };
193
194        let poor_relay = RelayInfo {
195            connection_count: 3,
196            battery_level: 25,
197            is_charging: false,
198            role: RelayRole::Regular,
199            link_quality: 40,
200            queue_depth: 60,
201            congestion_level: 0.8,
202        };
203
204        let good_score = manager.calculate_relay_score(&good_relay);
205        let poor_score = manager.calculate_relay_score(&poor_relay);
206
207        assert!(good_score > poor_score);
208        assert!(good_score > 70.0); // Good relay should score high
209    }
210
211    #[test]
212    fn test_select_best_relays() {
213        let manager = RelayManager::new();
214
215        let relays = vec![
216            RelayInfo {
217                connection_count: 8,
218                battery_level: 80,
219                is_charging: true,
220                role: RelayRole::Relay,
221                link_quality: 90,
222                queue_depth: 5,
223                congestion_level: 0.1,
224            },
225            RelayInfo {
226                connection_count: 5,
227                battery_level: 60,
228                is_charging: false,
229                role: RelayRole::Relay,
230                link_quality: 70,
231                queue_depth: 15,
232                congestion_level: 0.3,
233            },
234            RelayInfo {
235                connection_count: 3,
236                battery_level: 40,
237                is_charging: false,
238                role: RelayRole::Regular,
239                link_quality: 50,
240                queue_depth: 40,
241                congestion_level: 0.6,
242            },
243        ];
244
245        let best = manager.select_best_relays(relays, 2, 0.7);
246        assert_eq!(best.len(), 2);
247
248        // First relay should be the best one
249        assert_eq!(best[0].connection_count, 8);
250    }
251
252    #[test]
253    fn test_select_best_relays_honors_congestion_threshold() {
254        let manager = RelayManager::new();
255        let candidate = |congestion_level: f32| RelayInfo {
256            connection_count: 5,
257            battery_level: 60,
258            is_charging: false,
259            role: RelayRole::Relay,
260            link_quality: 70,
261            queue_depth: 10,
262            congestion_level,
263        };
264
265        // A 0.6-congestion candidate passes the default-style 0.7 threshold
266        // but must be excluded under a stricter 0.5 policy.
267        let best = manager.select_best_relays(vec![candidate(0.6)], 1, 0.7);
268        assert_eq!(best.len(), 1);
269
270        let best = manager.select_best_relays(vec![candidate(0.6)], 1, 0.5);
271        assert!(best.is_empty());
272    }
273}