From 27354108fc4fba0f3906b4665679f0110c397c9c Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Wed, 1 Apr 2026 11:08:11 +0400 Subject: [PATCH] Fix CPU reporting: MikroTik uses 0x80|pct encoding, add CPU to CSV - MikroTik encodes CPU as 0x80 | percentage (high bit flag) - Deserialize: mask with 0x7F and cap at 100 - Serialize: set high bit (0x80 | cpu) to match MikroTik format - CSV now includes local_cpu_pct and remote_cpu_pct columns - Both client and server write CPU to CSV Co-Authored-By: Claude Opus 4.6 (1M context) --- src/csv_output.rs | 9 ++++++--- src/main.rs | 4 +++- src/protocol.rs | 9 ++++++--- src/server.rs | 3 ++- tests/full_integration_test.rs | 9 +++++++-- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/csv_output.rs b/src/csv_output.rs index f4a043c..e1993b7 100644 --- a/src/csv_output.rs +++ b/src/csv_output.rs @@ -12,7 +12,7 @@ use std::time::SystemTime; static CSV_FILE: Mutex> = Mutex::new(None); static QUIET: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); -const HEADER: &str = "timestamp,host,port,protocol,direction,duration_s,tx_avg_mbps,rx_avg_mbps,tx_bytes,rx_bytes,lost_packets,auth_type"; +const HEADER: &str = "timestamp,host,port,protocol,direction,duration_s,tx_avg_mbps,rx_avg_mbps,tx_bytes,rx_bytes,lost_packets,local_cpu_pct,remote_cpu_pct,auth_type"; /// Initialize CSV output. Creates file with headers if needed. pub fn init(path: &str) -> std::io::Result<()> { @@ -45,6 +45,8 @@ pub fn write_result( tx_bytes: u64, rx_bytes: u64, lost_packets: u64, + local_cpu: u8, + remote_cpu: u8, auth_type: &str, ) { let guard = CSV_FILE.lock().unwrap(); @@ -66,9 +68,10 @@ pub fn write_result( .as_secs(); let row = format!( - "{},{},{},{},{},{},{:.2},{:.2},{},{},{},{}", + "{},{},{},{},{},{},{:.2},{:.2},{},{},{},{},{},{}", now, host, port, protocol, direction, duration_secs, - tx_mbps, rx_mbps, tx_bytes, rx_bytes, lost_packets, auth_type, + tx_mbps, rx_mbps, tx_bytes, rx_bytes, lost_packets, + local_cpu, remote_cpu, auth_type, ); if let Ok(mut f) = OpenOptions::new().append(true).open(path) { diff --git a/src/main.rs b/src/main.rs index 956706d..5eb20b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -226,9 +226,11 @@ async fn main() -> anyhow::Result<()> { // Write CSV if enabled if csv_output::is_enabled() { let auth_type = if cli.auth_user.is_some() { "auth" } else { "none" }; + let local_cpu = cpu::get(); + let remote_cpu = shared_state.remote_cpu.load(std::sync::atomic::Ordering::Relaxed); csv_output::write_result( &host, cli.port, proto_str, dir_str, - elapsed, total_tx, total_rx, total_lost, auth_type, + elapsed, total_tx, total_rx, total_lost, local_cpu, remote_cpu, auth_type, ); } } else { diff --git a/src/protocol.rs b/src/protocol.rs index a7e8e73..4388c2e 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -144,8 +144,8 @@ impl StatusMessage { pub fn serialize(&self) -> [u8; STATUS_MSG_SIZE] { let mut buf = [0u8; STATUS_MSG_SIZE]; buf[0] = STATUS_MSG_TYPE; - // Byte 1: CPU load percentage (0-100) - buf[1] = self.cpu_load; + // Byte 1: CPU load with high bit set (MikroTik format: 0x80 | percentage) + buf[1] = 0x80 | (self.cpu_load & 0x7F); buf[2] = 0; buf[3] = 0; // Bytes 4-7: sequence number (LE) @@ -156,8 +156,11 @@ impl StatusMessage { } pub fn deserialize(buf: &[u8; STATUS_MSG_SIZE]) -> Self { + // MikroTik encodes CPU with high bit set: actual = byte & 0x7F + let raw_cpu = buf[1]; + let cpu = if raw_cpu > 128 { raw_cpu & 0x7F } else { raw_cpu }; Self { - cpu_load: buf[1], + cpu_load: cpu.min(100), seq: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]), bytes_received: u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]), } diff --git a/src/server.rs b/src/server.rs index 771d0bf..a46c269 100644 --- a/src/server.rs +++ b/src/server.rs @@ -357,7 +357,8 @@ async fn handle_client( if crate::csv_output::is_enabled() { crate::csv_output::write_result( &peer.ip().to_string(), peer.port(), proto_str, dir_str, - intervals as u64, total_tx, total_rx, total_lost, auth_type, + intervals as u64, total_tx, total_rx, total_lost, + crate::cpu::get(), 0, auth_type, ); } result.map(|_| ()) diff --git a/tests/full_integration_test.rs b/tests/full_integration_test.rs index 96f4bd5..9dc2d23 100644 --- a/tests/full_integration_test.rs +++ b/tests/full_integration_test.rs @@ -242,7 +242,7 @@ async fn test_csv_created_client() { // Write result like main.rs does btest_rs::csv_output::write_result( "127.0.0.1", port, "TCP", "receive", - 2, tx, rx, lost, "none", + 2, tx, rx, lost, 0, 0, "none", ); // Verify CSV exists and has data @@ -251,7 +251,12 @@ async fn test_csv_created_client() { assert!(lines.len() >= 2, "CSV should have header + at least 1 row, got {} lines", lines.len()); assert!(lines[0].starts_with("timestamp,"), "CSV header missing"); assert!(lines[1].contains("TCP"), "CSV row should contain protocol"); - assert!(!lines[1].contains(",0,0,0,"), "CSV should have non-zero bytes"); + // Check that tx or rx bytes are non-zero (the 7th or 8th CSV field) + let fields: Vec<&str> = lines[1].split(',').collect(); + assert!(fields.len() >= 10, "CSV row should have enough fields"); + let tx_bytes: u64 = fields[8].parse().unwrap_or(0); + let rx_bytes: u64 = fields[9].parse().unwrap_or(0); + assert!(tx_bytes > 0 || rx_bytes > 0, "CSV should have non-zero bytes: tx={} rx={}", tx_bytes, rx_bytes); let _ = std::fs::remove_file(&csv_path); }