use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::{Arc, Mutex};
pub type SharedCallback = Arc<Mutex<Option<Arc<dyn Fn() + Send + Sync>>>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TransportType {
Internet,
BLE,
#[serde(rename = "wifiDirect")]
WiFiDirect,
#[serde(rename = "reticulum")]
Reticulum,
#[serde(rename = "nostr")]
Nostr,
}
impl TransportType {
pub fn label(self) -> &'static str {
match self {
TransportType::Internet => "internet",
TransportType::BLE => "ble",
TransportType::WiFiDirect => "wifiDirect",
TransportType::Reticulum => "reticulum",
TransportType::Nostr => "nostr",
}
}
pub fn from_label(label: &str) -> Self {
let normalized = label.to_ascii_lowercase();
match normalized.as_str() {
"internet" => TransportType::Internet,
"wifidirect" | "wifi_direct" | "wifi-direct" => TransportType::WiFiDirect,
"reticulum" => TransportType::Reticulum,
"nostr" => TransportType::Nostr,
_ => TransportType::BLE,
}
}
}
impl fmt::Display for TransportType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TransportMetrics {
pub rssi: Option<i16>,
pub latency_ms: Option<u32>,
pub bandwidth_bps: Option<u64>,
pub congestion: f32,
pub queue_depth: usize,
pub success_count: u32,
pub failure_count: u32,
pub battery_level: Option<u8>,
pub is_charging: bool,
pub relay_connection_count: u8,
pub is_active_relay: bool,
pub delivery_ratio: Option<f32>,
pub drop_rate: Option<f32>,
pub average_hop_count: Option<f32>,
pub energy_cost: Option<f32>,
}
impl Default for TransportMetrics {
fn default() -> Self {
Self {
rssi: None,
latency_ms: None,
bandwidth_bps: None,
congestion: 0.0,
queue_depth: 0,
success_count: 0,
failure_count: 0,
battery_level: None,
is_charging: false,
relay_connection_count: 0,
is_active_relay: false,
delivery_ratio: None,
drop_rate: None,
average_hop_count: None,
energy_cost: None,
}
}
}
impl TransportMetrics {
pub fn effective_delivery_ratio(&self) -> Option<f32> {
if let Some(ratio) = self.delivery_ratio {
return Some(ratio.clamp(0.0, 1.0));
}
let total = self.success_count + self.failure_count;
if total == 0 {
None
} else {
Some((self.success_count as f32 / total as f32).clamp(0.0, 1.0))
}
}
pub fn effective_drop_ratio(&self) -> Option<f32> {
if let Some(rate) = self.drop_rate {
return Some(rate.clamp(0.0, 1.0));
}
let total = self.success_count + self.failure_count;
if total == 0 {
None
} else {
Some((self.failure_count as f32 / total as f32).clamp(0.0, 1.0))
}
}
pub fn sample_count(&self) -> u32 {
self.success_count + self.failure_count
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerLink {
pub peer_id: String,
pub rssi: Option<i16>,
}
impl PeerLink {
pub fn new(peer_id: impl Into<String>) -> Self {
Self {
peer_id: peer_id.into(),
rssi: None,
}
}
pub fn with_rssi(peer_id: impl Into<String>, rssi: i16) -> Self {
Self {
peer_id: peer_id.into(),
rssi: Some(rssi),
}
}
pub fn link_quality(&self) -> LinkQuality {
match self.rssi {
Some(rssi) => LinkQuality::from_rssi(rssi),
None => LinkQuality::new(DEFAULT_LINK_QUALITY_WITHOUT_RSSI),
}
}
}
pub const DEFAULT_LINK_QUALITY_WITHOUT_RSSI: u8 = 50;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct LinkQuality(u8);
impl LinkQuality {
pub const MAX: u8 = 100;
pub const MIN: u8 = 0;
pub fn new(value: u8) -> Self {
Self(value.min(Self::MAX))
}
pub fn value(&self) -> u8 {
self.0
}
pub fn from_rssi(rssi: i16) -> Self {
let quality = if rssi >= -50 {
100
} else if rssi >= -70 {
70 + ((rssi + 70) * 30 / 20) as u8
} else if rssi >= -85 {
40 + ((rssi + 85) * 30 / 15) as u8
} else {
((rssi + 100).max(0) * 40 / 15) as u8
};
Self::new(quality)
}
pub fn is_good(&self) -> bool {
self.0 >= 70
}
pub fn is_poor(&self) -> bool {
self.0 <= 40
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_type_label() {
assert_eq!(TransportType::Internet.label(), "internet");
assert_eq!(TransportType::BLE.label(), "ble");
assert_eq!(TransportType::WiFiDirect.label(), "wifiDirect");
assert_eq!(TransportType::Reticulum.label(), "reticulum");
assert_eq!(TransportType::Nostr.label(), "nostr");
}
#[test]
fn test_transport_type_from_label() {
assert_eq!(
TransportType::from_label("internet"),
TransportType::Internet
);
assert_eq!(
TransportType::from_label("INTERNET"),
TransportType::Internet
);
assert_eq!(TransportType::from_label("ble"), TransportType::BLE);
assert_eq!(TransportType::from_label("BLE"), TransportType::BLE);
assert_eq!(
TransportType::from_label("wifiDirect"),
TransportType::WiFiDirect
);
assert_eq!(
TransportType::from_label("wifi_direct"),
TransportType::WiFiDirect
);
assert_eq!(
TransportType::from_label("wifi-direct"),
TransportType::WiFiDirect
);
assert_eq!(
TransportType::from_label("wifidirect"),
TransportType::WiFiDirect
);
assert_eq!(
TransportType::from_label("reticulum"),
TransportType::Reticulum
);
assert_eq!(
TransportType::from_label("RETICULUM"),
TransportType::Reticulum
);
assert_eq!(TransportType::from_label("nostr"), TransportType::Nostr);
assert_eq!(TransportType::from_label("NOSTR"), TransportType::Nostr);
assert_eq!(TransportType::from_label("unknown"), TransportType::BLE);
assert_eq!(TransportType::from_label(""), TransportType::BLE);
}
#[test]
fn test_transport_type_display() {
assert_eq!(TransportType::Internet.to_string(), "internet");
assert_eq!(TransportType::BLE.to_string(), "ble");
assert_eq!(TransportType::WiFiDirect.to_string(), "wifiDirect");
assert_eq!(TransportType::Reticulum.to_string(), "reticulum");
assert_eq!(TransportType::Nostr.to_string(), "nostr");
}
#[test]
fn test_transport_metrics_default() {
let m = TransportMetrics::default();
assert_eq!(m.rssi, None);
assert_eq!(m.congestion, 0.0);
assert_eq!(m.queue_depth, 0);
assert_eq!(m.success_count, 0);
assert_eq!(m.failure_count, 0);
assert_eq!(m.sample_count(), 0);
}
#[test]
fn test_transport_metrics_effective_delivery_ratio_explicit() {
let mut m = TransportMetrics::default();
m.delivery_ratio = Some(0.9);
assert_eq!(m.effective_delivery_ratio(), Some(0.9));
}
#[test]
fn test_transport_metrics_effective_delivery_ratio_from_counts() {
let mut m = TransportMetrics::default();
m.success_count = 8;
m.failure_count = 2;
assert_eq!(m.effective_delivery_ratio(), Some(0.8));
assert_eq!(m.sample_count(), 10);
}
#[test]
fn test_transport_metrics_effective_delivery_ratio_none_when_no_samples() {
let m = TransportMetrics::default();
assert_eq!(m.effective_delivery_ratio(), None);
assert_eq!(m.effective_drop_ratio(), None);
}
#[test]
fn test_transport_metrics_effective_drop_ratio() {
let mut m = TransportMetrics::default();
m.drop_rate = Some(0.2);
assert_eq!(m.effective_drop_ratio(), Some(0.2));
}
#[test]
fn test_transport_metrics_effective_drop_ratio_from_counts() {
let mut m = TransportMetrics::default();
m.success_count = 7;
m.failure_count = 3;
assert_eq!(m.effective_drop_ratio(), Some(0.3));
}
#[test]
fn test_transport_metrics_delivery_ratio_clamped() {
let mut m = TransportMetrics::default();
m.delivery_ratio = Some(1.5);
assert_eq!(m.effective_delivery_ratio(), Some(1.0));
m.delivery_ratio = Some(-0.1);
assert_eq!(m.effective_delivery_ratio(), Some(0.0));
}
#[test]
fn test_link_quality_from_rssi() {
let excellent = LinkQuality::from_rssi(-40);
assert_eq!(excellent.value(), 100);
assert!(excellent.is_good());
let good = LinkQuality::from_rssi(-60);
assert!(good.value() >= 70 && good.value() <= 90);
assert!(good.is_good());
let fair = LinkQuality::from_rssi(-75);
assert!(fair.value() >= 40 && fair.value() <= 70);
let poor = LinkQuality::from_rssi(-90);
assert!(poor.value() <= 40);
assert!(poor.is_poor());
}
#[test]
fn test_link_quality_clamping() {
let quality = LinkQuality::new(150);
assert_eq!(quality.value(), 100);
assert_eq!(LinkQuality::new(0).value(), 0);
assert_eq!(LinkQuality::new(100).value(), 100);
}
#[test]
fn test_link_quality_new_and_value() {
let q = LinkQuality::new(50);
assert_eq!(q.value(), 50);
}
#[test]
fn test_link_quality_is_good_is_poor() {
assert!(LinkQuality::new(70).is_good());
assert!(!LinkQuality::new(69).is_good());
assert!(LinkQuality::new(40).is_poor());
assert!(LinkQuality::new(39).is_poor());
assert!(!LinkQuality::new(41).is_poor());
}
#[test]
fn test_link_quality_from_rssi_boundaries() {
assert_eq!(LinkQuality::from_rssi(-50).value(), 100);
assert!(LinkQuality::from_rssi(-70).value() >= 70);
assert!(
LinkQuality::from_rssi(-85).value() >= 40 && LinkQuality::from_rssi(-85).value() <= 70
);
assert!(LinkQuality::from_rssi(-100).value() <= 40);
}
#[test]
fn test_link_quality_constants() {
assert_eq!(LinkQuality::MAX, 100);
assert_eq!(LinkQuality::MIN, 0);
}
}