Fix CPU reporting: MikroTik uses 0x80|pct encoding, add CPU to CSV
All checks were successful
CI / test (push) Successful in 2m9s

- 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) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-01 11:08:11 +04:00
parent 24f634170d
commit 27354108fc
5 changed files with 24 additions and 10 deletions

View File

@@ -12,7 +12,7 @@ use std::time::SystemTime;
static CSV_FILE: Mutex<Option<String>> = 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) {

View File

@@ -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 {

View File

@@ -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]]),
}

View File

@@ -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(|_| ())

View File

@@ -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);
}