use std::process::Command;
use std::thread;
use std::time::{Duration, Instant};
use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
const MEASUREMENT_DELAY: Duration = Duration::from_millis(10);
const SAMPLES_PER_COLLECT: usize = 8;
const WIFI_COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
pub struct WiFiRSSISource;
static WIFI_RSSI_INFO: SourceInfo = SourceInfo {
name: "wifi_rssi",
description: "WiFi signal strength (RSSI) and noise floor fluctuations",
physics: "Reads WiFi signal strength (RSSI) and noise floor via CoreWLAN \
framework. RSSI fluctuates due to: multipath fading (reflections \
off walls/objects), constructive/destructive interference at \
2.4/5/6 GHz, Rayleigh fading from moving objects, atmospheric \
absorption, and thermal noise in the radio receiver's LNA.",
category: SourceCategory::Network,
platform: Platform::MacOS,
requirements: &[Requirement::Wifi],
entropy_rate_estimate: 0.5,
composite: false,
is_fast: false,
};
impl WiFiRSSISource {
pub fn new() -> Self {
Self
}
}
impl Default for WiFiRSSISource {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
struct WifiMeasurement {
rssi: i32,
noise: i32,
timing_nanos: u128,
}
fn run_command_timed(cmd: &str, args: &[&str], timeout: Duration) -> (Option<String>, u64) {
let t0 = Instant::now();
let child = Command::new(cmd)
.args(args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn();
let mut child = match child {
Ok(c) => c,
Err(_) => return (None, t0.elapsed().as_nanos() as u64),
};
let deadline = Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(status)) => {
let elapsed = t0.elapsed().as_nanos() as u64;
if !status.success() {
return (None, elapsed);
}
use std::io::Read;
let mut stdout_str = String::new();
if let Some(mut out) = child.stdout.take() {
let _ = out.read_to_string(&mut stdout_str);
}
let result = if stdout_str.is_empty() {
None
} else {
Some(stdout_str)
};
return (result, elapsed);
}
Ok(None) => {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return (None, t0.elapsed().as_nanos() as u64);
}
std::thread::sleep(Duration::from_millis(50));
}
Err(_) => return (None, t0.elapsed().as_nanos() as u64),
}
}
}
fn discover_wifi_device() -> Option<String> {
let (output, _) = run_command_timed(
"/usr/sbin/networksetup",
&["-listallhardwareports"],
WIFI_COMMAND_TIMEOUT,
);
let text = output?;
let mut found_wifi = false;
for line in text.lines() {
if line.contains("Wi-Fi") || line.contains("AirPort") {
found_wifi = true;
continue;
}
if found_wifi && line.starts_with("Device:") {
let device = line.trim_start_matches("Device:").trim();
if !device.is_empty() {
return Some(device.to_string());
}
}
if found_wifi && line.starts_with("Hardware Port:") {
found_wifi = false;
}
}
None
}
fn read_via_ipconfig(device: &str) -> (Option<(i32, i32)>, u64) {
let (output, elapsed) = run_command_timed(
"/usr/sbin/ipconfig",
&["getsummary", device],
WIFI_COMMAND_TIMEOUT,
);
let text = match output {
Some(t) => t,
None => return (None, elapsed),
};
let rssi = match parse_field_value(&text, "RSSI") {
Some(v) => v,
None => return (None, elapsed),
};
let noise = parse_field_value(&text, "Noise").unwrap_or(rssi - 30);
(Some((rssi, noise)), elapsed)
}
fn read_via_airport() -> (Option<(i32, i32)>, u64) {
let (output, elapsed) = run_command_timed(
"/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport",
&["-I"],
WIFI_COMMAND_TIMEOUT,
);
let text = match output {
Some(t) => t,
None => return (None, elapsed),
};
let rssi = match parse_field_value(&text, "agrCtlRSSI") {
Some(v) => v,
None => return (None, elapsed),
};
let noise = parse_field_value(&text, "agrCtlNoise").unwrap_or(rssi - 30);
(Some((rssi, noise)), elapsed)
}
fn parse_field_value(text: &str, field: &str) -> Option<i32> {
for line in text.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix(field) {
let rest = rest.trim_start();
if let Some(val_str) = rest.strip_prefix(':') {
let val_str = val_str.trim();
if let Ok(v) = val_str.parse::<i32>() {
return Some(v);
}
}
}
}
None
}
fn measure_once(device: &Option<String>) -> WifiMeasurement {
let start = Instant::now();
let result = if let Some(dev) = device {
let (ipconfig_result, elapsed1) = read_via_ipconfig(dev);
if let Some(vals) = ipconfig_result {
Some((vals, elapsed1))
} else {
let (airport_result, elapsed2) = read_via_airport();
airport_result.map(|vals| (vals, elapsed1 + elapsed2))
}
} else {
let (airport_result, elapsed) = read_via_airport();
airport_result.map(|vals| (vals, elapsed))
};
let timing_nanos = start.elapsed().as_nanos();
match result {
Some(((rssi, noise), _)) => WifiMeasurement {
rssi,
noise,
timing_nanos,
},
None => WifiMeasurement {
rssi: 0,
noise: 0,
timing_nanos,
},
}
}
impl EntropySource for WiFiRSSISource {
fn info(&self) -> &SourceInfo {
&WIFI_RSSI_INFO
}
fn is_available(&self) -> bool {
if !cfg!(target_os = "macos") {
return false;
}
let device = discover_wifi_device();
let m = measure_once(&device);
m.rssi != 0 || m.noise != 0
}
fn collect(&self, n_samples: usize) -> Vec<u8> {
let mut raw = Vec::with_capacity(n_samples * 4);
let device = discover_wifi_device();
let mut measurements = Vec::with_capacity(SAMPLES_PER_COLLECT);
let max_bursts = 4;
for _ in 0..max_bursts {
measurements.clear();
for _ in 0..SAMPLES_PER_COLLECT {
let m = measure_once(&device);
measurements.push(m);
thread::sleep(MEASUREMENT_DELAY);
}
for i in 0..measurements.len() {
let m = &measurements[i];
let t_bytes = m.timing_nanos.to_le_bytes();
raw.push(t_bytes[0]);
raw.push(t_bytes[1]);
raw.push(t_bytes[2]);
raw.push(t_bytes[3]);
if m.rssi != 0 || m.noise != 0 {
raw.push(m.rssi as u8);
raw.push(m.noise as u8);
}
if i > 0 {
let prev = &measurements[i - 1];
raw.push((m.rssi.wrapping_sub(prev.rssi)) as u8);
let timing_delta = m.timing_nanos.abs_diff(prev.timing_nanos);
raw.push(timing_delta.to_le_bytes()[0]);
raw.push((m.rssi ^ m.noise) as u8);
}
}
if raw.len() >= n_samples * 2 {
break;
}
}
raw.truncate(n_samples);
raw
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_rssi_from_airport_output() {
let sample = "\
agrCtlRSSI: -62\n\
agrCtlNoise: -90\n\
state: running\n";
assert_eq!(parse_field_value(sample, "agrCtlRSSI"), Some(-62));
assert_eq!(parse_field_value(sample, "agrCtlNoise"), Some(-90));
}
#[test]
fn parse_rssi_from_ipconfig_output() {
let sample = "\
SSID : MyNetwork\n\
RSSI : -55\n\
Noise : -88\n";
assert_eq!(parse_field_value(sample, "RSSI"), Some(-55));
assert_eq!(parse_field_value(sample, "Noise"), Some(-88));
}
#[test]
fn parse_field_missing() {
assert_eq!(parse_field_value("nothing here", "RSSI"), None);
}
#[test]
fn source_info() {
let src = WiFiRSSISource::new();
assert_eq!(src.info().name, "wifi_rssi");
assert_eq!(src.info().category, SourceCategory::Network);
assert!((src.info().entropy_rate_estimate - 0.5).abs() < f64::EPSILON);
assert_eq!(src.info().platform, Platform::MacOS);
assert_eq!(src.info().requirements, &[Requirement::Wifi]);
assert!(!src.info().composite);
}
#[test]
#[cfg(target_os = "macos")]
#[ignore] fn wifi_rssi_collects_bytes() {
let src = WiFiRSSISource::new();
if src.is_available() {
let data = src.collect(32);
assert!(!data.is_empty());
assert!(data.len() <= 32);
}
}
}