Skip to main content

peat_mesh/beacon/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// Geographic position with latitude, longitude, and optional altitude
4#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
5pub struct GeoPosition {
6    /// Latitude in decimal degrees (-90 to 90)
7    pub lat: f64,
8
9    /// Longitude in decimal degrees (-180 to 180)
10    pub lon: f64,
11
12    /// Altitude in meters above sea level (optional)
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub alt: Option<f64>,
15}
16
17impl GeoPosition {
18    pub fn new(lat: f64, lon: f64) -> Self {
19        Self {
20            lat,
21            lon,
22            alt: None,
23        }
24    }
25
26    pub fn with_altitude(mut self, alt: f64) -> Self {
27        self.alt = Some(alt);
28        self
29    }
30
31    /// Calculate Haversine distance to another position in meters
32    pub fn distance_to(&self, other: &GeoPosition) -> f64 {
33        let r = 6371000.0; // Earth radius in meters
34        let lat1 = self.lat.to_radians();
35        let lat2 = other.lat.to_radians();
36        let delta_lat = (other.lat - self.lat).to_radians();
37        let delta_lon = (other.lon - self.lon).to_radians();
38
39        let a = (delta_lat / 2.0).sin().powi(2)
40            + lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
41        let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
42
43        r * c
44    }
45}
46
47/// Hierarchy level in military command structure
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
49pub enum HierarchyLevel {
50    /// Individual platform (vehicle, soldier, etc.)
51    Platform = 0,
52    /// Squad level (typically 4-13 platforms)
53    Squad = 1,
54    /// Platoon level (typically 2-4 squads)
55    Platoon = 2,
56    /// Company level (typically 2-4 platoons)
57    Company = 3,
58}
59
60impl HierarchyLevel {
61    /// Get the parent level in the hierarchy
62    pub fn parent(&self) -> Option<HierarchyLevel> {
63        match self {
64            HierarchyLevel::Platform => Some(HierarchyLevel::Squad),
65            HierarchyLevel::Squad => Some(HierarchyLevel::Platoon),
66            HierarchyLevel::Platoon => Some(HierarchyLevel::Company),
67            HierarchyLevel::Company => None,
68        }
69    }
70
71    /// Get the child level in the hierarchy
72    pub fn child(&self) -> Option<HierarchyLevel> {
73        match self {
74            HierarchyLevel::Platform => None,
75            HierarchyLevel::Squad => Some(HierarchyLevel::Platform),
76            HierarchyLevel::Platoon => Some(HierarchyLevel::Squad),
77            HierarchyLevel::Company => Some(HierarchyLevel::Platoon),
78        }
79    }
80
81    /// Check if this level can be a parent of another level
82    /// A level can be parent if it's at least one level higher in hierarchy
83    pub fn can_be_parent_of(&self, child: &HierarchyLevel) -> bool {
84        self > child
85    }
86}
87
88impl std::fmt::Display for HierarchyLevel {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            HierarchyLevel::Platform => write!(f, "Platform"),
92            HierarchyLevel::Squad => write!(f, "Squad"),
93            HierarchyLevel::Platoon => write!(f, "Platoon"),
94            HierarchyLevel::Company => write!(f, "Company"),
95        }
96    }
97}
98
99/// Geographic beacon representing a node's presence and status
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct GeographicBeacon {
102    /// Unique node identifier
103    pub node_id: String,
104
105    /// Geographic position
106    pub position: GeoPosition,
107
108    /// Geohash encoding of position (precision 7 = ~153m cells)
109    pub geohash: String,
110
111    /// Hierarchy level in command structure
112    pub hierarchy_level: HierarchyLevel,
113
114    /// Node capabilities (e.g., "ai-inference", "sensor-fusion", etc.)
115    #[serde(default)]
116    pub capabilities: Vec<String>,
117
118    /// Whether node is operational
119    pub operational: bool,
120
121    /// Unix timestamp (seconds since epoch)
122    pub timestamp: u64,
123
124    /// Node mobility type
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub mobility: Option<super::config::NodeMobility>,
127
128    /// Whether this node can act as a parent
129    #[serde(default)]
130    pub can_parent: bool,
131
132    /// Parent selection priority (0-255, higher = more preferred)
133    #[serde(default)]
134    pub parent_priority: u8,
135
136    /// Current resource metrics
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub resources: Option<super::config::NodeResources>,
139
140    /// Optional metadata
141    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
142    pub metadata: std::collections::HashMap<String, String>,
143}
144
145impl GeographicBeacon {
146    /// Create a new beacon
147    pub fn new(node_id: String, position: GeoPosition, hierarchy_level: HierarchyLevel) -> Self {
148        use geohash::encode;
149        use geohash::Coord;
150
151        let geohash = encode(
152            Coord {
153                x: position.lon,
154                y: position.lat,
155            },
156            7,
157        )
158        .unwrap_or_else(|_| String::from("invalid"));
159
160        let timestamp = std::time::SystemTime::now()
161            .duration_since(std::time::UNIX_EPOCH)
162            .map(|d| d.as_secs())
163            .unwrap_or(0);
164
165        Self {
166            node_id,
167            position,
168            geohash,
169            hierarchy_level,
170            capabilities: Vec::new(),
171            operational: true,
172            timestamp,
173            mobility: None,
174            can_parent: false,
175            parent_priority: 0,
176            resources: None,
177            metadata: std::collections::HashMap::new(),
178        }
179    }
180
181    /// Update the timestamp to current time
182    pub fn update_timestamp(&mut self) {
183        self.timestamp = std::time::SystemTime::now()
184            .duration_since(std::time::UNIX_EPOCH)
185            .map(|d| d.as_secs())
186            .unwrap_or(0);
187    }
188
189    /// Add a capability
190    pub fn add_capability(&mut self, capability: String) {
191        if !self.capabilities.contains(&capability) {
192            self.capabilities.push(capability);
193        }
194    }
195
196    /// Check if beacon has a specific capability
197    pub fn has_capability(&self, capability: &str) -> bool {
198        self.capabilities.iter().any(|c| c == capability)
199    }
200
201    /// Get age of beacon in seconds
202    pub fn age_seconds(&self) -> u64 {
203        std::time::SystemTime::now()
204            .duration_since(std::time::UNIX_EPOCH)
205            .map(|d| d.as_secs())
206            .unwrap_or(0)
207            .saturating_sub(self.timestamp)
208    }
209
210    /// Check if beacon is expired based on TTL
211    pub fn is_expired(&self, ttl_seconds: u64) -> bool {
212        self.age_seconds() > ttl_seconds
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_geo_position_distance() {
222        // Test distance between two known points (approximately)
223        let pos1 = GeoPosition::new(37.7749, -122.4194); // San Francisco
224        let pos2 = GeoPosition::new(34.0522, -118.2437); // Los Angeles
225
226        let distance = pos1.distance_to(&pos2);
227
228        // Distance should be approximately 559 km
229        assert!(distance > 550_000.0 && distance < 570_000.0);
230    }
231
232    #[test]
233    fn test_hierarchy_level_parent_child() {
234        assert_eq!(
235            HierarchyLevel::Platform.parent(),
236            Some(HierarchyLevel::Squad)
237        );
238        assert_eq!(
239            HierarchyLevel::Squad.parent(),
240            Some(HierarchyLevel::Platoon)
241        );
242        assert_eq!(
243            HierarchyLevel::Platoon.parent(),
244            Some(HierarchyLevel::Company)
245        );
246        assert_eq!(HierarchyLevel::Company.parent(), None);
247
248        assert_eq!(HierarchyLevel::Platform.child(), None);
249        assert_eq!(
250            HierarchyLevel::Squad.child(),
251            Some(HierarchyLevel::Platform)
252        );
253        assert_eq!(HierarchyLevel::Platoon.child(), Some(HierarchyLevel::Squad));
254        assert_eq!(
255            HierarchyLevel::Company.child(),
256            Some(HierarchyLevel::Platoon)
257        );
258    }
259
260    #[test]
261    fn test_geographic_beacon_creation() {
262        let position = GeoPosition::new(37.7749, -122.4194);
263        let beacon = GeographicBeacon::new(
264            "test-node-1".to_string(),
265            position,
266            HierarchyLevel::Platform,
267        );
268
269        assert_eq!(beacon.node_id, "test-node-1");
270        assert_eq!(beacon.position.lat, 37.7749);
271        assert_eq!(beacon.hierarchy_level, HierarchyLevel::Platform);
272        assert!(beacon.operational);
273        assert!(!beacon.geohash.is_empty());
274    }
275
276    #[test]
277    fn test_beacon_capabilities() {
278        let position = GeoPosition::new(37.7749, -122.4194);
279        let mut beacon =
280            GeographicBeacon::new("test-node".to_string(), position, HierarchyLevel::Squad);
281
282        beacon.add_capability("ai-inference".to_string());
283        beacon.add_capability("sensor-fusion".to_string());
284
285        assert!(beacon.has_capability("ai-inference"));
286        assert!(beacon.has_capability("sensor-fusion"));
287        assert!(!beacon.has_capability("non-existent"));
288    }
289
290    #[test]
291    fn test_beacon_serialization() {
292        let position = GeoPosition::new(37.7749, -122.4194);
293        let beacon =
294            GeographicBeacon::new("test-node".to_string(), position, HierarchyLevel::Platform);
295
296        // Test serialization
297        let json = serde_json::to_string(&beacon).unwrap();
298        assert!(json.contains("test-node"));
299        assert!(json.contains("37.7749"));
300
301        // Test deserialization
302        let deserialized: GeographicBeacon = serde_json::from_str(&json).unwrap();
303        assert_eq!(deserialized.node_id, beacon.node_id);
304        assert_eq!(deserialized.position.lat, beacon.position.lat);
305    }
306}