use std::{collections::HashSet, fmt};
use chrono::Utc;
use objc2_core_wlan::{
CWChannel, CWChannelBand, CWChannelWidth, CWInterface, CWInterfaceMode, CWPHYMode, CWSecurity,
CWWiFiClient,
};
use crate::{Band, Bss, ChannelWidth, Scan, ScanError};
pub(super) fn interfaces() -> Vec<Interface> {
let client = unsafe { CWWiFiClient::sharedWiFiClient() };
let Some(interfaces) = (unsafe { client.interfaces() }) else {
return Vec::new();
};
interfaces
.iter()
.map(|interface| Interface { interface })
.collect()
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Interface {
interface: objc2::rc::Retained<CWInterface>,
}
impl fmt::Debug for Interface {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Interface")
.field("name", &self.name())
.field("hardware_address", &self.hardware_address())
.field("ssid", &self.ssid())
.field("bssid", &self.bssid())
.field("power_on", &self.power_on())
.field("wlan_channel", &self.wlan_channel())
.field("active_phy_mode", &self.active_phy_mode())
.field("security", &self.security())
.field("transmit_rate_mbps", &self.transmit_rate_mbps())
.field("country_code", &self.country_code())
.field("interface_mode", &self.interface_mode())
.field("transmit_power_mw", &self.transmit_power_mw())
.field("service_active", &self.service_active())
.finish_non_exhaustive()
}
}
impl Interface {
pub fn name(&self) -> Option<String> {
unsafe { self.interface.interfaceName() }.map(|name| name.to_string())
}
pub fn hardware_address(&self) -> Option<String> {
unsafe { self.interface.hardwareAddress() }.map(|address| address.to_string())
}
pub fn ssid(&self) -> Option<String> {
unsafe { self.interface.ssid() }.map(|ssid| ssid.to_string())
}
pub fn bssid(&self) -> Option<[u8; 6]> {
unsafe { self.interface.bssid() }.and_then(|bssid| parse_bssid(&bssid.to_string()))
}
pub fn power_on(&self) -> bool {
unsafe { self.interface.powerOn() }
}
pub fn supported_wlan_channels(&self) -> HashSet<(Band, i32, ChannelWidth)> {
let Some(channels) = (unsafe { self.interface.supportedWLANChannels() }) else {
return HashSet::new();
};
channels
.iter()
.map(|channel| wlan_channel_tuple(&channel))
.collect()
}
pub fn wlan_channel(&self) -> Option<(Band, i32, ChannelWidth)> {
unsafe { self.interface.wlanChannel() }
.as_ref()
.map(|channel| wlan_channel_tuple(channel))
}
pub fn active_phy_mode(&self) -> CWPHYMode {
unsafe { self.interface.activePHYMode() }
}
pub fn security(&self) -> CWSecurity {
unsafe { self.interface.security() }
}
pub fn transmit_rate_mbps(&self) -> f64 {
unsafe { self.interface.transmitRate() }
}
pub fn country_code(&self) -> Option<String> {
unsafe { self.interface.countryCode() }.map(|country_code| country_code.to_string())
}
pub fn interface_mode(&self) -> CWInterfaceMode {
unsafe { self.interface.interfaceMode() }
}
pub fn transmit_power_mw(&self) -> i32 {
(unsafe { self.interface.transmitPower() }) as i32
}
pub fn signal_dbm(&self) -> i32 {
(unsafe { self.interface.rssiValue() }) as i32
}
pub fn noise_dbm(&self) -> i32 {
(unsafe { self.interface.noiseMeasurement() }) as i32
}
pub fn service_active(&self) -> bool {
unsafe { self.interface.serviceActive() }
}
#[tracing::instrument(skip(self), fields(interface = ?self.name()))]
pub async fn scan(&self) -> Result<Scan, ScanError> {
self.scan_blocking()
}
#[tracing::instrument(skip(self), fields(interface = ?self.name()))]
pub fn scan_blocking(&self) -> Result<Scan, ScanError> {
let start_time = Utc::now();
let networks = unsafe { self.interface.scanForNetworksWithSSID_error(None) }?;
let bss_list = networks
.iter()
.filter_map(|network| Bss::from_core_wlan_network(&network))
.collect();
let end_time = Utc::now();
Ok(Scan::new(bss_list, start_time, end_time))
}
pub async fn cached_scan_results(&self) -> Result<Vec<Bss>, ScanError> {
self.cached_scan_results_blocking()
}
pub fn cached_scan_results_blocking(&self) -> Result<Vec<Bss>, ScanError> {
let Some(networks) = (unsafe { self.interface.cachedScanResults() }) else {
return Ok(Vec::new());
};
Ok(networks
.iter()
.filter_map(|network| Bss::from_core_wlan_network(&network))
.collect())
}
}
fn wlan_channel_tuple(channel: &CWChannel) -> (Band, i32, ChannelWidth) {
(
band_from_core_wlan(unsafe { channel.channelBand() }),
unsafe { channel.channelNumber() } as i32,
channel_width_from_core_wlan(unsafe { channel.channelWidth() }),
)
}
fn band_from_core_wlan(band: CWChannelBand) -> Band {
match band {
CWChannelBand::Band2GHz => Band::TwoPointFourGhz,
CWChannelBand::Band5GHz => Band::FiveGhz,
CWChannelBand::Band6GHz => Band::SixGhz,
_ => Band::TwoPointFourGhz,
}
}
fn channel_width_from_core_wlan(channel_width: CWChannelWidth) -> ChannelWidth {
match channel_width {
CWChannelWidth::Width20MHz => ChannelWidth::TwentyMhz,
CWChannelWidth::Width40MHz => ChannelWidth::FortyMhz,
CWChannelWidth::Width80MHz => ChannelWidth::EightyMhz,
CWChannelWidth::Width160MHz => ChannelWidth::OneSixtyMhz,
_ => ChannelWidth::TwentyMhz,
}
}
pub(crate) fn parse_bssid(bssid: &str) -> Option<[u8; 6]> {
let mut bytes = [0; 6];
let mut parts = bssid.split(':');
for byte in &mut bytes {
*byte = u8::from_str_radix(parts.next()?, 16).ok()?;
}
parts.next().is_none().then_some(bytes)
}