Skip to main content

peat_mesh/beacon/
config.rs

1use serde::{Deserialize, Serialize};
2
3/// Node mobility type
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5pub enum NodeMobility {
6    /// Static node (command post, fixed infrastructure)
7    Static,
8    /// Mobile node (vehicle, soldier)
9    Mobile,
10    /// Semi-mobile (can relocate but not constantly moving)
11    SemiMobile,
12}
13
14/// Node resource profile
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct NodeResources {
17    /// CPU cores available for mesh operations
18    pub cpu_cores: u8,
19
20    /// Memory available in MB
21    pub memory_mb: u32,
22
23    /// Network bandwidth capacity in Mbps
24    pub bandwidth_mbps: u32,
25
26    /// Current CPU usage percentage (0-100)
27    #[serde(default)]
28    pub cpu_usage_percent: u8,
29
30    /// Current memory usage percentage (0-100)
31    #[serde(default)]
32    pub memory_usage_percent: u8,
33
34    /// Battery level if applicable (0-100, None if AC powered)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub battery_percent: Option<u8>,
37}
38
39impl NodeResources {
40    /// Check if node has sufficient resources to be a parent
41    pub fn can_parent(&self, config: &ParentingRequirements) -> bool {
42        self.cpu_cores >= config.min_cpu_cores
43            && self.memory_mb >= config.min_memory_mb
44            && self.bandwidth_mbps >= config.min_bandwidth_mbps
45            && self.cpu_usage_percent < config.max_cpu_usage_percent
46            && self.memory_usage_percent < config.max_memory_usage_percent
47            && self.has_sufficient_battery(config.min_battery_percent)
48    }
49
50    fn has_sufficient_battery(&self, min_battery: Option<u8>) -> bool {
51        match (self.battery_percent, min_battery) {
52            (Some(battery), Some(min)) => battery >= min,
53            (None, _) => true,       // AC powered, always OK
54            (Some(_), None) => true, // No battery requirement
55        }
56    }
57}
58
59/// Requirements for a node to act as a parent
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ParentingRequirements {
62    pub min_cpu_cores: u8,
63    pub min_memory_mb: u32,
64    pub min_bandwidth_mbps: u32,
65    pub max_cpu_usage_percent: u8,
66    pub max_memory_usage_percent: u8,
67    pub min_battery_percent: Option<u8>,
68}
69
70impl Default for ParentingRequirements {
71    fn default() -> Self {
72        Self {
73            min_cpu_cores: 2,
74            min_memory_mb: 512,
75            min_bandwidth_mbps: 10,
76            max_cpu_usage_percent: 80,
77            max_memory_usage_percent: 80,
78            min_battery_percent: Some(20),
79        }
80    }
81}
82
83/// Node profile combining mobility and resources
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct NodeProfile {
86    /// Mobility type
87    pub mobility: NodeMobility,
88
89    /// Resource capacity
90    pub resources: NodeResources,
91
92    /// Whether this node can act as a parent
93    pub can_parent: bool,
94
95    /// Whether this node prefers to be a leaf (never parent)
96    pub prefer_leaf: bool,
97
98    /// Priority for parent selection (higher = more preferred)
99    pub parent_priority: u8,
100}
101
102impl NodeProfile {
103    /// Create a static node profile (command post, fixed infrastructure)
104    pub fn static_node(resources: NodeResources) -> Self {
105        Self {
106            mobility: NodeMobility::Static,
107            resources,
108            can_parent: true,
109            prefer_leaf: false,
110            parent_priority: 255, // Highest priority
111        }
112    }
113
114    /// Create a mobile node profile (vehicle, soldier)
115    pub fn mobile_node(resources: NodeResources) -> Self {
116        Self {
117            mobility: NodeMobility::Mobile,
118            resources,
119            can_parent: false, // Mobile nodes shouldn't parent by default
120            prefer_leaf: true,
121            parent_priority: 0, // Lowest priority
122        }
123    }
124
125    /// Create a semi-mobile node profile (relocatable but stable)
126    pub fn semi_mobile_node(resources: NodeResources) -> Self {
127        Self {
128            mobility: NodeMobility::SemiMobile,
129            resources,
130            can_parent: true, // Can parent if resources allow
131            prefer_leaf: false,
132            parent_priority: 128, // Medium priority
133        }
134    }
135
136    /// Check if this node should be considered as a parent candidate
137    pub fn is_parent_candidate(&self, requirements: &ParentingRequirements) -> bool {
138        if self.prefer_leaf || !self.can_parent {
139            return false;
140        }
141
142        // Static nodes are always good candidates if they meet resource requirements
143        if self.mobility == NodeMobility::Static {
144            return self.resources.can_parent(requirements);
145        }
146
147        // Mobile nodes should not parent
148        if self.mobility == NodeMobility::Mobile {
149            return false;
150        }
151
152        // Semi-mobile can parent if resources are sufficient
153        self.resources.can_parent(requirements)
154    }
155}
156
157/// Beacon configuration
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct BeaconConfig {
160    /// Geohash precision (5-9, default 7)
161    /// 5 = ~4.9km cells, 6 = ~1.2km, 7 = ~153m, 8 = ~38m, 9 = ~4.8m
162    pub geohash_precision: usize,
163
164    /// Whether to track all beacons regardless of distance
165    pub track_all: bool,
166
167    /// Maximum distance in meters to track (None = unlimited)
168    pub max_distance_meters: Option<f64>,
169
170    /// Broadcast interval
171    pub broadcast_interval_secs: u64,
172
173    /// Beacon TTL (time to live)
174    pub ttl_secs: u64,
175
176    /// Cleanup interval for janitor
177    pub cleanup_interval_secs: u64,
178}
179
180impl Default for BeaconConfig {
181    fn default() -> Self {
182        Self {
183            geohash_precision: 7,
184            track_all: false,
185            max_distance_meters: Some(5000.0), // 5km default
186            broadcast_interval_secs: 5,
187            ttl_secs: 30,
188            cleanup_interval_secs: 5,
189        }
190    }
191}
192
193impl BeaconConfig {
194    /// Tactical field operations - tight proximity, frequent updates
195    pub fn tactical() -> Self {
196        Self {
197            geohash_precision: 8, // ~38m cells
198            track_all: false,
199            max_distance_meters: Some(2000.0), // 2km
200            broadcast_interval_secs: 3,
201            ttl_secs: 15,
202            cleanup_interval_secs: 3,
203        }
204    }
205
206    /// Distributed cloud - no proximity filtering
207    pub fn distributed() -> Self {
208        Self {
209            geohash_precision: 5, // ~4.9km cells (doesn't matter much)
210            track_all: true,
211            max_distance_meters: None,
212            broadcast_interval_secs: 10,
213            ttl_secs: 60,
214            cleanup_interval_secs: 10,
215        }
216    }
217
218    /// Hybrid - balance between proximity and connectivity
219    pub fn hybrid() -> Self {
220        Self {
221            geohash_precision: 6, // ~1.2km cells
222            track_all: false,
223            max_distance_meters: Some(10000.0), // 10km
224            broadcast_interval_secs: 5,
225            ttl_secs: 30,
226            cleanup_interval_secs: 5,
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_node_mobility_types() {
237        let static_resources = NodeResources {
238            cpu_cores: 4,
239            memory_mb: 2048,
240            bandwidth_mbps: 100,
241            cpu_usage_percent: 30,
242            memory_usage_percent: 40,
243            battery_percent: None, // AC powered
244        };
245
246        let mobile_resources = NodeResources {
247            cpu_cores: 2,
248            memory_mb: 512,
249            bandwidth_mbps: 20,
250            cpu_usage_percent: 50,
251            memory_usage_percent: 60,
252            battery_percent: Some(75),
253        };
254
255        let static_profile = NodeProfile::static_node(static_resources);
256        let mobile_profile = NodeProfile::mobile_node(mobile_resources);
257
258        let requirements = ParentingRequirements::default();
259
260        // Static node should be a parent candidate
261        assert!(static_profile.is_parent_candidate(&requirements));
262        assert_eq!(static_profile.parent_priority, 255);
263
264        // Mobile node should NOT be a parent candidate
265        assert!(!mobile_profile.is_parent_candidate(&requirements));
266        assert_eq!(mobile_profile.parent_priority, 0);
267    }
268
269    #[test]
270    fn test_resource_based_parenting() {
271        let low_resources = NodeResources {
272            cpu_cores: 1,
273            memory_mb: 256,
274            bandwidth_mbps: 5,
275            cpu_usage_percent: 90,
276            memory_usage_percent: 85,
277            battery_percent: Some(15),
278        };
279
280        let high_resources = NodeResources {
281            cpu_cores: 8,
282            memory_mb: 4096,
283            bandwidth_mbps: 1000,
284            cpu_usage_percent: 20,
285            memory_usage_percent: 30,
286            battery_percent: None,
287        };
288
289        let requirements = ParentingRequirements::default();
290
291        assert!(!low_resources.can_parent(&requirements));
292        assert!(high_resources.can_parent(&requirements));
293    }
294
295    #[test]
296    fn test_semi_mobile_profile() {
297        let resources = NodeResources {
298            cpu_cores: 4,
299            memory_mb: 1024,
300            bandwidth_mbps: 50,
301            cpu_usage_percent: 40,
302            memory_usage_percent: 50,
303            battery_percent: Some(80),
304        };
305
306        let profile = NodeProfile::semi_mobile_node(resources);
307        let requirements = ParentingRequirements::default();
308
309        // Semi-mobile with good resources should be parent candidate
310        assert!(profile.is_parent_candidate(&requirements));
311        assert_eq!(profile.parent_priority, 128);
312        assert_eq!(profile.mobility, NodeMobility::SemiMobile);
313    }
314
315    #[test]
316    fn test_beacon_config_presets() {
317        let tactical = BeaconConfig::tactical();
318        assert_eq!(tactical.geohash_precision, 8);
319        assert_eq!(tactical.max_distance_meters, Some(2000.0));
320
321        let distributed = BeaconConfig::distributed();
322        assert!(distributed.track_all);
323        assert_eq!(distributed.max_distance_meters, None);
324
325        let hybrid = BeaconConfig::hybrid();
326        assert_eq!(hybrid.geohash_precision, 6);
327        assert_eq!(hybrid.max_distance_meters, Some(10000.0));
328    }
329}