use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum SimError {
LinkNotFound(usize),
InvalidInput(String),
}
impl fmt::Display for SimError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LinkNotFound(id) => write!(f, "link {id} not found"),
Self::InvalidInput(m) => write!(f, "invalid input: {m}"),
}
}
}
impl std::error::Error for SimError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProtocolType {
Iec61850Goose,
Iec61850Sampled,
Dnp3,
Modbus,
Mqtt,
CimXml,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MessageType {
ControlCommand,
StatusReport,
SampledValues,
Alarm,
Heartbeat,
TimeSync,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub id: u64,
pub protocol: ProtocolType,
pub source: u32,
pub destination: u32,
pub message_type: MessageType,
pub payload: Vec<u8>,
pub timestamp_us: u64,
pub priority: u8,
pub size_bytes: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkLink {
pub id: usize,
pub bandwidth_mbps: f64,
pub latency_ms: f64,
pub packet_loss_rate: f64,
pub jitter_ms: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtocolSimResult {
pub messages_sent: usize,
pub messages_received: usize,
pub messages_lost: usize,
pub avg_latency_ms: f64,
pub max_latency_ms: f64,
pub p99_latency_ms: f64,
pub throughput_kbps: f64,
pub goose_delivery_pct: f64,
}
struct Lcg {
state: u64,
}
impl Lcg {
const MULT: u64 = 6_364_136_223_846_793_005;
const ADD: u64 = 1_442_695_040_888_963_407;
fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_add(1),
}
}
fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_mul(Self::MULT).wrapping_add(Self::ADD);
self.state
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn next_normal(&mut self) -> f64 {
let sum: f64 = (0..12).map(|_| self.next_f64()).sum();
sum - 6.0
}
}
pub struct ProtocolSimulator {
links: Vec<NetworkLink>,
message_log: Vec<(Message, f64, bool)>,
lcg: Lcg,
}
impl Default for ProtocolSimulator {
fn default() -> Self {
Self::new()
}
}
impl ProtocolSimulator {
pub fn new() -> Self {
Self {
links: Vec::new(),
message_log: Vec::new(),
lcg: Lcg::new(0xDEAD_BEEF_1234_5678),
}
}
pub fn add_link(&mut self, link: NetworkLink) {
self.links.push(link);
}
pub fn simulate(
&mut self,
messages: Vec<Message>,
link_id: usize,
simulation_time_ms: f64,
) -> Result<ProtocolSimResult, SimError> {
if simulation_time_ms <= 0.0 {
return Err(SimError::InvalidInput(
"simulation_time_ms must be positive".to_string(),
));
}
let link = self
.links
.iter()
.find(|l| l.id == link_id)
.ok_or(SimError::LinkNotFound(link_id))?
.clone();
let bandwidth_bytes_per_ms = link.bandwidth_mbps * 1_000_000.0 / 8.0 / 1_000.0;
let mut queue_time_ms = 0.0_f64; let mut latencies: Vec<f64> = Vec::new();
let mut goose_sent = 0usize;
let mut goose_received = 0usize;
let mut total_bytes_delivered = 0usize;
for msg in &messages {
let is_goose = msg.protocol == ProtocolType::Iec61850Goose;
if is_goose {
goose_sent += 1;
}
let loss_roll = self.lcg.next_f64();
if loss_roll < link.packet_loss_rate {
self.message_log.push((msg.clone(), 0.0, false));
continue;
}
let raw_jitter = self.lcg.next_normal() * link.jitter_ms;
let jitter = raw_jitter.clamp(-2.0 * link.jitter_ms, 2.0 * link.jitter_ms);
let latency_base = (link.latency_ms + jitter).max(0.0);
let tx_time_ms = msg.size_bytes as f64 / bandwidth_bytes_per_ms;
let msg_dispatch_time = msg.timestamp_us as f64 / 1_000.0; let start_tx = queue_time_ms.max(msg_dispatch_time);
queue_time_ms = start_tx + tx_time_ms;
let arrival_ms = start_tx + tx_time_ms + latency_base;
if arrival_ms > simulation_time_ms {
self.message_log.push((msg.clone(), arrival_ms, false));
continue;
}
let total_latency = arrival_ms - msg_dispatch_time;
latencies.push(total_latency);
total_bytes_delivered += msg.size_bytes;
self.message_log.push((msg.clone(), arrival_ms, true));
if is_goose {
goose_received += 1;
}
}
let messages_sent = messages.len();
let messages_received = latencies.len();
let messages_lost = messages_sent - messages_received;
let avg_latency_ms = if latencies.is_empty() {
0.0
} else {
latencies.iter().sum::<f64>() / latencies.len() as f64
};
let max_latency_ms = latencies
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
.max(0.0);
let p99_latency_ms = percentile_sorted(&latencies, 99.0);
let throughput_kbps = if simulation_time_ms > 0.0 {
total_bytes_delivered as f64 * 8.0 / simulation_time_ms } else {
0.0
};
let goose_delivery_pct = if goose_sent == 0 {
100.0
} else {
goose_received as f64 / goose_sent as f64 * 100.0
};
Ok(ProtocolSimResult {
messages_sent,
messages_received,
messages_lost,
avg_latency_ms,
max_latency_ms,
p99_latency_ms,
throughput_kbps,
goose_delivery_pct,
})
}
pub fn generate_goose_sequence(event_time_ms: f64, n_retransmit: usize) -> Vec<Message> {
let mut msgs: Vec<Message> = Vec::with_capacity(n_retransmit + 1);
msgs.push(Message {
id: 0,
protocol: ProtocolType::Iec61850Goose,
source: 0x0001,
destination: 0xFFFF,
message_type: MessageType::ControlCommand,
payload: vec![0x01, 0x00, 0x00, 0x00], timestamp_us: (event_time_ms * 1_000.0) as u64,
priority: 6, size_bytes: 128, });
let mut interval_ms = 2.0_f64;
let max_interval_ms = 1_000.0_f64;
let mut t = event_time_ms + interval_ms;
for i in 1..=n_retransmit {
msgs.push(Message {
id: i as u64,
protocol: ProtocolType::Iec61850Goose,
source: 0x0001,
destination: 0xFFFF,
message_type: MessageType::ControlCommand,
payload: vec![0x01, 0x00, 0x00, 0x00],
timestamp_us: (t * 1_000.0) as u64,
priority: 6,
size_bytes: 128,
});
interval_ms = (interval_ms * 2.0).min(max_interval_ms);
t += interval_ms;
}
msgs
}
pub fn message_statistics(&self) -> ProtocolSimResult {
let messages_sent = self.message_log.len();
let delivered: Vec<f64> = self
.message_log
.iter()
.filter(|(_, _, ok)| *ok)
.map(|(msg, arrival, _)| {
let dispatch_ms = msg.timestamp_us as f64 / 1_000.0;
(*arrival - dispatch_ms).max(0.0)
})
.collect();
let messages_received = delivered.len();
let messages_lost = messages_sent - messages_received;
let avg_latency_ms = if delivered.is_empty() {
0.0
} else {
delivered.iter().sum::<f64>() / delivered.len() as f64
};
let max_latency_ms = delivered
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
.max(0.0);
let p99_latency_ms = percentile_sorted(&delivered, 99.0);
let total_bytes: usize = self
.message_log
.iter()
.filter(|(_, _, ok)| *ok)
.map(|(msg, _, _)| msg.size_bytes)
.sum();
let t_max = self
.message_log
.iter()
.map(|(_, t, _)| *t)
.fold(0.0_f64, f64::max);
let throughput_kbps = if t_max > 0.0 {
total_bytes as f64 * 8.0 / t_max
} else {
0.0
};
let goose_sent: usize = self
.message_log
.iter()
.filter(|(msg, _, _)| msg.protocol == ProtocolType::Iec61850Goose)
.count();
let goose_received: usize = self
.message_log
.iter()
.filter(|(msg, _, ok)| msg.protocol == ProtocolType::Iec61850Goose && *ok)
.count();
let goose_delivery_pct = if goose_sent == 0 {
100.0
} else {
goose_received as f64 / goose_sent as f64 * 100.0
};
ProtocolSimResult {
messages_sent,
messages_received,
messages_lost,
avg_latency_ms,
max_latency_ms,
p99_latency_ms,
throughput_kbps,
goose_delivery_pct,
}
}
}
fn percentile_sorted(data: &[f64], p: f64) -> f64 {
if data.is_empty() {
return 0.0;
}
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((p / 100.0) * (sorted.len() - 1) as f64).round() as usize;
sorted[idx.min(sorted.len() - 1)]
}
#[cfg(test)]
mod tests {
use super::*;
fn make_link(id: usize, loss: f64) -> NetworkLink {
NetworkLink {
id,
bandwidth_mbps: 100.0,
latency_ms: 1.0,
packet_loss_rate: loss,
jitter_ms: 0.1,
}
}
fn make_messages(n: usize, proto: ProtocolType) -> Vec<Message> {
(0..n)
.map(|i| Message {
id: i as u64,
protocol: proto,
source: 1,
destination: 2,
message_type: MessageType::StatusReport,
payload: vec![0u8; 64],
timestamp_us: i as u64 * 1_000, priority: 4,
size_bytes: 100,
})
.collect()
}
#[test]
fn test_zero_loss_link_all_delivered() {
let mut sim = ProtocolSimulator::new();
sim.add_link(make_link(0, 0.0));
let msgs = make_messages(50, ProtocolType::Dnp3);
let result = sim
.simulate(msgs, 0, 10_000.0)
.expect("zero-loss simulate must succeed");
assert_eq!(
result.messages_lost, 0,
"zero-loss link must deliver all messages"
);
assert_eq!(result.messages_received, 50);
}
#[test]
fn test_high_loss_link_reduces_delivery() {
let mut sim = ProtocolSimulator::new();
sim.add_link(make_link(1, 0.5));
let msgs = make_messages(1000, ProtocolType::Modbus);
let result = sim
.simulate(msgs, 1, 100_000.0)
.expect("high-loss simulate must succeed");
let loss_pct = result.messages_lost as f64 / 1000.0 * 100.0;
assert!(
loss_pct > 30.0 && loss_pct < 70.0,
"50 % loss link should lose 30–70 % of messages, lost {:.1} %",
loss_pct
);
}
#[test]
fn test_goose_sequence_retransmit_intervals() {
let event_t = 100.0_f64;
let msgs = ProtocolSimulator::generate_goose_sequence(event_t, 5);
assert_eq!(msgs.len(), 6, "1 initial + 5 retransmissions");
assert_eq!(msgs[0].timestamp_us, (event_t * 1_000.0) as u64);
assert!(msgs
.iter()
.all(|m| m.protocol == ProtocolType::Iec61850Goose));
let times_ms: Vec<f64> = msgs
.iter()
.map(|m| m.timestamp_us as f64 / 1_000.0)
.collect();
for i in 1..times_ms.len().saturating_sub(1) {
let gap_i = times_ms[i] - times_ms[i - 1];
let gap_ip1 = times_ms[i + 1] - times_ms[i];
assert!(
gap_ip1 >= gap_i - 1e-9,
"GOOSE retransmit interval should be non-decreasing: gap[{i}]={gap_i:.2} ms, gap[{i}+1]={gap_ip1:.2} ms"
);
}
}
#[test]
fn test_latency_statistics_consistency() {
let mut sim = ProtocolSimulator::new();
sim.add_link(make_link(2, 0.0));
let msgs = make_messages(200, ProtocolType::Mqtt);
let result = sim
.simulate(msgs, 2, 100_000.0)
.expect("latency-stats simulate must succeed");
assert!(
result.avg_latency_ms <= result.max_latency_ms + 1e-9,
"avg ({:.3}) must be ≤ max ({:.3})",
result.avg_latency_ms,
result.max_latency_ms
);
assert!(
result.p99_latency_ms >= result.avg_latency_ms - 1e-9,
"P99 ({:.3}) should be ≥ avg ({:.3})",
result.p99_latency_ms,
result.avg_latency_ms
);
assert!(
result.p99_latency_ms <= result.max_latency_ms + 1e-9,
"P99 ({:.3}) should be ≤ max ({:.3})",
result.p99_latency_ms,
result.max_latency_ms
);
}
#[test]
fn test_bandwidth_throughput_within_link_capacity() {
let link_mbps = 1.0_f64;
let mut sim = ProtocolSimulator::new();
sim.add_link(NetworkLink {
id: 3,
bandwidth_mbps: link_mbps,
latency_ms: 0.5,
packet_loss_rate: 0.0,
jitter_ms: 0.01,
});
let msgs: Vec<Message> = (0..100)
.map(|i| Message {
id: i,
protocol: ProtocolType::Dnp3,
source: 1,
destination: 2,
message_type: MessageType::StatusReport,
payload: vec![0u8; 1000],
timestamp_us: 0,
priority: 4,
size_bytes: 1000,
})
.collect();
let result = sim
.simulate(msgs, 3, 10_000.0)
.expect("bandwidth simulate must succeed");
assert!(
result.throughput_kbps <= link_mbps * 1_000.0 + 1.0,
"Throughput ({:.1} kbps) must not exceed link capacity ({:.0} kbps)",
result.throughput_kbps,
link_mbps * 1_000.0
);
}
#[test]
fn test_goose_delivery_pct_with_zero_loss() {
let mut sim = ProtocolSimulator::new();
sim.add_link(make_link(4, 0.0));
let msgs = ProtocolSimulator::generate_goose_sequence(0.0, 4);
let result = sim
.simulate(msgs, 4, 100_000.0)
.expect("goose zero-loss simulate must succeed");
assert!(
(result.goose_delivery_pct - 100.0).abs() < 1e-9,
"Zero-loss GOOSE delivery should be 100 %, got {:.2} %",
result.goose_delivery_pct
);
}
#[test]
fn test_link_not_found_error() {
let mut sim = ProtocolSimulator::new();
let msgs = make_messages(1, ProtocolType::Dnp3);
let err = sim.simulate(msgs, 99, 1000.0).unwrap_err();
assert!(matches!(err, SimError::LinkNotFound(99)));
}
#[test]
fn test_invalid_simulation_time() {
let mut sim = ProtocolSimulator::new();
sim.add_link(make_link(5, 0.0));
let msgs = make_messages(1, ProtocolType::Dnp3);
let err = sim.simulate(msgs, 5, -1.0).unwrap_err();
assert!(matches!(err, SimError::InvalidInput(_)));
}
#[test]
fn test_sim_error_display_formatting() {
let link_err = SimError::LinkNotFound(42);
assert_eq!(link_err.to_string(), "link 42 not found");
let input_err = SimError::InvalidInput("bad time".to_string());
assert!(
input_err.to_string().contains("bad time"),
"InvalidInput display must include the message text"
);
}
#[test]
fn test_empty_message_list_returns_zero_stats() {
let mut sim = ProtocolSimulator::new();
sim.add_link(make_link(6, 0.0));
let result = sim
.simulate(vec![], 6, 1_000.0)
.expect("empty message list must succeed");
assert_eq!(result.messages_sent, 0);
assert_eq!(result.messages_received, 0);
assert_eq!(result.messages_lost, 0);
assert_eq!(result.avg_latency_ms, 0.0);
assert_eq!(result.max_latency_ms, 0.0);
assert_eq!(result.throughput_kbps, 0.0);
assert!(
(result.goose_delivery_pct - 100.0).abs() < 1e-9,
"empty run GOOSE delivery pct must be 100.0"
);
}
#[test]
fn test_total_loss_link_delivers_nothing() {
let mut sim = ProtocolSimulator::new();
sim.add_link(make_link(7, 1.0)); let msgs = make_messages(20, ProtocolType::Mqtt);
let result = sim
.simulate(msgs, 7, 100_000.0)
.expect("simulate with 100% loss must not error");
assert_eq!(
result.messages_received, 0,
"100 % loss link must receive no messages"
);
assert_eq!(result.messages_lost, 20);
assert_eq!(result.avg_latency_ms, 0.0);
}
#[test]
fn test_short_simulation_window_drops_late_messages() {
let mut sim = ProtocolSimulator::new();
sim.add_link(NetworkLink {
id: 8,
bandwidth_mbps: 100.0,
latency_ms: 5_000.0, packet_loss_rate: 0.0,
jitter_ms: 0.0,
});
let msgs = make_messages(10, ProtocolType::Dnp3);
let result = sim
.simulate(msgs, 8, 1.0)
.expect("short window simulate must not error");
assert_eq!(
result.messages_received, 0,
"5 s latency on a 1 ms window must drop all messages"
);
assert_eq!(result.messages_lost, 10);
}
#[test]
fn test_message_statistics_aggregates_across_simulate_calls() {
let mut sim = ProtocolSimulator::new();
sim.add_link(make_link(9, 0.0));
let batch_a = make_messages(10, ProtocolType::Dnp3);
let batch_b = make_messages(5, ProtocolType::Modbus);
sim.simulate(batch_a, 9, 100_000.0)
.expect("first batch must succeed");
sim.simulate(batch_b, 9, 100_000.0)
.expect("second batch must succeed");
let stats = sim.message_statistics();
assert_eq!(
stats.messages_sent, 15,
"message_statistics must aggregate both batches"
);
}
#[test]
fn test_goose_sequence_zero_retransmissions() {
let msgs = ProtocolSimulator::generate_goose_sequence(50.0, 0);
assert_eq!(
msgs.len(),
1,
"zero retransmissions must produce exactly 1 message"
);
assert_eq!(msgs[0].protocol, ProtocolType::Iec61850Goose);
assert_eq!(msgs[0].timestamp_us, 50_000);
assert_eq!(msgs[0].priority, 6, "GOOSE priority must be 6");
}
#[test]
fn test_default_constructor_behaves_like_new() {
let mut sim_default = ProtocolSimulator::default();
let mut sim_new = ProtocolSimulator::new();
sim_default.add_link(make_link(10, 0.0));
sim_new.add_link(make_link(10, 0.0));
let msgs_d = make_messages(5, ProtocolType::CimXml);
let msgs_n = make_messages(5, ProtocolType::CimXml);
let r_default = sim_default
.simulate(msgs_d, 10, 100_000.0)
.expect("default simulator must work");
let r_new = sim_new
.simulate(msgs_n, 10, 100_000.0)
.expect("new simulator must work");
assert_eq!(
r_default.messages_sent, r_new.messages_sent,
"default() and new() must produce identical sent counts"
);
assert_eq!(r_default.messages_received, r_new.messages_received);
}
#[test]
fn test_protocol_type_and_message_type_equality() {
let p1 = ProtocolType::Modbus;
let p2 = p1;
assert_eq!(p1, p2);
let p3 = ProtocolType::Mqtt;
assert_ne!(p1, p3);
let m1 = MessageType::Alarm;
let m2 = m1;
assert_eq!(m1, m2);
let m3 = MessageType::Heartbeat;
assert_ne!(m1, m3);
}
}