Fix CPU reporting: MikroTik uses 0x80|pct encoding, add CPU to CSV
All checks were successful
CI / test (push) Successful in 2m9s
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:
@@ -12,7 +12,7 @@ use std::time::SystemTime;
|
|||||||
static CSV_FILE: Mutex<Option<String>> = Mutex::new(None);
|
static CSV_FILE: Mutex<Option<String>> = Mutex::new(None);
|
||||||
static QUIET: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
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.
|
/// Initialize CSV output. Creates file with headers if needed.
|
||||||
pub fn init(path: &str) -> std::io::Result<()> {
|
pub fn init(path: &str) -> std::io::Result<()> {
|
||||||
@@ -45,6 +45,8 @@ pub fn write_result(
|
|||||||
tx_bytes: u64,
|
tx_bytes: u64,
|
||||||
rx_bytes: u64,
|
rx_bytes: u64,
|
||||||
lost_packets: u64,
|
lost_packets: u64,
|
||||||
|
local_cpu: u8,
|
||||||
|
remote_cpu: u8,
|
||||||
auth_type: &str,
|
auth_type: &str,
|
||||||
) {
|
) {
|
||||||
let guard = CSV_FILE.lock().unwrap();
|
let guard = CSV_FILE.lock().unwrap();
|
||||||
@@ -66,9 +68,10 @@ pub fn write_result(
|
|||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
let row = format!(
|
let row = format!(
|
||||||
"{},{},{},{},{},{},{:.2},{:.2},{},{},{},{}",
|
"{},{},{},{},{},{},{:.2},{:.2},{},{},{},{},{},{}",
|
||||||
now, host, port, protocol, direction, duration_secs,
|
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) {
|
if let Ok(mut f) = OpenOptions::new().append(true).open(path) {
|
||||||
|
|||||||
@@ -226,9 +226,11 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
// Write CSV if enabled
|
// Write CSV if enabled
|
||||||
if csv_output::is_enabled() {
|
if csv_output::is_enabled() {
|
||||||
let auth_type = if cli.auth_user.is_some() { "auth" } else { "none" };
|
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(
|
csv_output::write_result(
|
||||||
&host, cli.port, proto_str, dir_str,
|
&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 {
|
} else {
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ impl StatusMessage {
|
|||||||
pub fn serialize(&self) -> [u8; STATUS_MSG_SIZE] {
|
pub fn serialize(&self) -> [u8; STATUS_MSG_SIZE] {
|
||||||
let mut buf = [0u8; STATUS_MSG_SIZE];
|
let mut buf = [0u8; STATUS_MSG_SIZE];
|
||||||
buf[0] = STATUS_MSG_TYPE;
|
buf[0] = STATUS_MSG_TYPE;
|
||||||
// Byte 1: CPU load percentage (0-100)
|
// Byte 1: CPU load with high bit set (MikroTik format: 0x80 | percentage)
|
||||||
buf[1] = self.cpu_load;
|
buf[1] = 0x80 | (self.cpu_load & 0x7F);
|
||||||
buf[2] = 0;
|
buf[2] = 0;
|
||||||
buf[3] = 0;
|
buf[3] = 0;
|
||||||
// Bytes 4-7: sequence number (LE)
|
// Bytes 4-7: sequence number (LE)
|
||||||
@@ -156,8 +156,11 @@ impl StatusMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize(buf: &[u8; STATUS_MSG_SIZE]) -> Self {
|
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 {
|
Self {
|
||||||
cpu_load: buf[1],
|
cpu_load: cpu.min(100),
|
||||||
seq: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
|
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]]),
|
bytes_received: u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,7 +357,8 @@ async fn handle_client(
|
|||||||
if crate::csv_output::is_enabled() {
|
if crate::csv_output::is_enabled() {
|
||||||
crate::csv_output::write_result(
|
crate::csv_output::write_result(
|
||||||
&peer.ip().to_string(), peer.port(), proto_str, dir_str,
|
&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(|_| ())
|
result.map(|_| ())
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ async fn test_csv_created_client() {
|
|||||||
// Write result like main.rs does
|
// Write result like main.rs does
|
||||||
btest_rs::csv_output::write_result(
|
btest_rs::csv_output::write_result(
|
||||||
"127.0.0.1", port, "TCP", "receive",
|
"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
|
// 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.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[0].starts_with("timestamp,"), "CSV header missing");
|
||||||
assert!(lines[1].contains("TCP"), "CSV row should contain protocol");
|
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);
|
let _ = std::fs::remove_file(&csv_path);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user