use portable_atomic::Ordering;
use crate::state::{SharedState, saturating_increment};
use crate::{DiagnosticStage, DiagnosticTrace, DiagnosticTraceKind, Error};
pub mod security;
pub use security::{ManagementFrameProtection, PersonalSecurity, SaePwe};
pub(crate) const MAX_SCAN_RESULTS: usize = 32;
const SSID_CAPACITY: usize = 32;
const PASSPHRASE_CAPACITY: usize = 63;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct RadioConfig {
pub wifi: WifiConfig,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WifiConfig {
pub initialize_timeout: BackendTimeout,
pub disconnect_timeout: BackendTimeout,
}
impl Default for WifiConfig {
fn default() -> Self {
Self {
initialize_timeout: BackendTimeout::from_millis_const(30_000),
disconnect_timeout: BackendTimeout::from_millis_const(10_000),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OperationTimeout(u32);
impl OperationTimeout {
pub const fn try_from_millis(milliseconds: u32) -> Option<Self> {
if milliseconds == 0 {
None
} else {
Some(Self(milliseconds))
}
}
pub const fn as_millis(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BackendTimeout(u32);
impl BackendTimeout {
pub const fn try_from_millis(milliseconds: u32) -> Option<Self> {
if milliseconds == 0 {
None
} else {
Some(Self(milliseconds))
}
}
pub const fn as_millis(self) -> u32 {
self.0
}
const fn from_millis_const(milliseconds: u32) -> Self {
assert!(milliseconds != 0);
Self(milliseconds)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WifiL2Capabilities {
station_mac_address: [u8; 6],
}
impl WifiL2Capabilities {
pub const fn try_new(station_mac_address: [u8; 6]) -> Option<Self> {
let any_nonzero = station_mac_address[0]
| station_mac_address[1]
| station_mac_address[2]
| station_mac_address[3]
| station_mac_address[4]
| station_mac_address[5];
if any_nonzero == 0 || station_mac_address[0] & 1 != 0 {
None
} else {
Some(Self {
station_mac_address,
})
}
}
pub const fn station_mac_address(self) -> [u8; 6] {
self.station_mac_address
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Ssid {
bytes: [u8; SSID_CAPACITY],
len: u8,
}
impl Ssid {
pub fn try_from_bytes(value: &[u8]) -> Option<Self> {
if value.is_empty() || value.len() > SSID_CAPACITY {
return None;
}
let mut bytes = [0; SSID_CAPACITY];
bytes[..value.len()].copy_from_slice(value);
Some(Self {
bytes,
len: value.len() as u8,
})
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len as usize]
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct Passphrase {
bytes: [u8; PASSPHRASE_CAPACITY],
len: u8,
}
impl Passphrase {
pub fn try_from_ascii(value: &[u8]) -> Option<Self> {
if !(8..=PASSPHRASE_CAPACITY).contains(&value.len())
|| value.iter().any(|byte| *byte < 32 || *byte == 127)
{
return None;
}
let mut bytes = [0; PASSPHRASE_CAPACITY];
bytes[..value.len()].copy_from_slice(value);
Some(Self {
bytes,
len: value.len() as u8,
})
}
pub fn expose_secret(&self) -> &[u8] {
&self.bytes[..self.len as usize]
}
}
impl Drop for Passphrase {
fn drop(&mut self) {
for byte in &mut self.bytes {
unsafe { core::ptr::write_volatile(byte, 0) };
}
self.len = 0;
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Security {
Open,
Wpa2Personal,
Wpa3Personal,
Wpa2Wpa3PersonalTransition,
OtherProtected,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScanResult {
pub ssid: Ssid,
pub bssid: [u8; 6],
pub frequency_mhz: u16,
pub rssi_dbm: i16,
pub security: Security,
pub channel: u8,
}
impl ScanResult {
pub(crate) const EMPTY: Self = Self {
ssid: Ssid {
bytes: [0; SSID_CAPACITY],
len: 0,
},
bssid: [0; 6],
frequency_mhz: 0,
rssi_dbm: 0,
security: Security::Open,
channel: 0,
};
pub const fn empty() -> Self {
Self::EMPTY
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScanConfig {
operation_timeout: OperationTimeout,
}
impl ScanConfig {
pub const fn new(operation_timeout: OperationTimeout) -> Self {
Self { operation_timeout }
}
pub const fn operation_timeout(self) -> OperationTimeout {
self.operation_timeout
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScanOutcome {
pub count: usize,
pub truncated: bool,
}
#[derive(Debug, Eq, PartialEq)]
pub struct StationConfig {
pub ssid: Ssid,
pub bssid: [u8; 6],
pub channel: u8,
pub passphrase: Passphrase,
security: PersonalSecurity,
operation_timeout: OperationTimeout,
}
impl StationConfig {
pub fn wpa2_personal(
result: &ScanResult,
passphrase: Passphrase,
operation_timeout: OperationTimeout,
) -> Option<Self> {
if !matches!(
result.security,
Security::Wpa2Personal | Security::Wpa2Wpa3PersonalTransition
) {
return None;
}
Some(Self {
ssid: result.ssid,
bssid: result.bssid,
channel: result.channel,
passphrase,
security: PersonalSecurity::Wpa2,
operation_timeout,
})
}
pub fn wpa3_personal(
result: &ScanResult,
passphrase: Passphrase,
sae_pwe: SaePwe,
operation_timeout: OperationTimeout,
) -> Option<Self> {
if !matches!(
result.security,
Security::Wpa3Personal | Security::Wpa2Wpa3PersonalTransition
) {
return None;
}
Some(Self {
ssid: result.ssid,
bssid: result.bssid,
channel: result.channel,
passphrase,
security: PersonalSecurity::Wpa3 { sae_pwe },
operation_timeout,
})
}
pub const fn security(&self) -> PersonalSecurity {
self.security
}
pub const fn operation_timeout(&self) -> OperationTimeout {
self.operation_timeout
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ConnectionInfo {
pub bssid: [u8; 6],
pub frequency_mhz: u16,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BackendErrorClass {
Initialize,
Busy,
OperationTimeout,
BackendTimeout,
Cancelled,
ResourceUnavailable,
UnsupportedSecurity,
Connect,
Other,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BackendError {
class: BackendErrorClass,
code: u32,
stage: DiagnosticStage,
profile_revision: Option<&'static str>,
trace: DiagnosticTrace,
}
impl BackendError {
pub const fn new(class: BackendErrorClass, code: u32) -> Self {
let stage = match class {
BackendErrorClass::Initialize => DiagnosticStage::Initialize,
BackendErrorClass::UnsupportedSecurity | BackendErrorClass::Connect => {
DiagnosticStage::Connect
}
BackendErrorClass::Busy
| BackendErrorClass::OperationTimeout
| BackendErrorClass::Cancelled => DiagnosticStage::Operation,
BackendErrorClass::BackendTimeout => DiagnosticStage::Backend,
BackendErrorClass::ResourceUnavailable => DiagnosticStage::Runtime,
BackendErrorClass::Other => DiagnosticStage::Backend,
};
Self {
class,
code,
stage,
profile_revision: None,
trace: DiagnosticTrace::new(),
}
}
pub const fn class(self) -> BackendErrorClass {
self.class
}
pub const fn code(self) -> u32 {
self.code
}
pub const fn with_stage(mut self, stage: DiagnosticStage) -> Self {
self.stage = stage;
self
}
pub const fn with_profile_revision(mut self, revision: &'static str) -> Self {
self.profile_revision = Some(revision);
self
}
pub fn with_trace(mut self, kind: DiagnosticTraceKind, value: u32) -> Self {
self.trace.push(kind, value);
self
}
pub(crate) const fn stage(self) -> DiagnosticStage {
self.stage
}
pub(crate) const fn profile_revision(self) -> Option<&'static str> {
self.profile_revision
}
pub(crate) const fn trace(self) -> DiagnosticTrace {
self.trace
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WifiEvent {
Initialized,
ScanCompleted { count: usize, truncated: bool },
Connected(ConnectionInfo),
Disconnected { reason: u16 },
Failed(BackendError),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EventDiagnostics {
pub capacity: usize,
pub accepted: u32,
pub consumed: u32,
pub pending: usize,
pub high_water: usize,
pub dropped: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BlockingRunnerDiagnostics {
pub command_queue_pending: usize,
pub command_queue_high_water: usize,
pub run_once_calls: u32,
pub commands_processed: u32,
pub backend_poll_calls: u32,
pub backend_poll_work_batches: u32,
pub backend_poll_errors: u32,
pub immediate_repoll_hints: u32,
}
pub trait WifiBackend {
fn initialize(&mut self, config: &WifiConfig) -> Result<(), BackendError>;
fn scan(
&mut self,
config: ScanConfig,
output: &mut [ScanResult],
) -> Result<ScanOutcome, BackendError>;
fn connect(&mut self, config: &StationConfig) -> Result<ConnectionInfo, BackendError>;
fn disconnect(&mut self, config: &WifiConfig) -> Result<(), BackendError>;
fn l2_capabilities(&self) -> Option<WifiL2Capabilities> {
None
}
fn poll(&mut self) -> Result<bool, BackendError> {
Ok(false)
}
}
pub struct RadioResources<B, D> {
pub backend: B,
pub device: D,
}
pub struct RadioState<const EVENTS: usize> {
pub(crate) shared: SharedState<EVENTS>,
}
impl<const EVENTS: usize> RadioState<EVENTS> {
pub const fn new() -> Self {
Self {
shared: SharedState::new(),
}
}
}
impl<const EVENTS: usize> Default for RadioState<EVENTS> {
fn default() -> Self {
Self::new()
}
}
pub struct RadioController<B, D, const EVENTS: usize> {
config: RadioConfig,
resources: RadioResources<B, D>,
state: &'static RadioState<EVENTS>,
}
pub fn init<B, D, const EVENTS: usize>(
config: RadioConfig,
resources: RadioResources<B, D>,
state: &'static RadioState<EVENTS>,
) -> Result<RadioController<B, D, EVENTS>, Error> {
if !state.shared.claim() {
return Err(Error::AlreadyInitialized);
}
Ok(RadioController {
config,
resources,
state,
})
}
impl<B, D, const EVENTS: usize> RadioController<B, D, EVENTS> {
pub fn split(self) -> RadioParts<B, D, EVENTS> {
let (wifi, backend, config, state) = self.split_components();
RadioParts {
wifi,
runner: RadioRunner {
backend,
config,
state,
last_poll_error: None,
},
}
}
pub(crate) fn split_components(
self,
) -> (
WifiParts<D, EVENTS>,
B,
WifiConfig,
&'static RadioState<EVENTS>,
) {
(
WifiParts {
controller: WifiController {
state: self.state,
next_sequence: 0,
},
device: WifiDevice {
inner: self.resources.device,
l2_capabilities: &self.state.shared.l2_capabilities,
},
},
self.resources.backend,
self.config.wifi,
self.state,
)
}
}
pub struct RadioParts<B, D, const EVENTS: usize> {
pub wifi: WifiParts<D, EVENTS>,
pub runner: RadioRunner<B, EVENTS>,
}
pub struct WifiParts<D, const EVENTS: usize> {
pub controller: WifiController<EVENTS>,
pub device: WifiDevice<D>,
}
pub struct WifiController<const EVENTS: usize> {
state: &'static RadioState<EVENTS>,
next_sequence: u32,
}
struct OperationCancellation<const EVENTS: usize> {
state: &'static RadioState<EVENTS>,
sequence: u32,
armed: bool,
}
impl<const EVENTS: usize> OperationCancellation<EVENTS> {
const fn new(state: &'static RadioState<EVENTS>, sequence: u32) -> Self {
Self {
state,
sequence,
armed: true,
}
}
fn complete(&mut self) {
self.armed = false;
}
}
impl<const EVENTS: usize> Drop for OperationCancellation<EVENTS> {
fn drop(&mut self) {
if self.armed {
let _ = self.state.shared.cancellations.try_send(self.sequence);
}
}
}
impl<const EVENTS: usize> WifiController<EVENTS> {
pub async fn initialize(&mut self) -> Result<(), Error> {
let sequence = self.allocate_sequence();
self.send_command(Command {
sequence,
kind: CommandKind::Initialize,
})
.await;
let mut cancellation = OperationCancellation::new(self.state, sequence);
loop {
let completion = self.state.shared.completion.wait().await;
if completion.sequence != sequence {
continue;
}
let result = match completion.kind {
CompletionKind::Initialize(result) => result.map_err(Error::Backend),
#[cfg(feature = "incremental-backend-experiment")]
CompletionKind::Protocol => Err(Error::Protocol),
_ => Err(Error::Protocol),
};
cancellation.complete();
return result;
}
}
pub async fn scan(
&mut self,
config: ScanConfig,
output: &mut [ScanResult],
) -> Result<ScanOutcome, Error> {
let sequence = self.allocate_sequence();
self.send_command(Command {
sequence,
kind: CommandKind::Scan(config),
})
.await;
let mut cancellation = OperationCancellation::new(self.state, sequence);
loop {
let completion = self.state.shared.completion.wait().await;
if completion.sequence != sequence {
continue;
}
let result = match completion.kind {
CompletionKind::Scan(result) => match result {
Ok(backend) => {
let count = backend.count.min(output.len());
output[..count].copy_from_slice(&self.state.shared.scan_results()[..count]);
Ok(ScanOutcome {
count,
truncated: backend.truncated || backend.count > output.len(),
})
}
Err(error) => Err(Error::Backend(error)),
},
#[cfg(feature = "incremental-backend-experiment")]
CompletionKind::Protocol => Err(Error::Protocol),
_ => Err(Error::Protocol),
};
cancellation.complete();
return result;
}
}
pub async fn connect(&mut self, config: StationConfig) -> Result<ConnectionInfo, Error> {
let sequence = self.allocate_sequence();
self.send_command(Command {
sequence,
kind: CommandKind::Connect(config),
})
.await;
let mut cancellation = OperationCancellation::new(self.state, sequence);
loop {
let completion = self.state.shared.completion.wait().await;
if completion.sequence != sequence {
continue;
}
let result = match completion.kind {
CompletionKind::Connect(result) => result.map_err(Error::Backend),
#[cfg(feature = "incremental-backend-experiment")]
CompletionKind::Protocol => Err(Error::Protocol),
_ => Err(Error::Protocol),
};
cancellation.complete();
return result;
}
}
pub async fn disconnect(&mut self) -> Result<(), Error> {
let sequence = self.allocate_sequence();
self.send_command(Command {
sequence,
kind: CommandKind::Disconnect,
})
.await;
let mut cancellation = OperationCancellation::new(self.state, sequence);
loop {
let completion = self.state.shared.completion.wait().await;
if completion.sequence != sequence {
continue;
}
let result = match completion.kind {
CompletionKind::Disconnect(result) => result.map_err(Error::Backend),
#[cfg(feature = "incremental-backend-experiment")]
CompletionKind::Protocol => Err(Error::Protocol),
_ => Err(Error::Protocol),
};
cancellation.complete();
return result;
}
}
pub async fn next_event(&mut self) -> WifiEvent {
let event = self.state.shared.events.receive().await;
saturating_increment(&self.state.shared.consumed_events);
event
}
pub fn event_diagnostics(&self) -> EventDiagnostics {
EventDiagnostics {
capacity: EVENTS,
accepted: self.state.shared.accepted_events.load(Ordering::Relaxed),
consumed: self.state.shared.consumed_events.load(Ordering::Relaxed),
pending: self.state.shared.events.len(),
high_water: usize::try_from(self.state.shared.event_high_water.load(Ordering::Relaxed))
.unwrap_or(usize::MAX),
dropped: self.state.shared.dropped_events.load(Ordering::Relaxed),
}
}
pub fn blocking_runner_diagnostics(&self) -> BlockingRunnerDiagnostics {
BlockingRunnerDiagnostics {
command_queue_pending: self.state.shared.commands.len(),
command_queue_high_water: usize::try_from(
self.state.shared.command_high_water.load(Ordering::Relaxed),
)
.unwrap_or(usize::MAX),
run_once_calls: self.state.shared.run_once_calls.load(Ordering::Relaxed),
commands_processed: self.state.shared.commands_processed.load(Ordering::Relaxed),
backend_poll_calls: self.state.shared.backend_poll_calls.load(Ordering::Relaxed),
backend_poll_work_batches: self
.state
.shared
.backend_poll_work_batches
.load(Ordering::Relaxed),
backend_poll_errors: self
.state
.shared
.backend_poll_errors
.load(Ordering::Relaxed),
immediate_repoll_hints: self
.state
.shared
.immediate_repoll_hints
.load(Ordering::Relaxed),
}
}
#[cfg(feature = "incremental-backend-experiment")]
pub fn incremental_runner_diagnostics(&self) -> crate::IncrementalRunnerDiagnostics {
self.state.shared.incremental_diagnostics.snapshot()
}
fn allocate_sequence(&mut self) -> u32 {
self.next_sequence = self.next_sequence.wrapping_add(1);
if self.next_sequence == 0 {
self.next_sequence = 1;
}
self.next_sequence
}
async fn send_command(&self, command: Command) {
self.state.shared.commands.send(command).await;
self.state.shared.record_command_accepted();
}
}
pub struct RadioRunner<B, const EVENTS: usize> {
backend: B,
config: WifiConfig,
state: &'static RadioState<EVENTS>,
last_poll_error: Option<BackendError>,
}
impl<B: WifiBackend, const EVENTS: usize> RadioRunner<B, EVENTS> {
pub fn run_once(&mut self) -> bool {
saturating_increment(&self.state.shared.run_once_calls);
let mut did_work = false;
if let Ok(command) = self.state.shared.commands.try_receive() {
self.process_or_cancel_command(command);
did_work = true;
}
saturating_increment(&self.state.shared.backend_poll_calls);
let immediate_repoll = match self.backend.poll() {
Ok(background_work) => {
self.last_poll_error = None;
if background_work {
saturating_increment(&self.state.shared.backend_poll_work_batches);
}
did_work || background_work
}
Err(error) => {
saturating_increment(&self.state.shared.backend_poll_errors);
if self.last_poll_error != Some(error) {
self.state.shared.publish_event(WifiEvent::Failed(error));
self.last_poll_error = Some(error);
true
} else {
did_work
}
}
};
if immediate_repoll {
saturating_increment(&self.state.shared.immediate_repoll_hints);
}
immediate_repoll
}
pub async fn run(mut self) -> ! {
loop {
let command = self.state.shared.commands.receive().await;
self.process_or_cancel_command(command);
}
}
fn process_or_cancel_command(&mut self, command: Command) {
while let Ok(sequence) = self.state.shared.cancellations.try_receive() {
if sequence == command.sequence {
let error = BackendError::new(BackendErrorClass::Cancelled, 0);
self.state.shared.publish_event(WifiEvent::Failed(error));
self.state.shared.completion.signal(Completion {
sequence,
kind: match command.kind {
CommandKind::Initialize => CompletionKind::Initialize(Err(error)),
CommandKind::Scan(_) => CompletionKind::Scan(Err(error)),
CommandKind::Connect(_) => CompletionKind::Connect(Err(error)),
CommandKind::Disconnect => CompletionKind::Disconnect(Err(error)),
},
});
return;
}
}
self.process_command(command);
}
fn process_command(&mut self, command: Command) {
saturating_increment(&self.state.shared.commands_processed);
let sequence = command.sequence;
let completion = match command.kind {
CommandKind::Initialize => {
let result = self.backend.initialize(&self.config);
if result.is_ok()
&& let Some(capabilities) = self.backend.l2_capabilities()
{
self.state.shared.l2_capabilities.publish_once(capabilities);
}
self.publish_result(result, WifiEvent::Initialized);
CompletionKind::Initialize(result)
}
CommandKind::Scan(config) => {
let output = unsafe { &mut *self.state.shared.scan_results_ptr() };
let result = self.backend.scan(config, output);
match result {
Ok(outcome) => self.state.shared.publish_event(WifiEvent::ScanCompleted {
count: outcome.count,
truncated: outcome.truncated,
}),
Err(error) => self.state.shared.publish_event(WifiEvent::Failed(error)),
}
CompletionKind::Scan(result)
}
CommandKind::Connect(config) => {
let result = self.backend.connect(&config);
match result {
Ok(info) => self.state.shared.publish_event(WifiEvent::Connected(info)),
Err(error) => self.state.shared.publish_event(WifiEvent::Failed(error)),
}
CompletionKind::Connect(result)
}
CommandKind::Disconnect => {
let result = self.backend.disconnect(&self.config);
self.publish_result(result, WifiEvent::Disconnected { reason: 0 });
CompletionKind::Disconnect(result)
}
};
self.state.shared.completion.signal(Completion {
sequence,
kind: completion,
});
}
fn publish_result(&self, result: Result<(), BackendError>, success: WifiEvent) {
self.state.shared.publish_event(match result {
Ok(()) => success,
Err(error) => WifiEvent::Failed(error),
});
}
}
pub struct WifiDevice<D> {
inner: D,
l2_capabilities: &'static crate::state::L2CapabilityState,
}
impl<D> WifiDevice<D> {
pub fn l2_capabilities(&self) -> Option<WifiL2Capabilities> {
self.l2_capabilities.snapshot()
}
pub fn station_mac_address(&self) -> Option<[u8; 6]> {
self.l2_capabilities()
.map(WifiL2Capabilities::station_mac_address)
}
pub fn inner(&self) -> &D {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut D {
&mut self.inner
}
pub fn into_inner(self) -> D {
self.inner
}
}
#[cfg(feature = "smoltcp")]
impl<D: smoltcp::phy::Device> smoltcp::phy::Device for WifiDevice<D> {
type RxToken<'a>
= D::RxToken<'a>
where
Self: 'a;
type TxToken<'a>
= D::TxToken<'a>
where
Self: 'a;
fn receive(
&mut self,
timestamp: smoltcp::time::Instant,
) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
self.inner.receive(timestamp)
}
fn transmit(&mut self, timestamp: smoltcp::time::Instant) -> Option<Self::TxToken<'_>> {
self.inner.transmit(timestamp)
}
fn capabilities(&self) -> smoltcp::phy::DeviceCapabilities {
self.inner.capabilities()
}
}
pub(crate) struct Command {
pub(crate) sequence: u32,
pub(crate) kind: CommandKind,
}
pub(crate) enum CommandKind {
Initialize,
Scan(ScanConfig),
Connect(StationConfig),
Disconnect,
}
#[derive(Clone, Copy)]
pub(crate) struct Completion {
pub(crate) sequence: u32,
pub(crate) kind: CompletionKind,
}
#[derive(Clone, Copy)]
pub(crate) enum CompletionKind {
Initialize(Result<(), BackendError>),
Scan(Result<ScanOutcome, BackendError>),
Connect(Result<ConnectionInfo, BackendError>),
Disconnect(Result<(), BackendError>),
#[cfg(feature = "incremental-backend-experiment")]
Protocol,
}
#[cfg(test)]
mod tests {
extern crate std;
use core::future::Future;
use core::task::{Context, Poll, Waker};
use std::boxed::Box;
use super::*;
struct MockBackend {
calls: u8,
poll_work: bool,
poll_error: Option<BackendError>,
initialize_error: Option<BackendError>,
station_mac_address: [u8; 6],
}
impl Default for MockBackend {
fn default() -> Self {
Self {
calls: 0,
poll_work: false,
poll_error: None,
initialize_error: None,
station_mac_address: [0x02, 1, 2, 3, 4, 5],
}
}
}
impl WifiBackend for MockBackend {
fn initialize(&mut self, _: &WifiConfig) -> Result<(), BackendError> {
self.calls += 1;
self.initialize_error.map_or(Ok(()), Err)
}
fn scan(
&mut self,
_: ScanConfig,
output: &mut [ScanResult],
) -> Result<ScanOutcome, BackendError> {
self.calls += 1;
output[0] = ScanResult {
ssid: Ssid::try_from_bytes(b"test-ap").unwrap(),
bssid: [1, 2, 3, 4, 5, 6],
frequency_mhz: 2437,
rssi_dbm: -42,
security: Security::Wpa2Personal,
channel: 6,
};
Ok(ScanOutcome {
count: 1,
truncated: false,
})
}
fn connect(&mut self, config: &StationConfig) -> Result<ConnectionInfo, BackendError> {
self.calls += 1;
Ok(ConnectionInfo {
bssid: config.bssid,
frequency_mhz: 2437,
})
}
fn disconnect(&mut self, _: &WifiConfig) -> Result<(), BackendError> {
self.calls += 1;
Ok(())
}
fn l2_capabilities(&self) -> Option<WifiL2Capabilities> {
WifiL2Capabilities::try_new(self.station_mac_address)
}
fn poll(&mut self) -> Result<bool, BackendError> {
if let Some(error) = self.poll_error {
Err(error)
} else {
Ok(core::mem::take(&mut self.poll_work))
}
}
}
fn poll<F: Future>(future: core::pin::Pin<&mut F>) -> Poll<F::Output> {
let waker = Waker::noop();
future.poll(&mut Context::from_waker(waker))
}
#[test]
fn runner_is_the_only_backend_execution_path() {
let state = Box::leak(Box::new(RadioState::<4>::new()));
let radio = init(
RadioConfig::default(),
RadioResources {
backend: MockBackend::default(),
device: (),
},
state,
)
.unwrap();
let RadioParts {
mut wifi,
mut runner,
} = radio.split();
assert_eq!(wifi.device.l2_capabilities(), None);
{
let mut initialize = core::pin::pin!(wifi.controller.initialize());
assert!(poll(initialize.as_mut()).is_pending());
assert_eq!(state.shared.commands.len(), 1);
assert_eq!(state.shared.command_high_water.load(Ordering::Relaxed), 1);
assert!(runner.run_once());
assert_eq!(poll(initialize.as_mut()), Poll::Ready(Ok(())));
}
assert_eq!(
wifi.device.l2_capabilities(),
WifiL2Capabilities::try_new([0x02, 1, 2, 3, 4, 5])
);
assert_eq!(
wifi.device.station_mac_address(),
Some([0x02, 1, 2, 3, 4, 5])
);
let mut results = [ScanResult::EMPTY; 1];
{
let mut scan = core::pin::pin!(wifi.controller.scan(
ScanConfig::new(OperationTimeout::try_from_millis(1_000).unwrap()),
&mut results,
));
assert!(poll(scan.as_mut()).is_pending());
assert!(runner.run_once());
assert_eq!(
poll(scan.as_mut()),
Poll::Ready(Ok(ScanOutcome {
count: 1,
truncated: false,
}))
);
}
assert_eq!(results[0].ssid.as_bytes(), b"test-ap");
}
#[test]
fn failed_initialization_does_not_publish_l2_capabilities() {
let state = Box::leak(Box::new(RadioState::<2>::new()));
let error = BackendError::new(BackendErrorClass::Initialize, 7);
let radio = init(
RadioConfig::default(),
RadioResources {
backend: MockBackend {
initialize_error: Some(error),
..MockBackend::default()
},
device: (),
},
state,
)
.unwrap();
let RadioParts {
mut wifi,
mut runner,
} = radio.split();
let mut initialize = core::pin::pin!(wifi.controller.initialize());
assert!(poll(initialize.as_mut()).is_pending());
assert!(runner.run_once());
assert_eq!(
poll(initialize.as_mut()),
Poll::Ready(Err(Error::Backend(error)))
);
assert_eq!(wifi.device.l2_capabilities(), None);
assert_eq!(wifi.device.station_mac_address(), None);
}
#[test]
fn l2_capabilities_are_owned_by_each_radio_instance() {
let state_a = Box::leak(Box::new(RadioState::<2>::new()));
let state_b = Box::leak(Box::new(RadioState::<2>::new()));
let mac_a = [0x02, 1, 1, 1, 1, 1];
let mac_b = [0x02, 2, 2, 2, 2, 2];
let radio_a = init(
RadioConfig::default(),
RadioResources {
backend: MockBackend {
station_mac_address: mac_a,
..MockBackend::default()
},
device: (),
},
state_a,
)
.unwrap();
let radio_b = init(
RadioConfig::default(),
RadioResources {
backend: MockBackend {
station_mac_address: mac_b,
..MockBackend::default()
},
device: (),
},
state_b,
)
.unwrap();
let RadioParts {
wifi: mut wifi_a,
runner: mut runner_a,
} = radio_a.split();
let RadioParts {
wifi: mut wifi_b,
runner: mut runner_b,
} = radio_b.split();
let mut initialize_a = core::pin::pin!(wifi_a.controller.initialize());
let mut initialize_b = core::pin::pin!(wifi_b.controller.initialize());
assert!(poll(initialize_a.as_mut()).is_pending());
assert!(poll(initialize_b.as_mut()).is_pending());
assert!(runner_a.run_once());
assert!(runner_b.run_once());
assert_eq!(poll(initialize_a.as_mut()), Poll::Ready(Ok(())));
assert_eq!(poll(initialize_b.as_mut()), Poll::Ready(Ok(())));
assert_eq!(wifi_a.device.station_mac_address(), Some(mac_a));
assert_eq!(wifi_b.device.station_mac_address(), Some(mac_b));
}
#[test]
fn bounded_events_drop_the_oldest_and_report_overflow() {
let state = Box::leak(Box::new(RadioState::<1>::new()));
let radio = init(
RadioConfig::default(),
RadioResources {
backend: MockBackend::default(),
device: (),
},
state,
)
.unwrap();
let RadioParts {
mut wifi,
mut runner,
} = radio.split();
for _ in 0..2 {
let mut initialize = core::pin::pin!(wifi.controller.initialize());
assert!(poll(initialize.as_mut()).is_pending());
assert!(runner.run_once());
assert_eq!(poll(initialize.as_mut()), Poll::Ready(Ok(())));
}
assert_eq!(
wifi.controller.event_diagnostics(),
EventDiagnostics {
capacity: 1,
accepted: 2,
consumed: 0,
pending: 1,
high_water: 1,
dropped: 1,
}
);
}
#[test]
fn runner_advances_background_work_without_a_command() {
let state = Box::leak(Box::new(RadioState::<2>::new()));
let radio = init(
RadioConfig::default(),
RadioResources {
backend: MockBackend {
poll_work: true,
..MockBackend::default()
},
device: (),
},
state,
)
.unwrap();
let mut runner = radio.split().runner;
assert!(runner.run_once());
assert!(!runner.run_once());
}
#[test]
fn repeated_background_error_publishes_one_event() {
let state = Box::leak(Box::new(RadioState::<2>::new()));
let error = BackendError::new(BackendErrorClass::Other, 0x55);
let radio = init(
RadioConfig::default(),
RadioResources {
backend: MockBackend {
poll_error: Some(error),
..MockBackend::default()
},
device: (),
},
state,
)
.unwrap();
let RadioParts {
mut wifi,
mut runner,
} = radio.split();
assert!(runner.run_once());
assert!(!runner.run_once());
assert_eq!(
wifi.controller.event_diagnostics(),
EventDiagnostics {
capacity: 2,
accepted: 1,
consumed: 0,
pending: 1,
high_water: 1,
dropped: 0,
}
);
{
let mut event = core::pin::pin!(wifi.controller.next_event());
assert_eq!(poll(event.as_mut()), Poll::Ready(WifiEvent::Failed(error)));
}
assert_eq!(
wifi.controller.event_diagnostics(),
EventDiagnostics {
capacity: 2,
accepted: 1,
consumed: 1,
pending: 0,
high_water: 1,
dropped: 0,
}
);
}
#[test]
fn blocking_runner_diagnostics_count_bounded_work() {
let state = Box::leak(Box::new(RadioState::<2>::new()));
let error = BackendError::new(BackendErrorClass::Other, 0x55);
let radio = init(
RadioConfig::default(),
RadioResources {
backend: MockBackend {
poll_work: true,
..MockBackend::default()
},
device: (),
},
state,
)
.unwrap();
let RadioParts {
mut wifi,
mut runner,
} = radio.split();
{
let mut initialize = core::pin::pin!(wifi.controller.initialize());
assert!(poll(initialize.as_mut()).is_pending());
assert!(runner.run_once());
assert_eq!(poll(initialize.as_mut()), Poll::Ready(Ok(())));
}
assert!(!runner.run_once());
runner.backend.poll_error = Some(error);
assert!(runner.run_once());
assert!(!runner.run_once());
assert_eq!(
wifi.controller.blocking_runner_diagnostics(),
BlockingRunnerDiagnostics {
command_queue_pending: 0,
command_queue_high_water: 1,
run_once_calls: 4,
commands_processed: 1,
backend_poll_calls: 4,
backend_poll_work_batches: 1,
backend_poll_errors: 2,
immediate_repoll_hints: 2,
}
);
}
#[test]
fn cancelled_control_future_cannot_poison_the_next_command() {
let state = Box::leak(Box::new(RadioState::<2>::new()));
let radio = init(
RadioConfig::default(),
RadioResources {
backend: MockBackend::default(),
device: (),
},
state,
)
.unwrap();
let RadioParts {
mut wifi,
mut runner,
} = radio.split();
{
let mut cancelled = core::pin::pin!(wifi.controller.initialize());
assert!(poll(cancelled.as_mut()).is_pending());
}
assert!(runner.run_once());
assert_eq!(runner.backend.calls, 0);
let mut next = core::pin::pin!(wifi.controller.initialize());
assert!(poll(next.as_mut()).is_pending());
assert!(runner.run_once());
assert_eq!(runner.backend.calls, 1);
assert_eq!(poll(next.as_mut()), Poll::Ready(Ok(())));
}
#[test]
fn validated_configuration_rejects_invalid_inputs() {
assert!(Ssid::try_from_bytes(b"").is_none());
assert!(Ssid::try_from_bytes(&[b'x'; 33]).is_none());
assert!(Passphrase::try_from_ascii(b"short").is_none());
assert!(Passphrase::try_from_ascii(b"testtest").is_some());
assert!(OperationTimeout::try_from_millis(0).is_none());
assert!(BackendTimeout::try_from_millis(0).is_none());
assert_eq!(OperationTimeout::try_from_millis(1).unwrap().as_millis(), 1);
assert_eq!(BackendTimeout::try_from_millis(1).unwrap().as_millis(), 1);
assert!(WifiL2Capabilities::try_new([0; 6]).is_none());
assert!(WifiL2Capabilities::try_new([1, 2, 3, 4, 5, 6]).is_none());
assert_eq!(
WifiL2Capabilities::try_new([0x02, 1, 2, 3, 4, 5])
.unwrap()
.station_mac_address(),
[0x02, 1, 2, 3, 4, 5]
);
}
#[test]
fn wpa3_config_requires_wpa3_scan_and_implies_required_pmf() {
let result = ScanResult {
ssid: Ssid::try_from_bytes(b"wpa3-ap").unwrap(),
bssid: [1, 2, 3, 4, 5, 6],
frequency_mhz: 5180,
rssi_dbm: -38,
security: Security::Wpa3Personal,
channel: 36,
};
let config = StationConfig::wpa3_personal(
&result,
Passphrase::try_from_ascii(b"testtest").unwrap(),
SaePwe::Both,
OperationTimeout::try_from_millis(10_000).unwrap(),
)
.unwrap();
assert_eq!(
config.security(),
PersonalSecurity::Wpa3 {
sae_pwe: SaePwe::Both
}
);
assert_eq!(
config.security().management_frame_protection(),
ManagementFrameProtection::Required
);
}
#[test]
fn transition_scan_requires_an_explicit_personal_mode_choice() {
let result = ScanResult {
ssid: Ssid::try_from_bytes(b"transition-ap").unwrap(),
bssid: [1, 2, 3, 4, 5, 6],
frequency_mhz: 5180,
rssi_dbm: -38,
security: Security::Wpa2Wpa3PersonalTransition,
channel: 36,
};
let wpa2 = StationConfig::wpa2_personal(
&result,
Passphrase::try_from_ascii(b"testtest").unwrap(),
OperationTimeout::try_from_millis(10_000).unwrap(),
)
.unwrap();
assert_eq!(wpa2.security(), PersonalSecurity::Wpa2);
let wpa3 = StationConfig::wpa3_personal(
&result,
Passphrase::try_from_ascii(b"testtest").unwrap(),
SaePwe::Both,
OperationTimeout::try_from_millis(10_000).unwrap(),
)
.unwrap();
assert_eq!(
wpa3.security(),
PersonalSecurity::Wpa3 {
sae_pwe: SaePwe::Both
}
);
}
}