Skip to main content

hisi_rf_core/
wifi.rs

1use portable_atomic::Ordering;
2
3use crate::state::{SharedState, saturating_increment};
4use crate::{DiagnosticStage, DiagnosticTrace, DiagnosticTraceKind, Error};
5
6pub mod security;
7pub use security::{ManagementFrameProtection, PersonalSecurity, SaePwe};
8
9pub(crate) const MAX_SCAN_RESULTS: usize = 32;
10const SSID_CAPACITY: usize = 32;
11const PASSPHRASE_CAPACITY: usize = 63;
12
13/// Radio-wide configuration.
14#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15#[non_exhaustive]
16pub struct RadioConfig {
17    /// Wi-Fi control-plane defaults.
18    pub wifi: WifiConfig,
19}
20
21/// Wi-Fi control-plane defaults.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub struct WifiConfig {
24    /// Backend lifecycle timeout for radio initialization.
25    pub initialize_timeout: BackendTimeout,
26    /// Backend lifecycle timeout for disconnect cleanup.
27    pub disconnect_timeout: BackendTimeout,
28}
29
30impl Default for WifiConfig {
31    fn default() -> Self {
32        Self {
33            initialize_timeout: BackendTimeout::from_millis_const(30_000),
34            disconnect_timeout: BackendTimeout::from_millis_const(10_000),
35        }
36    }
37}
38
39/// Non-zero end-to-end timeout for one protocol operation.
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct OperationTimeout(u32);
42
43impl OperationTimeout {
44    /// Validate a non-zero timeout in milliseconds.
45    pub const fn try_from_millis(milliseconds: u32) -> Option<Self> {
46        if milliseconds == 0 {
47            None
48        } else {
49            Some(Self(milliseconds))
50        }
51    }
52
53    /// Return the timeout in milliseconds.
54    pub const fn as_millis(self) -> u32 {
55        self.0
56    }
57}
58
59/// Non-zero timeout for a bounded backend or vendor lifecycle call.
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub struct BackendTimeout(u32);
62
63impl BackendTimeout {
64    /// Validate a non-zero timeout in milliseconds.
65    pub const fn try_from_millis(milliseconds: u32) -> Option<Self> {
66        if milliseconds == 0 {
67            None
68        } else {
69            Some(Self(milliseconds))
70        }
71    }
72
73    /// Return the timeout in milliseconds.
74    pub const fn as_millis(self) -> u32 {
75        self.0
76    }
77
78    const fn from_millis_const(milliseconds: u32) -> Self {
79        assert!(milliseconds != 0);
80        Self(milliseconds)
81    }
82}
83
84/// Immutable link-layer identity published by one initialized Wi-Fi backend.
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub struct WifiL2Capabilities {
87    station_mac_address: [u8; 6],
88}
89
90impl WifiL2Capabilities {
91    /// Validate a non-zero unicast station MAC address.
92    pub const fn try_new(station_mac_address: [u8; 6]) -> Option<Self> {
93        let any_nonzero = station_mac_address[0]
94            | station_mac_address[1]
95            | station_mac_address[2]
96            | station_mac_address[3]
97            | station_mac_address[4]
98            | station_mac_address[5];
99        if any_nonzero == 0 || station_mac_address[0] & 1 != 0 {
100            None
101        } else {
102            Some(Self {
103                station_mac_address,
104            })
105        }
106    }
107
108    /// Return the station MAC address owned by this radio instance.
109    pub const fn station_mac_address(self) -> [u8; 6] {
110        self.station_mac_address
111    }
112}
113
114/// Validated IEEE 802.11 SSID bytes.
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub struct Ssid {
117    bytes: [u8; SSID_CAPACITY],
118    len: u8,
119}
120
121impl Ssid {
122    /// Validate and copy a non-empty SSID of at most 32 bytes.
123    pub fn try_from_bytes(value: &[u8]) -> Option<Self> {
124        if value.is_empty() || value.len() > SSID_CAPACITY {
125            return None;
126        }
127        let mut bytes = [0; SSID_CAPACITY];
128        bytes[..value.len()].copy_from_slice(value);
129        Some(Self {
130            bytes,
131            len: value.len() as u8,
132        })
133    }
134
135    /// Return the exact SSID bytes.
136    pub fn as_bytes(&self) -> &[u8] {
137        &self.bytes[..self.len as usize]
138    }
139}
140
141/// Owned WPA2/WPA3-Personal passphrase that is erased on drop.
142#[derive(Debug, Eq, PartialEq)]
143pub struct Passphrase {
144    bytes: [u8; PASSPHRASE_CAPACITY],
145    len: u8,
146}
147
148impl Passphrase {
149    /// Validate and copy an 8-63 byte printable ASCII passphrase.
150    pub fn try_from_ascii(value: &[u8]) -> Option<Self> {
151        if !(8..=PASSPHRASE_CAPACITY).contains(&value.len())
152            || value.iter().any(|byte| *byte < 32 || *byte == 127)
153        {
154            return None;
155        }
156        let mut bytes = [0; PASSPHRASE_CAPACITY];
157        bytes[..value.len()].copy_from_slice(value);
158        Some(Self {
159            bytes,
160            len: value.len() as u8,
161        })
162    }
163
164    /// Borrow the passphrase bytes for a backend call.
165    pub fn expose_secret(&self) -> &[u8] {
166        &self.bytes[..self.len as usize]
167    }
168}
169
170impl Drop for Passphrase {
171    fn drop(&mut self) {
172        for byte in &mut self.bytes {
173            // Volatile stores keep secret erasure observable to the compiler.
174            // SAFETY: `byte` uniquely borrows one live element of this owned
175            // array and is valid for a one-byte volatile write.
176            unsafe { core::ptr::write_volatile(byte, 0) };
177        }
178        self.len = 0;
179    }
180}
181
182/// Link-layer security discovered during scan.
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub enum Security {
185    /// Open network.
186    Open,
187    /// WPA2-Personal with CCMP.
188    Wpa2Personal,
189    /// WPA3-Personal with SAE and mandatory PMF.
190    Wpa3Personal,
191    /// WPA2/WPA3-Personal transition BSS advertising both PSK and SAE.
192    ///
193    /// Applications must explicitly choose WPA2 or WPA3 when constructing the
194    /// station configuration; discovery never silently downgrades the link.
195    Wpa2Wpa3PersonalTransition,
196    /// A protected mode not yet represented by this public API.
197    OtherProtected,
198}
199
200/// One chip-neutral bounded scan result.
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub struct ScanResult {
203    /// Advertised SSID.
204    pub ssid: Ssid,
205    /// Basic service set identifier.
206    pub bssid: [u8; 6],
207    /// Center frequency in MHz.
208    pub frequency_mhz: u16,
209    /// Signal strength in dBm.
210    pub rssi_dbm: i16,
211    /// Link-layer security class.
212    pub security: Security,
213    /// Primary channel when known, otherwise zero.
214    pub channel: u8,
215}
216
217impl ScanResult {
218    pub(crate) const EMPTY: Self = Self {
219        ssid: Ssid {
220            bytes: [0; SSID_CAPACITY],
221            len: 0,
222        },
223        bssid: [0; 6],
224        frequency_mhz: 0,
225        rssi_dbm: 0,
226        security: Security::Open,
227        channel: 0,
228    };
229
230    /// Empty value for caller-provided fixed scan buffers.
231    pub const fn empty() -> Self {
232        Self::EMPTY
233    }
234}
235
236/// Bounded station scan request.
237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
238pub struct ScanConfig {
239    operation_timeout: OperationTimeout,
240}
241
242impl ScanConfig {
243    /// Construct a bounded scan request.
244    pub const fn new(operation_timeout: OperationTimeout) -> Self {
245        Self { operation_timeout }
246    }
247
248    /// End-to-end scan timeout enforced by the protocol backend.
249    pub const fn operation_timeout(self) -> OperationTimeout {
250        self.operation_timeout
251    }
252}
253
254/// Result count for a caller-provided scan buffer.
255#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub struct ScanOutcome {
257    /// Entries copied into the caller's buffer.
258    pub count: usize,
259    /// At least one result did not fit in the backend or caller buffer.
260    pub truncated: bool,
261}
262
263/// Validated station connection request.
264#[derive(Debug, Eq, PartialEq)]
265pub struct StationConfig {
266    /// SSID selected by the application.
267    pub ssid: Ssid,
268    /// BSSID selected by the immediately preceding scan.
269    pub bssid: [u8; 6],
270    /// Primary channel selected by the immediately preceding scan.
271    pub channel: u8,
272    /// WPA2/WPA3-Personal passphrase.
273    pub passphrase: Passphrase,
274    security: PersonalSecurity,
275    operation_timeout: OperationTimeout,
276}
277
278impl StationConfig {
279    /// Select a WPA2-Personal scan result and take ownership of its passphrase.
280    pub fn wpa2_personal(
281        result: &ScanResult,
282        passphrase: Passphrase,
283        operation_timeout: OperationTimeout,
284    ) -> Option<Self> {
285        if !matches!(
286            result.security,
287            Security::Wpa2Personal | Security::Wpa2Wpa3PersonalTransition
288        ) {
289            return None;
290        }
291        Some(Self {
292            ssid: result.ssid,
293            bssid: result.bssid,
294            channel: result.channel,
295            passphrase,
296            security: PersonalSecurity::Wpa2,
297            operation_timeout,
298        })
299    }
300
301    /// Select a WPA3-Personal scan result and take ownership of its passphrase.
302    ///
303    /// PMF is mandatory by construction; callers only choose the SAE
304    /// password-element policy supported by their controlled deployment.
305    pub fn wpa3_personal(
306        result: &ScanResult,
307        passphrase: Passphrase,
308        sae_pwe: SaePwe,
309        operation_timeout: OperationTimeout,
310    ) -> Option<Self> {
311        if !matches!(
312            result.security,
313            Security::Wpa3Personal | Security::Wpa2Wpa3PersonalTransition
314        ) {
315            return None;
316        }
317        Some(Self {
318            ssid: result.ssid,
319            bssid: result.bssid,
320            channel: result.channel,
321            passphrase,
322            security: PersonalSecurity::Wpa3 { sae_pwe },
323            operation_timeout,
324        })
325    }
326
327    /// Typed Personal-mode security consumed by the chip backend.
328    pub const fn security(&self) -> PersonalSecurity {
329        self.security
330    }
331
332    /// End-to-end association and authorization timeout.
333    pub const fn operation_timeout(&self) -> OperationTimeout {
334        self.operation_timeout
335    }
336}
337
338/// Successful station association.
339#[derive(Clone, Copy, Debug, Eq, PartialEq)]
340pub struct ConnectionInfo {
341    /// Associated BSSID.
342    pub bssid: [u8; 6],
343    /// Associated center frequency in MHz.
344    pub frequency_mhz: u16,
345}
346
347/// Stable class for backend-specific failures.
348#[derive(Clone, Copy, Debug, Eq, PartialEq)]
349pub enum BackendErrorClass {
350    /// Radio initialization failed.
351    Initialize,
352    /// The requested operation is already active.
353    Busy,
354    /// The end-to-end protocol operation timeout elapsed.
355    OperationTimeout,
356    /// A bounded backend or vendor lifecycle call timed out.
357    BackendTimeout,
358    /// The operation was explicitly cancelled before completion.
359    Cancelled,
360    /// The selected profile could not acquire a required bounded resource.
361    ResourceUnavailable,
362    /// The requested security mode is unsupported.
363    UnsupportedSecurity,
364    /// Association or authorization failed.
365    Connect,
366    /// A chip-specific failure outside the stable classes.
367    Other,
368}
369
370/// Backend error with a stable class and lossless chip-specific context.
371///
372/// Chip backends construct this value explicitly instead of exposing their
373/// private error enums through the portable API. The fixed-size diagnostic
374/// context remains allocation-free and contains no arbitrary backend text.
375#[derive(Clone, Copy, Debug, Eq, PartialEq)]
376pub struct BackendError {
377    /// Stable failure class.
378    class: BackendErrorClass,
379    /// Chip/backend-specific diagnostic code.
380    code: u32,
381    stage: DiagnosticStage,
382    profile_revision: Option<&'static str>,
383    trace: DiagnosticTrace,
384}
385
386impl BackendError {
387    /// Create an error with a stable class and lossless backend code.
388    pub const fn new(class: BackendErrorClass, code: u32) -> Self {
389        let stage = match class {
390            BackendErrorClass::Initialize => DiagnosticStage::Initialize,
391            BackendErrorClass::UnsupportedSecurity | BackendErrorClass::Connect => {
392                DiagnosticStage::Connect
393            }
394            BackendErrorClass::Busy
395            | BackendErrorClass::OperationTimeout
396            | BackendErrorClass::Cancelled => DiagnosticStage::Operation,
397            BackendErrorClass::BackendTimeout => DiagnosticStage::Backend,
398            BackendErrorClass::ResourceUnavailable => DiagnosticStage::Runtime,
399            BackendErrorClass::Other => DiagnosticStage::Backend,
400        };
401        Self {
402            class,
403            code,
404            stage,
405            profile_revision: None,
406            trace: DiagnosticTrace::new(),
407        }
408    }
409
410    /// Stable backend failure class.
411    pub const fn class(self) -> BackendErrorClass {
412        self.class
413    }
414
415    /// Lossless chip/backend-specific code.
416    pub const fn code(self) -> u32 {
417        self.code
418    }
419
420    /// Attach the protocol stage known by the backend.
421    pub const fn with_stage(mut self, stage: DiagnosticStage) -> Self {
422        self.stage = stage;
423        self
424    }
425
426    /// Attach the immutable backend/profile revision used by this firmware.
427    pub const fn with_profile_revision(mut self, revision: &'static str) -> Self {
428        self.profile_revision = Some(revision);
429        self
430    }
431
432    /// Append one bounded numeric trace entry.
433    pub fn with_trace(mut self, kind: DiagnosticTraceKind, value: u32) -> Self {
434        self.trace.push(kind, value);
435        self
436    }
437
438    pub(crate) const fn stage(self) -> DiagnosticStage {
439        self.stage
440    }
441
442    pub(crate) const fn profile_revision(self) -> Option<&'static str> {
443        self.profile_revision
444    }
445
446    pub(crate) const fn trace(self) -> DiagnosticTrace {
447        self.trace
448    }
449}
450
451/// Successful and failed state transitions emitted by the runner.
452#[derive(Clone, Copy, Debug, Eq, PartialEq)]
453pub enum WifiEvent {
454    /// Backend initialization completed.
455    Initialized,
456    /// Scan completed and retained this many results.
457    ScanCompleted { count: usize, truncated: bool },
458    /// Station association and authorization completed.
459    Connected(ConnectionInfo),
460    /// The station disconnected.
461    Disconnected { reason: u16 },
462    /// An operation failed in the backend.
463    Failed(BackendError),
464}
465
466/// Event queue overflow diagnostics.
467#[derive(Clone, Copy, Debug, Eq, PartialEq)]
468pub struct EventDiagnostics {
469    /// Compile-time queue depth.
470    pub capacity: usize,
471    /// Events accepted into the bounded queue.
472    pub accepted: u32,
473    /// Events consumed by the controller.
474    pub consumed: u32,
475    /// Events currently waiting for the controller.
476    pub pending: usize,
477    /// Largest observed queue occupancy since initialization.
478    pub high_water: usize,
479    /// Oldest events discarded because the queue was full.
480    pub dropped: u32,
481}
482
483/// Observational counters for the blocking [`RadioRunner`] path.
484///
485/// Counters saturate at `u32::MAX`. They describe migration workload and must
486/// not be used as synchronization or correctness state.
487#[derive(Clone, Copy, Debug, Eq, PartialEq)]
488pub struct BlockingRunnerDiagnostics {
489    /// Commands currently waiting in the fixed-capacity control channel.
490    pub command_queue_pending: usize,
491    /// Largest observed control-channel occupancy since initialization.
492    pub command_queue_high_water: usize,
493    /// Calls to [`RadioRunner::run_once`].
494    pub run_once_calls: u32,
495    /// Commands processed by either runner entry point.
496    pub commands_processed: u32,
497    /// Calls to [`WifiBackend::poll`].
498    pub backend_poll_calls: u32,
499    /// Poll calls that reported useful background work.
500    pub backend_poll_work_batches: u32,
501    /// Poll calls that returned an error, including repeated errors.
502    pub backend_poll_errors: u32,
503    /// `run_once` calls that asked the platform to schedule another batch.
504    pub immediate_repoll_hints: u32,
505}
506
507/// Chip backend driven exclusively by [`RadioRunner`].
508pub trait WifiBackend {
509    /// Initialize the vendor/ROM radio runtime.
510    fn initialize(&mut self, config: &WifiConfig) -> Result<(), BackendError>;
511
512    /// Scan into the fixed runner-owned buffer.
513    fn scan(
514        &mut self,
515        config: ScanConfig,
516        output: &mut [ScanResult],
517    ) -> Result<ScanOutcome, BackendError>;
518
519    /// Associate and authorize one station connection.
520    fn connect(&mut self, config: &StationConfig) -> Result<ConnectionInfo, BackendError>;
521
522    /// Disconnect the station interface.
523    fn disconnect(&mut self, config: &WifiConfig) -> Result<(), BackendError>;
524
525    /// Snapshot immutable L2 identity after successful initialization.
526    ///
527    /// The runner publishes the first returned value into this radio
528    /// instance's state. It never exposes an unowned process-global accessor.
529    fn l2_capabilities(&self) -> Option<WifiL2Capabilities> {
530        None
531    }
532
533    /// Advance bounded background work owned by the radio runner.
534    ///
535    /// Push-only backends may keep the default implementation. Host-side
536    /// protocol engines use this seam for event-loop deadlines and queued RX;
537    /// it must not invoke application callbacks.
538    fn poll(&mut self) -> Result<bool, BackendError> {
539        Ok(false)
540    }
541}
542
543/// Caller-provided chip resources.
544pub struct RadioResources<B, D> {
545    /// Control-plane backend moved into the runner.
546    pub backend: B,
547    /// L2 device moved into the Wi-Fi data plane.
548    pub device: D,
549}
550
551/// Static storage for one radio controller and its bounded event queue.
552pub struct RadioState<const EVENTS: usize> {
553    pub(crate) shared: SharedState<EVENTS>,
554}
555
556impl<const EVENTS: usize> RadioState<EVENTS> {
557    /// Construct unclaimed radio state suitable for static allocation.
558    pub const fn new() -> Self {
559        Self {
560            shared: SharedState::new(),
561        }
562    }
563}
564
565impl<const EVENTS: usize> Default for RadioState<EVENTS> {
566    fn default() -> Self {
567        Self::new()
568    }
569}
570
571/// Exclusive unsplit radio ownership.
572pub struct RadioController<B, D, const EVENTS: usize> {
573    config: RadioConfig,
574    resources: RadioResources<B, D>,
575    state: &'static RadioState<EVENTS>,
576}
577
578/// Claim one radio instance without invoking its backend.
579pub fn init<B, D, const EVENTS: usize>(
580    config: RadioConfig,
581    resources: RadioResources<B, D>,
582    state: &'static RadioState<EVENTS>,
583) -> Result<RadioController<B, D, EVENTS>, Error> {
584    if !state.shared.claim() {
585        return Err(Error::AlreadyInitialized);
586    }
587    Ok(RadioController {
588        config,
589        resources,
590        state,
591    })
592}
593
594impl<B, D, const EVENTS: usize> RadioController<B, D, EVENTS> {
595    /// Split exclusive ownership into Wi-Fi control/data planes and the runner.
596    pub fn split(self) -> RadioParts<B, D, EVENTS> {
597        let (wifi, backend, config, state) = self.split_components();
598        RadioParts {
599            wifi,
600            runner: RadioRunner {
601                backend,
602                config,
603                state,
604                last_poll_error: None,
605            },
606        }
607    }
608
609    pub(crate) fn split_components(
610        self,
611    ) -> (
612        WifiParts<D, EVENTS>,
613        B,
614        WifiConfig,
615        &'static RadioState<EVENTS>,
616    ) {
617        (
618            WifiParts {
619                controller: WifiController {
620                    state: self.state,
621                    next_sequence: 0,
622                },
623                device: WifiDevice {
624                    inner: self.resources.device,
625                    l2_capabilities: &self.state.shared.l2_capabilities,
626                },
627            },
628            self.resources.backend,
629            self.config.wifi,
630            self.state,
631        )
632    }
633}
634
635/// Enabled protocol handles plus the mandatory runner.
636pub struct RadioParts<B, D, const EVENTS: usize> {
637    /// Wi-Fi control and L2 data planes.
638    pub wifi: WifiParts<D, EVENTS>,
639    /// Long-lived backend runner.
640    pub runner: RadioRunner<B, EVENTS>,
641}
642
643/// Separate Wi-Fi control and L2 data-plane ownership.
644pub struct WifiParts<D, const EVENTS: usize> {
645    /// Async control plane.
646    pub controller: WifiController<EVENTS>,
647    /// L2 data plane.
648    pub device: WifiDevice<D>,
649}
650
651/// Async Wi-Fi control plane. This handle is deliberately not cloneable.
652pub struct WifiController<const EVENTS: usize> {
653    state: &'static RadioState<EVENTS>,
654    next_sequence: u32,
655}
656
657struct OperationCancellation<const EVENTS: usize> {
658    state: &'static RadioState<EVENTS>,
659    sequence: u32,
660    armed: bool,
661}
662
663impl<const EVENTS: usize> OperationCancellation<EVENTS> {
664    const fn new(state: &'static RadioState<EVENTS>, sequence: u32) -> Self {
665        Self {
666            state,
667            sequence,
668            armed: true,
669        }
670    }
671
672    fn complete(&mut self) {
673        self.armed = false;
674    }
675}
676
677impl<const EVENTS: usize> Drop for OperationCancellation<EVENTS> {
678    fn drop(&mut self) {
679        if self.armed {
680            // The unique controller can have at most one command in the facade
681            // channel, one pending in the incremental driver, and one active
682            // operation. The three-entry cancellation channel therefore
683            // covers every accepted-but-unobserved control future.
684            let _ = self.state.shared.cancellations.try_send(self.sequence);
685        }
686    }
687}
688
689impl<const EVENTS: usize> WifiController<EVENTS> {
690    /// Ask the runner to initialize the backend.
691    ///
692    /// Dropping this future requests cancellation. The unique runner performs
693    /// backend abort and cleanup outside the drop path.
694    pub async fn initialize(&mut self) -> Result<(), Error> {
695        let sequence = self.allocate_sequence();
696        self.send_command(Command {
697            sequence,
698            kind: CommandKind::Initialize,
699        })
700        .await;
701        let mut cancellation = OperationCancellation::new(self.state, sequence);
702        loop {
703            let completion = self.state.shared.completion.wait().await;
704            if completion.sequence != sequence {
705                continue;
706            }
707            let result = match completion.kind {
708                CompletionKind::Initialize(result) => result.map_err(Error::Backend),
709                #[cfg(feature = "incremental-backend-experiment")]
710                CompletionKind::Protocol => Err(Error::Protocol),
711                _ => Err(Error::Protocol),
712            };
713            cancellation.complete();
714            return result;
715        }
716    }
717
718    /// Scan and copy results into a caller-provided fixed buffer.
719    pub async fn scan(
720        &mut self,
721        config: ScanConfig,
722        output: &mut [ScanResult],
723    ) -> Result<ScanOutcome, Error> {
724        let sequence = self.allocate_sequence();
725        self.send_command(Command {
726            sequence,
727            kind: CommandKind::Scan(config),
728        })
729        .await;
730        let mut cancellation = OperationCancellation::new(self.state, sequence);
731        loop {
732            let completion = self.state.shared.completion.wait().await;
733            if completion.sequence != sequence {
734                continue;
735            }
736            let result = match completion.kind {
737                CompletionKind::Scan(result) => match result {
738                    Ok(backend) => {
739                        let count = backend.count.min(output.len());
740                        output[..count].copy_from_slice(&self.state.shared.scan_results()[..count]);
741                        Ok(ScanOutcome {
742                            count,
743                            truncated: backend.truncated || backend.count > output.len(),
744                        })
745                    }
746                    Err(error) => Err(Error::Backend(error)),
747                },
748                #[cfg(feature = "incremental-backend-experiment")]
749                CompletionKind::Protocol => Err(Error::Protocol),
750                _ => Err(Error::Protocol),
751            };
752            cancellation.complete();
753            return result;
754        }
755    }
756
757    /// Associate and authorize a station connection.
758    pub async fn connect(&mut self, config: StationConfig) -> Result<ConnectionInfo, Error> {
759        let sequence = self.allocate_sequence();
760        self.send_command(Command {
761            sequence,
762            kind: CommandKind::Connect(config),
763        })
764        .await;
765        let mut cancellation = OperationCancellation::new(self.state, sequence);
766        loop {
767            let completion = self.state.shared.completion.wait().await;
768            if completion.sequence != sequence {
769                continue;
770            }
771            let result = match completion.kind {
772                CompletionKind::Connect(result) => result.map_err(Error::Backend),
773                #[cfg(feature = "incremental-backend-experiment")]
774                CompletionKind::Protocol => Err(Error::Protocol),
775                _ => Err(Error::Protocol),
776            };
777            cancellation.complete();
778            return result;
779        }
780    }
781
782    /// Disconnect the current station link.
783    pub async fn disconnect(&mut self) -> Result<(), Error> {
784        let sequence = self.allocate_sequence();
785        self.send_command(Command {
786            sequence,
787            kind: CommandKind::Disconnect,
788        })
789        .await;
790        let mut cancellation = OperationCancellation::new(self.state, sequence);
791        loop {
792            let completion = self.state.shared.completion.wait().await;
793            if completion.sequence != sequence {
794                continue;
795            }
796            let result = match completion.kind {
797                CompletionKind::Disconnect(result) => result.map_err(Error::Backend),
798                #[cfg(feature = "incremental-backend-experiment")]
799                CompletionKind::Protocol => Err(Error::Protocol),
800                _ => Err(Error::Protocol),
801            };
802            cancellation.complete();
803            return result;
804        }
805    }
806
807    /// Wait for the next bounded event produced by the runner.
808    pub async fn next_event(&mut self) -> WifiEvent {
809        let event = self.state.shared.events.receive().await;
810        saturating_increment(&self.state.shared.consumed_events);
811        event
812    }
813
814    /// Snapshot queue occupancy and overflow.
815    pub fn event_diagnostics(&self) -> EventDiagnostics {
816        EventDiagnostics {
817            capacity: EVENTS,
818            accepted: self.state.shared.accepted_events.load(Ordering::Relaxed),
819            consumed: self.state.shared.consumed_events.load(Ordering::Relaxed),
820            pending: self.state.shared.events.len(),
821            high_water: usize::try_from(self.state.shared.event_high_water.load(Ordering::Relaxed))
822                .unwrap_or(usize::MAX),
823            dropped: self.state.shared.dropped_events.load(Ordering::Relaxed),
824        }
825    }
826
827    /// Snapshot blocking-runner migration counters.
828    pub fn blocking_runner_diagnostics(&self) -> BlockingRunnerDiagnostics {
829        BlockingRunnerDiagnostics {
830            command_queue_pending: self.state.shared.commands.len(),
831            command_queue_high_water: usize::try_from(
832                self.state.shared.command_high_water.load(Ordering::Relaxed),
833            )
834            .unwrap_or(usize::MAX),
835            run_once_calls: self.state.shared.run_once_calls.load(Ordering::Relaxed),
836            commands_processed: self.state.shared.commands_processed.load(Ordering::Relaxed),
837            backend_poll_calls: self.state.shared.backend_poll_calls.load(Ordering::Relaxed),
838            backend_poll_work_batches: self
839                .state
840                .shared
841                .backend_poll_work_batches
842                .load(Ordering::Relaxed),
843            backend_poll_errors: self
844                .state
845                .shared
846                .backend_poll_errors
847                .load(Ordering::Relaxed),
848            immediate_repoll_hints: self
849                .state
850                .shared
851                .immediate_repoll_hints
852                .load(Ordering::Relaxed),
853        }
854    }
855
856    /// Snapshot the opt-in incremental runner counters for this radio instance.
857    #[cfg(feature = "incremental-backend-experiment")]
858    pub fn incremental_runner_diagnostics(&self) -> crate::IncrementalRunnerDiagnostics {
859        self.state.shared.incremental_diagnostics.snapshot()
860    }
861
862    fn allocate_sequence(&mut self) -> u32 {
863        self.next_sequence = self.next_sequence.wrapping_add(1);
864        if self.next_sequence == 0 {
865            self.next_sequence = 1;
866        }
867        self.next_sequence
868    }
869
870    async fn send_command(&self, command: Command) {
871        self.state.shared.commands.send(command).await;
872        self.state.shared.record_command_accepted();
873    }
874}
875
876/// Long-lived owner of a chip backend.
877pub struct RadioRunner<B, const EVENTS: usize> {
878    backend: B,
879    config: WifiConfig,
880    state: &'static RadioState<EVENTS>,
881    last_poll_error: Option<BackendError>,
882}
883
884impl<B: WifiBackend, const EVENTS: usize> RadioRunner<B, EVENTS> {
885    /// Process at most one command and one bounded background-work batch.
886    ///
887    /// A `true` result means another batch may be useful immediately; it does
888    /// not grant the caller permission to monopolize a cooperative executor.
889    /// A thread-based runner must yield or otherwise provide a scheduling point
890    /// between calls.
891    pub fn run_once(&mut self) -> bool {
892        saturating_increment(&self.state.shared.run_once_calls);
893        let mut did_work = false;
894        if let Ok(command) = self.state.shared.commands.try_receive() {
895            self.process_or_cancel_command(command);
896            did_work = true;
897        }
898        saturating_increment(&self.state.shared.backend_poll_calls);
899        let immediate_repoll = match self.backend.poll() {
900            Ok(background_work) => {
901                self.last_poll_error = None;
902                if background_work {
903                    saturating_increment(&self.state.shared.backend_poll_work_batches);
904                }
905                did_work || background_work
906            }
907            Err(error) => {
908                saturating_increment(&self.state.shared.backend_poll_errors);
909                if self.last_poll_error != Some(error) {
910                    self.state.shared.publish_event(WifiEvent::Failed(error));
911                    self.last_poll_error = Some(error);
912                    true
913                } else {
914                    did_work
915                }
916            }
917        };
918        if immediate_repoll {
919            saturating_increment(&self.state.shared.immediate_repoll_hints);
920        }
921        immediate_repoll
922    }
923
924    /// Run forever for command-driven backends.
925    ///
926    /// Backends with timer- or RX-driven [`WifiBackend::poll`] work must call
927    /// [`Self::run_once`] from their platform runner so its wait primitive can
928    /// cover both command and backend wake sources.
929    pub async fn run(mut self) -> ! {
930        loop {
931            let command = self.state.shared.commands.receive().await;
932            self.process_or_cancel_command(command);
933        }
934    }
935
936    fn process_or_cancel_command(&mut self, command: Command) {
937        while let Ok(sequence) = self.state.shared.cancellations.try_receive() {
938            if sequence == command.sequence {
939                let error = BackendError::new(BackendErrorClass::Cancelled, 0);
940                self.state.shared.publish_event(WifiEvent::Failed(error));
941                self.state.shared.completion.signal(Completion {
942                    sequence,
943                    kind: match command.kind {
944                        CommandKind::Initialize => CompletionKind::Initialize(Err(error)),
945                        CommandKind::Scan(_) => CompletionKind::Scan(Err(error)),
946                        CommandKind::Connect(_) => CompletionKind::Connect(Err(error)),
947                        CommandKind::Disconnect => CompletionKind::Disconnect(Err(error)),
948                    },
949                });
950                return;
951            }
952        }
953        self.process_command(command);
954    }
955
956    fn process_command(&mut self, command: Command) {
957        saturating_increment(&self.state.shared.commands_processed);
958        let sequence = command.sequence;
959        let completion = match command.kind {
960            CommandKind::Initialize => {
961                let result = self.backend.initialize(&self.config);
962                if result.is_ok()
963                    && let Some(capabilities) = self.backend.l2_capabilities()
964                {
965                    self.state.shared.l2_capabilities.publish_once(capabilities);
966                }
967                self.publish_result(result, WifiEvent::Initialized);
968                CompletionKind::Initialize(result)
969            }
970            CommandKind::Scan(config) => {
971                // SAFETY: RadioRunner is unique and processes one command at a
972                // time. Completion is signalled only after this borrow ends.
973                let output = unsafe { &mut *self.state.shared.scan_results_ptr() };
974                let result = self.backend.scan(config, output);
975                match result {
976                    Ok(outcome) => self.state.shared.publish_event(WifiEvent::ScanCompleted {
977                        count: outcome.count,
978                        truncated: outcome.truncated,
979                    }),
980                    Err(error) => self.state.shared.publish_event(WifiEvent::Failed(error)),
981                }
982                CompletionKind::Scan(result)
983            }
984            CommandKind::Connect(config) => {
985                let result = self.backend.connect(&config);
986                match result {
987                    Ok(info) => self.state.shared.publish_event(WifiEvent::Connected(info)),
988                    Err(error) => self.state.shared.publish_event(WifiEvent::Failed(error)),
989                }
990                CompletionKind::Connect(result)
991            }
992            CommandKind::Disconnect => {
993                let result = self.backend.disconnect(&self.config);
994                self.publish_result(result, WifiEvent::Disconnected { reason: 0 });
995                CompletionKind::Disconnect(result)
996            }
997        };
998        self.state.shared.completion.signal(Completion {
999            sequence,
1000            kind: completion,
1001        });
1002    }
1003
1004    fn publish_result(&self, result: Result<(), BackendError>, success: WifiEvent) {
1005        self.state.shared.publish_event(match result {
1006            Ok(()) => success,
1007            Err(error) => WifiEvent::Failed(error),
1008        });
1009    }
1010}
1011
1012/// L2 data-plane ownership independent of the control backend.
1013pub struct WifiDevice<D> {
1014    inner: D,
1015    l2_capabilities: &'static crate::state::L2CapabilityState,
1016}
1017
1018impl<D> WifiDevice<D> {
1019    /// Snapshot immutable link-layer identity for this radio instance.
1020    ///
1021    /// Returns `None` until the instance's backend initialization succeeds.
1022    pub fn l2_capabilities(&self) -> Option<WifiL2Capabilities> {
1023        self.l2_capabilities.snapshot()
1024    }
1025
1026    /// Return this radio instance's station MAC address after initialization.
1027    pub fn station_mac_address(&self) -> Option<[u8; 6]> {
1028        self.l2_capabilities()
1029            .map(WifiL2Capabilities::station_mac_address)
1030    }
1031
1032    /// Borrow the chip L2 device.
1033    pub fn inner(&self) -> &D {
1034        &self.inner
1035    }
1036
1037    /// Mutably borrow the chip L2 device.
1038    pub fn inner_mut(&mut self) -> &mut D {
1039        &mut self.inner
1040    }
1041
1042    /// Recover the chip L2 device.
1043    pub fn into_inner(self) -> D {
1044        self.inner
1045    }
1046}
1047
1048#[cfg(feature = "smoltcp")]
1049impl<D: smoltcp::phy::Device> smoltcp::phy::Device for WifiDevice<D> {
1050    type RxToken<'a>
1051        = D::RxToken<'a>
1052    where
1053        Self: 'a;
1054    type TxToken<'a>
1055        = D::TxToken<'a>
1056    where
1057        Self: 'a;
1058
1059    fn receive(
1060        &mut self,
1061        timestamp: smoltcp::time::Instant,
1062    ) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
1063        self.inner.receive(timestamp)
1064    }
1065
1066    fn transmit(&mut self, timestamp: smoltcp::time::Instant) -> Option<Self::TxToken<'_>> {
1067        self.inner.transmit(timestamp)
1068    }
1069
1070    fn capabilities(&self) -> smoltcp::phy::DeviceCapabilities {
1071        self.inner.capabilities()
1072    }
1073}
1074
1075pub(crate) struct Command {
1076    pub(crate) sequence: u32,
1077    pub(crate) kind: CommandKind,
1078}
1079
1080pub(crate) enum CommandKind {
1081    Initialize,
1082    Scan(ScanConfig),
1083    Connect(StationConfig),
1084    Disconnect,
1085}
1086
1087#[derive(Clone, Copy)]
1088pub(crate) struct Completion {
1089    pub(crate) sequence: u32,
1090    pub(crate) kind: CompletionKind,
1091}
1092
1093#[derive(Clone, Copy)]
1094pub(crate) enum CompletionKind {
1095    Initialize(Result<(), BackendError>),
1096    Scan(Result<ScanOutcome, BackendError>),
1097    Connect(Result<ConnectionInfo, BackendError>),
1098    Disconnect(Result<(), BackendError>),
1099    #[cfg(feature = "incremental-backend-experiment")]
1100    Protocol,
1101}
1102
1103#[cfg(test)]
1104mod tests {
1105    extern crate std;
1106
1107    use core::future::Future;
1108    use core::task::{Context, Poll, Waker};
1109    use std::boxed::Box;
1110
1111    use super::*;
1112
1113    struct MockBackend {
1114        calls: u8,
1115        poll_work: bool,
1116        poll_error: Option<BackendError>,
1117        initialize_error: Option<BackendError>,
1118        station_mac_address: [u8; 6],
1119    }
1120
1121    impl Default for MockBackend {
1122        fn default() -> Self {
1123            Self {
1124                calls: 0,
1125                poll_work: false,
1126                poll_error: None,
1127                initialize_error: None,
1128                station_mac_address: [0x02, 1, 2, 3, 4, 5],
1129            }
1130        }
1131    }
1132
1133    impl WifiBackend for MockBackend {
1134        fn initialize(&mut self, _: &WifiConfig) -> Result<(), BackendError> {
1135            self.calls += 1;
1136            self.initialize_error.map_or(Ok(()), Err)
1137        }
1138
1139        fn scan(
1140            &mut self,
1141            _: ScanConfig,
1142            output: &mut [ScanResult],
1143        ) -> Result<ScanOutcome, BackendError> {
1144            self.calls += 1;
1145            output[0] = ScanResult {
1146                ssid: Ssid::try_from_bytes(b"test-ap").unwrap(),
1147                bssid: [1, 2, 3, 4, 5, 6],
1148                frequency_mhz: 2437,
1149                rssi_dbm: -42,
1150                security: Security::Wpa2Personal,
1151                channel: 6,
1152            };
1153            Ok(ScanOutcome {
1154                count: 1,
1155                truncated: false,
1156            })
1157        }
1158
1159        fn connect(&mut self, config: &StationConfig) -> Result<ConnectionInfo, BackendError> {
1160            self.calls += 1;
1161            Ok(ConnectionInfo {
1162                bssid: config.bssid,
1163                frequency_mhz: 2437,
1164            })
1165        }
1166
1167        fn disconnect(&mut self, _: &WifiConfig) -> Result<(), BackendError> {
1168            self.calls += 1;
1169            Ok(())
1170        }
1171
1172        fn l2_capabilities(&self) -> Option<WifiL2Capabilities> {
1173            WifiL2Capabilities::try_new(self.station_mac_address)
1174        }
1175
1176        fn poll(&mut self) -> Result<bool, BackendError> {
1177            if let Some(error) = self.poll_error {
1178                Err(error)
1179            } else {
1180                Ok(core::mem::take(&mut self.poll_work))
1181            }
1182        }
1183    }
1184
1185    fn poll<F: Future>(future: core::pin::Pin<&mut F>) -> Poll<F::Output> {
1186        let waker = Waker::noop();
1187        future.poll(&mut Context::from_waker(waker))
1188    }
1189
1190    #[test]
1191    fn runner_is_the_only_backend_execution_path() {
1192        let state = Box::leak(Box::new(RadioState::<4>::new()));
1193        let radio = init(
1194            RadioConfig::default(),
1195            RadioResources {
1196                backend: MockBackend::default(),
1197                device: (),
1198            },
1199            state,
1200        )
1201        .unwrap();
1202        let RadioParts {
1203            mut wifi,
1204            mut runner,
1205        } = radio.split();
1206        assert_eq!(wifi.device.l2_capabilities(), None);
1207
1208        {
1209            let mut initialize = core::pin::pin!(wifi.controller.initialize());
1210            assert!(poll(initialize.as_mut()).is_pending());
1211            assert_eq!(state.shared.commands.len(), 1);
1212            assert_eq!(state.shared.command_high_water.load(Ordering::Relaxed), 1);
1213            assert!(runner.run_once());
1214            assert_eq!(poll(initialize.as_mut()), Poll::Ready(Ok(())));
1215        }
1216        assert_eq!(
1217            wifi.device.l2_capabilities(),
1218            WifiL2Capabilities::try_new([0x02, 1, 2, 3, 4, 5])
1219        );
1220        assert_eq!(
1221            wifi.device.station_mac_address(),
1222            Some([0x02, 1, 2, 3, 4, 5])
1223        );
1224
1225        let mut results = [ScanResult::EMPTY; 1];
1226        {
1227            let mut scan = core::pin::pin!(wifi.controller.scan(
1228                ScanConfig::new(OperationTimeout::try_from_millis(1_000).unwrap()),
1229                &mut results,
1230            ));
1231            assert!(poll(scan.as_mut()).is_pending());
1232            assert!(runner.run_once());
1233            assert_eq!(
1234                poll(scan.as_mut()),
1235                Poll::Ready(Ok(ScanOutcome {
1236                    count: 1,
1237                    truncated: false,
1238                }))
1239            );
1240        }
1241        assert_eq!(results[0].ssid.as_bytes(), b"test-ap");
1242    }
1243
1244    #[test]
1245    fn failed_initialization_does_not_publish_l2_capabilities() {
1246        let state = Box::leak(Box::new(RadioState::<2>::new()));
1247        let error = BackendError::new(BackendErrorClass::Initialize, 7);
1248        let radio = init(
1249            RadioConfig::default(),
1250            RadioResources {
1251                backend: MockBackend {
1252                    initialize_error: Some(error),
1253                    ..MockBackend::default()
1254                },
1255                device: (),
1256            },
1257            state,
1258        )
1259        .unwrap();
1260        let RadioParts {
1261            mut wifi,
1262            mut runner,
1263        } = radio.split();
1264
1265        let mut initialize = core::pin::pin!(wifi.controller.initialize());
1266        assert!(poll(initialize.as_mut()).is_pending());
1267        assert!(runner.run_once());
1268        assert_eq!(
1269            poll(initialize.as_mut()),
1270            Poll::Ready(Err(Error::Backend(error)))
1271        );
1272        assert_eq!(wifi.device.l2_capabilities(), None);
1273        assert_eq!(wifi.device.station_mac_address(), None);
1274    }
1275
1276    #[test]
1277    fn l2_capabilities_are_owned_by_each_radio_instance() {
1278        let state_a = Box::leak(Box::new(RadioState::<2>::new()));
1279        let state_b = Box::leak(Box::new(RadioState::<2>::new()));
1280        let mac_a = [0x02, 1, 1, 1, 1, 1];
1281        let mac_b = [0x02, 2, 2, 2, 2, 2];
1282        let radio_a = init(
1283            RadioConfig::default(),
1284            RadioResources {
1285                backend: MockBackend {
1286                    station_mac_address: mac_a,
1287                    ..MockBackend::default()
1288                },
1289                device: (),
1290            },
1291            state_a,
1292        )
1293        .unwrap();
1294        let radio_b = init(
1295            RadioConfig::default(),
1296            RadioResources {
1297                backend: MockBackend {
1298                    station_mac_address: mac_b,
1299                    ..MockBackend::default()
1300                },
1301                device: (),
1302            },
1303            state_b,
1304        )
1305        .unwrap();
1306        let RadioParts {
1307            wifi: mut wifi_a,
1308            runner: mut runner_a,
1309        } = radio_a.split();
1310        let RadioParts {
1311            wifi: mut wifi_b,
1312            runner: mut runner_b,
1313        } = radio_b.split();
1314
1315        let mut initialize_a = core::pin::pin!(wifi_a.controller.initialize());
1316        let mut initialize_b = core::pin::pin!(wifi_b.controller.initialize());
1317        assert!(poll(initialize_a.as_mut()).is_pending());
1318        assert!(poll(initialize_b.as_mut()).is_pending());
1319        assert!(runner_a.run_once());
1320        assert!(runner_b.run_once());
1321        assert_eq!(poll(initialize_a.as_mut()), Poll::Ready(Ok(())));
1322        assert_eq!(poll(initialize_b.as_mut()), Poll::Ready(Ok(())));
1323        assert_eq!(wifi_a.device.station_mac_address(), Some(mac_a));
1324        assert_eq!(wifi_b.device.station_mac_address(), Some(mac_b));
1325    }
1326
1327    #[test]
1328    fn bounded_events_drop_the_oldest_and_report_overflow() {
1329        let state = Box::leak(Box::new(RadioState::<1>::new()));
1330        let radio = init(
1331            RadioConfig::default(),
1332            RadioResources {
1333                backend: MockBackend::default(),
1334                device: (),
1335            },
1336            state,
1337        )
1338        .unwrap();
1339        let RadioParts {
1340            mut wifi,
1341            mut runner,
1342        } = radio.split();
1343
1344        for _ in 0..2 {
1345            let mut initialize = core::pin::pin!(wifi.controller.initialize());
1346            assert!(poll(initialize.as_mut()).is_pending());
1347            assert!(runner.run_once());
1348            assert_eq!(poll(initialize.as_mut()), Poll::Ready(Ok(())));
1349        }
1350        assert_eq!(
1351            wifi.controller.event_diagnostics(),
1352            EventDiagnostics {
1353                capacity: 1,
1354                accepted: 2,
1355                consumed: 0,
1356                pending: 1,
1357                high_water: 1,
1358                dropped: 1,
1359            }
1360        );
1361    }
1362
1363    #[test]
1364    fn runner_advances_background_work_without_a_command() {
1365        let state = Box::leak(Box::new(RadioState::<2>::new()));
1366        let radio = init(
1367            RadioConfig::default(),
1368            RadioResources {
1369                backend: MockBackend {
1370                    poll_work: true,
1371                    ..MockBackend::default()
1372                },
1373                device: (),
1374            },
1375            state,
1376        )
1377        .unwrap();
1378        let mut runner = radio.split().runner;
1379
1380        assert!(runner.run_once());
1381        assert!(!runner.run_once());
1382    }
1383
1384    #[test]
1385    fn repeated_background_error_publishes_one_event() {
1386        let state = Box::leak(Box::new(RadioState::<2>::new()));
1387        let error = BackendError::new(BackendErrorClass::Other, 0x55);
1388        let radio = init(
1389            RadioConfig::default(),
1390            RadioResources {
1391                backend: MockBackend {
1392                    poll_error: Some(error),
1393                    ..MockBackend::default()
1394                },
1395                device: (),
1396            },
1397            state,
1398        )
1399        .unwrap();
1400        let RadioParts {
1401            mut wifi,
1402            mut runner,
1403        } = radio.split();
1404
1405        assert!(runner.run_once());
1406        assert!(!runner.run_once());
1407        assert_eq!(
1408            wifi.controller.event_diagnostics(),
1409            EventDiagnostics {
1410                capacity: 2,
1411                accepted: 1,
1412                consumed: 0,
1413                pending: 1,
1414                high_water: 1,
1415                dropped: 0,
1416            }
1417        );
1418        {
1419            let mut event = core::pin::pin!(wifi.controller.next_event());
1420            assert_eq!(poll(event.as_mut()), Poll::Ready(WifiEvent::Failed(error)));
1421        }
1422        assert_eq!(
1423            wifi.controller.event_diagnostics(),
1424            EventDiagnostics {
1425                capacity: 2,
1426                accepted: 1,
1427                consumed: 1,
1428                pending: 0,
1429                high_water: 1,
1430                dropped: 0,
1431            }
1432        );
1433    }
1434
1435    #[test]
1436    fn blocking_runner_diagnostics_count_bounded_work() {
1437        let state = Box::leak(Box::new(RadioState::<2>::new()));
1438        let error = BackendError::new(BackendErrorClass::Other, 0x55);
1439        let radio = init(
1440            RadioConfig::default(),
1441            RadioResources {
1442                backend: MockBackend {
1443                    poll_work: true,
1444                    ..MockBackend::default()
1445                },
1446                device: (),
1447            },
1448            state,
1449        )
1450        .unwrap();
1451        let RadioParts {
1452            mut wifi,
1453            mut runner,
1454        } = radio.split();
1455
1456        {
1457            let mut initialize = core::pin::pin!(wifi.controller.initialize());
1458            assert!(poll(initialize.as_mut()).is_pending());
1459            assert!(runner.run_once());
1460            assert_eq!(poll(initialize.as_mut()), Poll::Ready(Ok(())));
1461        }
1462        assert!(!runner.run_once());
1463
1464        runner.backend.poll_error = Some(error);
1465        assert!(runner.run_once());
1466        assert!(!runner.run_once());
1467
1468        assert_eq!(
1469            wifi.controller.blocking_runner_diagnostics(),
1470            BlockingRunnerDiagnostics {
1471                command_queue_pending: 0,
1472                command_queue_high_water: 1,
1473                run_once_calls: 4,
1474                commands_processed: 1,
1475                backend_poll_calls: 4,
1476                backend_poll_work_batches: 1,
1477                backend_poll_errors: 2,
1478                immediate_repoll_hints: 2,
1479            }
1480        );
1481    }
1482
1483    #[test]
1484    fn cancelled_control_future_cannot_poison_the_next_command() {
1485        let state = Box::leak(Box::new(RadioState::<2>::new()));
1486        let radio = init(
1487            RadioConfig::default(),
1488            RadioResources {
1489                backend: MockBackend::default(),
1490                device: (),
1491            },
1492            state,
1493        )
1494        .unwrap();
1495        let RadioParts {
1496            mut wifi,
1497            mut runner,
1498        } = radio.split();
1499
1500        {
1501            let mut cancelled = core::pin::pin!(wifi.controller.initialize());
1502            assert!(poll(cancelled.as_mut()).is_pending());
1503        }
1504        assert!(runner.run_once());
1505        assert_eq!(runner.backend.calls, 0);
1506
1507        let mut next = core::pin::pin!(wifi.controller.initialize());
1508        assert!(poll(next.as_mut()).is_pending());
1509        assert!(runner.run_once());
1510        assert_eq!(runner.backend.calls, 1);
1511        assert_eq!(poll(next.as_mut()), Poll::Ready(Ok(())));
1512    }
1513
1514    #[test]
1515    fn validated_configuration_rejects_invalid_inputs() {
1516        assert!(Ssid::try_from_bytes(b"").is_none());
1517        assert!(Ssid::try_from_bytes(&[b'x'; 33]).is_none());
1518        assert!(Passphrase::try_from_ascii(b"short").is_none());
1519        assert!(Passphrase::try_from_ascii(b"testtest").is_some());
1520        assert!(OperationTimeout::try_from_millis(0).is_none());
1521        assert!(BackendTimeout::try_from_millis(0).is_none());
1522        assert_eq!(OperationTimeout::try_from_millis(1).unwrap().as_millis(), 1);
1523        assert_eq!(BackendTimeout::try_from_millis(1).unwrap().as_millis(), 1);
1524        assert!(WifiL2Capabilities::try_new([0; 6]).is_none());
1525        assert!(WifiL2Capabilities::try_new([1, 2, 3, 4, 5, 6]).is_none());
1526        assert_eq!(
1527            WifiL2Capabilities::try_new([0x02, 1, 2, 3, 4, 5])
1528                .unwrap()
1529                .station_mac_address(),
1530            [0x02, 1, 2, 3, 4, 5]
1531        );
1532    }
1533
1534    #[test]
1535    fn wpa3_config_requires_wpa3_scan_and_implies_required_pmf() {
1536        let result = ScanResult {
1537            ssid: Ssid::try_from_bytes(b"wpa3-ap").unwrap(),
1538            bssid: [1, 2, 3, 4, 5, 6],
1539            frequency_mhz: 5180,
1540            rssi_dbm: -38,
1541            security: Security::Wpa3Personal,
1542            channel: 36,
1543        };
1544        let config = StationConfig::wpa3_personal(
1545            &result,
1546            Passphrase::try_from_ascii(b"testtest").unwrap(),
1547            SaePwe::Both,
1548            OperationTimeout::try_from_millis(10_000).unwrap(),
1549        )
1550        .unwrap();
1551        assert_eq!(
1552            config.security(),
1553            PersonalSecurity::Wpa3 {
1554                sae_pwe: SaePwe::Both
1555            }
1556        );
1557        assert_eq!(
1558            config.security().management_frame_protection(),
1559            ManagementFrameProtection::Required
1560        );
1561    }
1562
1563    #[test]
1564    fn transition_scan_requires_an_explicit_personal_mode_choice() {
1565        let result = ScanResult {
1566            ssid: Ssid::try_from_bytes(b"transition-ap").unwrap(),
1567            bssid: [1, 2, 3, 4, 5, 6],
1568            frequency_mhz: 5180,
1569            rssi_dbm: -38,
1570            security: Security::Wpa2Wpa3PersonalTransition,
1571            channel: 36,
1572        };
1573
1574        let wpa2 = StationConfig::wpa2_personal(
1575            &result,
1576            Passphrase::try_from_ascii(b"testtest").unwrap(),
1577            OperationTimeout::try_from_millis(10_000).unwrap(),
1578        )
1579        .unwrap();
1580        assert_eq!(wpa2.security(), PersonalSecurity::Wpa2);
1581
1582        let wpa3 = StationConfig::wpa3_personal(
1583            &result,
1584            Passphrase::try_from_ascii(b"testtest").unwrap(),
1585            SaePwe::Both,
1586            OperationTimeout::try_from_millis(10_000).unwrap(),
1587        )
1588        .unwrap();
1589        assert_eq!(
1590            wpa3.security(),
1591            PersonalSecurity::Wpa3 {
1592                sae_pwe: SaePwe::Both
1593            }
1594        );
1595    }
1596}