1use crate::interface::InterfaceId;
15use nexcore_chrono::DateTime;
16use serde::{Deserialize, Serialize};
17
18#[non_exhaustive]
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23pub struct TrafficCounters {
24 pub bytes_sent: u64,
26 pub bytes_received: u64,
28 pub packets_sent: u64,
30 pub packets_received: u64,
32 pub packets_dropped: u64,
34 pub errors: u64,
36}
37
38impl TrafficCounters {
39 pub fn total_bytes(&self) -> u64 {
41 self.bytes_sent.saturating_add(self.bytes_received)
42 }
43
44 pub fn total_packets(&self) -> u64 {
46 self.packets_sent.saturating_add(self.packets_received)
47 }
48
49 pub fn packet_loss_rate(&self) -> f64 {
51 let total = self.total_packets().saturating_add(self.packets_dropped);
52 if total == 0 {
53 return 0.0;
54 }
55 #[allow(
57 clippy::as_conversions,
58 reason = "u64 packet counts fit exactly in f64 mantissa for any realistic traffic volume"
59 )]
60 let dropped = self.packets_dropped as f64;
61 #[allow(
62 clippy::as_conversions,
63 reason = "u64 packet counts fit exactly in f64 mantissa for any realistic traffic volume"
64 )]
65 let total_f = total as f64;
66 dropped / total_f
67 }
68
69 pub fn record_sent(&mut self, bytes: u64) {
71 self.bytes_sent = self.bytes_sent.saturating_add(bytes);
72 self.packets_sent = self.packets_sent.saturating_add(1);
73 }
74
75 pub fn record_received(&mut self, bytes: u64) {
77 self.bytes_received = self.bytes_received.saturating_add(bytes);
78 self.packets_received = self.packets_received.saturating_add(1);
79 }
80
81 pub fn record_dropped(&mut self) {
83 self.packets_dropped = self.packets_dropped.saturating_add(1);
84 }
85
86 pub fn record_error(&mut self) {
88 self.errors = self.errors.saturating_add(1);
89 }
90
91 pub fn reset(&mut self) {
93 *self = Self::default();
94 }
95
96 pub fn bytes_sent_human(&self) -> String {
98 format_bytes(self.bytes_sent)
99 }
100
101 pub fn bytes_received_human(&self) -> String {
103 format_bytes(self.bytes_received)
104 }
105}
106
107fn format_bytes(bytes: u64) -> String {
109 const KB: u64 = 1024;
110 const MB: u64 = 1024 * 1024;
111 const GB: u64 = 1024 * 1024 * 1024;
112
113 #[allow(
115 clippy::as_conversions,
116 reason = "display-only conversion; precision loss beyond 2^53 bytes (~8 exabytes) is acceptable for human-readable formatting"
117 )]
118 if bytes >= GB {
119 format!("{:.1} GB", bytes as f64 / GB as f64)
120 } else if bytes >= MB {
121 format!("{:.1} MB", bytes as f64 / MB as f64)
122 } else if bytes >= KB {
123 format!("{:.1} KB", bytes as f64 / KB as f64)
124 } else {
125 format!("{bytes} B")
126 }
127}
128
129#[non_exhaustive]
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct LatencySample {
135 pub rtt_us: u64,
137 pub timestamp: DateTime,
139}
140
141#[non_exhaustive]
145#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
146pub enum ConnectionQuality {
147 Unusable = 0,
149 Poor = 1,
151 Fair = 2,
153 Good = 3,
155 Excellent = 4,
157}
158
159impl ConnectionQuality {
160 pub fn from_metrics(latency_ms: u64, packet_loss_pct: f64) -> Self {
162 if packet_loss_pct > 10.0 || latency_ms > 1000 {
163 Self::Unusable
164 } else if packet_loss_pct > 5.0 || latency_ms > 500 {
165 Self::Poor
166 } else if packet_loss_pct > 2.0 || latency_ms > 200 {
167 Self::Fair
168 } else if packet_loss_pct > 0.5 || latency_ms > 50 {
169 Self::Good
170 } else {
171 Self::Excellent
172 }
173 }
174
175 pub const fn label(&self) -> &'static str {
177 match self {
178 Self::Unusable => "Unusable",
179 Self::Poor => "Poor",
180 Self::Fair => "Fair",
181 Self::Good => "Good",
182 Self::Excellent => "Excellent",
183 }
184 }
185}
186
187#[non_exhaustive]
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct InterfaceMonitor {
193 pub interface_id: InterfaceId,
195 pub counters: TrafficCounters,
197 pub latency_samples: Vec<LatencySample>,
199 pub max_samples: usize,
201 pub started_at: DateTime,
203}
204
205impl InterfaceMonitor {
206 pub fn new(interface_id: InterfaceId) -> Self {
208 Self {
209 interface_id,
210 counters: TrafficCounters::default(),
211 latency_samples: Vec::new(),
212 max_samples: 100,
213 started_at: DateTime::now(),
214 }
215 }
216
217 pub fn record_latency(&mut self, rtt_us: u64) {
219 if self.latency_samples.len() >= self.max_samples {
220 self.latency_samples.remove(0);
221 }
222 self.latency_samples.push(LatencySample {
223 rtt_us,
224 timestamp: DateTime::now(),
225 });
226 }
227
228 pub fn avg_latency_us(&self) -> Option<u64> {
230 if self.latency_samples.is_empty() {
231 return None;
232 }
233 let sum: u64 = self.latency_samples.iter().map(|s| s.rtt_us).sum();
234 let count = u64::try_from(self.latency_samples.len())
236 .unwrap_or(1)
237 .max(1);
238 #[allow(
239 clippy::arithmetic_side_effects,
240 reason = "count is derived from try_from with a minimum of 1, so division is always safe — no division by zero, no overflow"
241 )]
242 Some(sum / count)
243 }
244
245 pub fn avg_latency_ms(&self) -> Option<u64> {
247 self.avg_latency_us().map(|us| us / 1000)
248 }
249
250 pub fn min_latency_us(&self) -> Option<u64> {
252 self.latency_samples.iter().map(|s| s.rtt_us).min()
253 }
254
255 pub fn max_latency_us(&self) -> Option<u64> {
257 self.latency_samples.iter().map(|s| s.rtt_us).max()
258 }
259
260 pub fn quality(&self) -> ConnectionQuality {
262 let latency_ms = self.avg_latency_ms().unwrap_or(0);
263 let loss = self.counters.packet_loss_rate() * 100.0;
264 ConnectionQuality::from_metrics(latency_ms, loss)
265 }
266
267 pub fn summary(&self) -> String {
269 let latency = self
270 .avg_latency_ms()
271 .map_or("N/A".to_string(), |ms| format!("{ms}ms"));
272 format!(
273 "{}: ↑{} ↓{} latency={} quality={}",
274 self.interface_id.as_str(),
275 self.counters.bytes_sent_human(),
276 self.counters.bytes_received_human(),
277 latency,
278 self.quality().label(),
279 )
280 }
281}
282
283#[derive(Debug, Default)]
287pub struct NetworkMonitor {
288 monitors: Vec<InterfaceMonitor>,
290}
291
292impl NetworkMonitor {
293 pub fn new() -> Self {
295 Self::default()
296 }
297
298 pub fn add_interface(&mut self, interface_id: InterfaceId) {
300 if !self.monitors.iter().any(|m| m.interface_id == interface_id) {
301 self.monitors.push(InterfaceMonitor::new(interface_id));
302 }
303 }
304
305 pub fn remove_interface(&mut self, interface_id: &InterfaceId) {
307 self.monitors.retain(|m| &m.interface_id != interface_id);
308 }
309
310 pub fn get(&self, interface_id: &InterfaceId) -> Option<&InterfaceMonitor> {
312 self.monitors
313 .iter()
314 .find(|m| &m.interface_id == interface_id)
315 }
316
317 pub fn get_mut(&mut self, interface_id: &InterfaceId) -> Option<&mut InterfaceMonitor> {
319 self.monitors
320 .iter_mut()
321 .find(|m| &m.interface_id == interface_id)
322 }
323
324 pub fn total_bytes(&self) -> u64 {
326 self.monitors.iter().map(|m| m.counters.total_bytes()).sum()
327 }
328
329 pub fn total_bytes_sent(&self) -> u64 {
331 self.monitors.iter().map(|m| m.counters.bytes_sent).sum()
332 }
333
334 pub fn total_bytes_received(&self) -> u64 {
336 self.monitors
337 .iter()
338 .map(|m| m.counters.bytes_received)
339 .sum()
340 }
341
342 pub fn interface_count(&self) -> usize {
344 self.monitors.len()
345 }
346
347 pub fn summary(&self) -> String {
349 format!(
350 "Network: {} interfaces, ↑{} ↓{}",
351 self.monitors.len(),
352 format_bytes(self.total_bytes_sent()),
353 format_bytes(self.total_bytes_received()),
354 )
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 fn eth0() -> InterfaceId {
363 InterfaceId::new("eth0")
364 }
365
366 fn wlan0() -> InterfaceId {
367 InterfaceId::new("wlan0")
368 }
369
370 #[test]
371 fn traffic_counters_default() {
372 let c = TrafficCounters::default();
373 assert_eq!(c.total_bytes(), 0);
374 assert_eq!(c.total_packets(), 0);
375 assert!((c.packet_loss_rate() - 0.0).abs() < f64::EPSILON);
376 }
377
378 #[test]
379 fn traffic_counters_record() {
380 let mut c = TrafficCounters::default();
381 c.record_sent(1000);
382 c.record_received(2000);
383 assert_eq!(c.bytes_sent, 1000);
384 assert_eq!(c.bytes_received, 2000);
385 assert_eq!(c.packets_sent, 1);
386 assert_eq!(c.packets_received, 1);
387 assert_eq!(c.total_bytes(), 3000);
388 }
389
390 #[test]
391 fn traffic_counters_packet_loss() {
392 let mut c = TrafficCounters::default();
393 for _ in 0..9 {
394 c.record_sent(100);
395 }
396 c.record_dropped();
397 let rate = c.packet_loss_rate();
399 assert!((rate - 0.1).abs() < 0.01);
400 }
401
402 #[test]
403 fn traffic_counters_reset() {
404 let mut c = TrafficCounters::default();
405 c.record_sent(1000);
406 c.record_received(2000);
407 c.reset();
408 assert_eq!(c.total_bytes(), 0);
409 }
410
411 #[test]
412 fn format_bytes_display() {
413 assert_eq!(format_bytes(500), "500 B");
414 assert_eq!(format_bytes(1500), "1.5 KB");
415 assert_eq!(format_bytes(1_500_000), "1.4 MB");
416 assert_eq!(format_bytes(1_500_000_000), "1.4 GB");
417 }
418
419 #[test]
420 fn connection_quality_from_metrics() {
421 assert_eq!(
422 ConnectionQuality::from_metrics(10, 0.0),
423 ConnectionQuality::Excellent
424 );
425 assert_eq!(
426 ConnectionQuality::from_metrics(100, 1.0),
427 ConnectionQuality::Good
428 );
429 assert_eq!(
430 ConnectionQuality::from_metrics(300, 3.0),
431 ConnectionQuality::Fair
432 );
433 assert_eq!(
434 ConnectionQuality::from_metrics(700, 6.0),
435 ConnectionQuality::Poor
436 );
437 assert_eq!(
438 ConnectionQuality::from_metrics(2000, 15.0),
439 ConnectionQuality::Unusable
440 );
441 }
442
443 #[test]
444 fn connection_quality_ordering() {
445 assert!(ConnectionQuality::Excellent > ConnectionQuality::Good);
446 assert!(ConnectionQuality::Good > ConnectionQuality::Fair);
447 assert!(ConnectionQuality::Fair > ConnectionQuality::Poor);
448 assert!(ConnectionQuality::Poor > ConnectionQuality::Unusable);
449 }
450
451 #[test]
452 fn interface_monitor_latency() {
453 let mut m = InterfaceMonitor::new(eth0());
454 m.record_latency(10_000); m.record_latency(20_000); m.record_latency(30_000); assert_eq!(m.avg_latency_us(), Some(20_000));
459 assert_eq!(m.avg_latency_ms(), Some(20));
460 assert_eq!(m.min_latency_us(), Some(10_000));
461 assert_eq!(m.max_latency_us(), Some(30_000));
462 }
463
464 #[test]
465 fn interface_monitor_no_latency() {
466 let m = InterfaceMonitor::new(eth0());
467 assert!(m.avg_latency_us().is_none());
468 assert!(m.avg_latency_ms().is_none());
469 }
470
471 #[test]
472 fn interface_monitor_quality() {
473 let mut m = InterfaceMonitor::new(eth0());
474 m.record_latency(5_000); assert_eq!(m.quality(), ConnectionQuality::Excellent);
476 }
477
478 #[test]
479 fn interface_monitor_ring_buffer() {
480 let mut m = InterfaceMonitor::new(eth0());
481 m.max_samples = 3;
482 for i in 0..5 {
483 m.record_latency(i * 1000);
484 }
485 assert_eq!(m.latency_samples.len(), 3);
486 assert_eq!(m.latency_samples[0].rtt_us, 2000);
488 }
489
490 #[test]
491 fn network_monitor_add_remove() {
492 let mut nm = NetworkMonitor::new();
493 nm.add_interface(eth0());
494 nm.add_interface(wlan0());
495 assert_eq!(nm.interface_count(), 2);
496
497 nm.remove_interface(ð0());
498 assert_eq!(nm.interface_count(), 1);
499 }
500
501 #[test]
502 fn network_monitor_no_duplicates() {
503 let mut nm = NetworkMonitor::new();
504 nm.add_interface(eth0());
505 nm.add_interface(eth0()); assert_eq!(nm.interface_count(), 1);
507 }
508
509 #[test]
510 fn network_monitor_total_bytes() {
511 let mut nm = NetworkMonitor::new();
512 nm.add_interface(eth0());
513 nm.add_interface(wlan0());
514
515 if let Some(m) = nm.get_mut(ð0()) {
516 m.counters.record_sent(1000);
517 m.counters.record_received(2000);
518 }
519 if let Some(m) = nm.get_mut(&wlan0()) {
520 m.counters.record_sent(500);
521 m.counters.record_received(1500);
522 }
523
524 assert_eq!(nm.total_bytes_sent(), 1500);
525 assert_eq!(nm.total_bytes_received(), 3500);
526 assert_eq!(nm.total_bytes(), 5000);
527 }
528
529 #[test]
530 fn network_monitor_summary() {
531 let nm = NetworkMonitor::new();
532 let s = nm.summary();
533 assert!(s.contains("Network"));
534 assert!(s.contains("0 interfaces"));
535 }
536
537 #[test]
538 fn interface_monitor_summary() {
539 let m = InterfaceMonitor::new(eth0());
540 let s = m.summary();
541 assert!(s.contains("eth0"));
542 }
543}