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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15#[non_exhaustive]
16pub struct RadioConfig {
17 pub wifi: WifiConfig,
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub struct WifiConfig {
24 pub initialize_timeout: BackendTimeout,
26 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct OperationTimeout(u32);
42
43impl OperationTimeout {
44 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 pub const fn as_millis(self) -> u32 {
55 self.0
56 }
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub struct BackendTimeout(u32);
62
63impl BackendTimeout {
64 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 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub struct WifiL2Capabilities {
87 station_mac_address: [u8; 6],
88}
89
90impl WifiL2Capabilities {
91 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 pub const fn station_mac_address(self) -> [u8; 6] {
110 self.station_mac_address
111 }
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub struct Ssid {
117 bytes: [u8; SSID_CAPACITY],
118 len: u8,
119}
120
121impl Ssid {
122 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 pub fn as_bytes(&self) -> &[u8] {
137 &self.bytes[..self.len as usize]
138 }
139}
140
141#[derive(Debug, Eq, PartialEq)]
143pub struct Passphrase {
144 bytes: [u8; PASSPHRASE_CAPACITY],
145 len: u8,
146}
147
148impl Passphrase {
149 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 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 unsafe { core::ptr::write_volatile(byte, 0) };
177 }
178 self.len = 0;
179 }
180}
181
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub enum Security {
185 Open,
187 Wpa2Personal,
189 Wpa3Personal,
191 Wpa2Wpa3PersonalTransition,
196 OtherProtected,
198}
199
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub struct ScanResult {
203 pub ssid: Ssid,
205 pub bssid: [u8; 6],
207 pub frequency_mhz: u16,
209 pub rssi_dbm: i16,
211 pub security: Security,
213 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 pub const fn empty() -> Self {
232 Self::EMPTY
233 }
234}
235
236#[derive(Clone, Copy, Debug, Eq, PartialEq)]
238pub struct ScanConfig {
239 operation_timeout: OperationTimeout,
240}
241
242impl ScanConfig {
243 pub const fn new(operation_timeout: OperationTimeout) -> Self {
245 Self { operation_timeout }
246 }
247
248 pub const fn operation_timeout(self) -> OperationTimeout {
250 self.operation_timeout
251 }
252}
253
254#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub struct ScanOutcome {
257 pub count: usize,
259 pub truncated: bool,
261}
262
263#[derive(Debug, Eq, PartialEq)]
265pub struct StationConfig {
266 pub ssid: Ssid,
268 pub bssid: [u8; 6],
270 pub channel: u8,
272 pub passphrase: Passphrase,
274 security: PersonalSecurity,
275 operation_timeout: OperationTimeout,
276}
277
278impl StationConfig {
279 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 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 pub const fn security(&self) -> PersonalSecurity {
329 self.security
330 }
331
332 pub const fn operation_timeout(&self) -> OperationTimeout {
334 self.operation_timeout
335 }
336}
337
338#[derive(Clone, Copy, Debug, Eq, PartialEq)]
340pub struct ConnectionInfo {
341 pub bssid: [u8; 6],
343 pub frequency_mhz: u16,
345}
346
347#[derive(Clone, Copy, Debug, Eq, PartialEq)]
349pub enum BackendErrorClass {
350 Initialize,
352 Busy,
354 OperationTimeout,
356 BackendTimeout,
358 Cancelled,
360 ResourceUnavailable,
362 UnsupportedSecurity,
364 Connect,
366 Other,
368}
369
370#[derive(Clone, Copy, Debug, Eq, PartialEq)]
376pub struct BackendError {
377 class: BackendErrorClass,
379 code: u32,
381 stage: DiagnosticStage,
382 profile_revision: Option<&'static str>,
383 trace: DiagnosticTrace,
384}
385
386impl BackendError {
387 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 pub const fn class(self) -> BackendErrorClass {
412 self.class
413 }
414
415 pub const fn code(self) -> u32 {
417 self.code
418 }
419
420 pub const fn with_stage(mut self, stage: DiagnosticStage) -> Self {
422 self.stage = stage;
423 self
424 }
425
426 pub const fn with_profile_revision(mut self, revision: &'static str) -> Self {
428 self.profile_revision = Some(revision);
429 self
430 }
431
432 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
453pub enum WifiEvent {
454 Initialized,
456 ScanCompleted { count: usize, truncated: bool },
458 Connected(ConnectionInfo),
460 Disconnected { reason: u16 },
462 Failed(BackendError),
464}
465
466#[derive(Clone, Copy, Debug, Eq, PartialEq)]
468pub struct EventDiagnostics {
469 pub capacity: usize,
471 pub accepted: u32,
473 pub consumed: u32,
475 pub pending: usize,
477 pub high_water: usize,
479 pub dropped: u32,
481}
482
483#[derive(Clone, Copy, Debug, Eq, PartialEq)]
488pub struct BlockingRunnerDiagnostics {
489 pub command_queue_pending: usize,
491 pub command_queue_high_water: usize,
493 pub run_once_calls: u32,
495 pub commands_processed: u32,
497 pub backend_poll_calls: u32,
499 pub backend_poll_work_batches: u32,
501 pub backend_poll_errors: u32,
503 pub immediate_repoll_hints: u32,
505}
506
507pub trait WifiBackend {
509 fn initialize(&mut self, config: &WifiConfig) -> Result<(), BackendError>;
511
512 fn scan(
514 &mut self,
515 config: ScanConfig,
516 output: &mut [ScanResult],
517 ) -> Result<ScanOutcome, BackendError>;
518
519 fn connect(&mut self, config: &StationConfig) -> Result<ConnectionInfo, BackendError>;
521
522 fn disconnect(&mut self, config: &WifiConfig) -> Result<(), BackendError>;
524
525 fn l2_capabilities(&self) -> Option<WifiL2Capabilities> {
530 None
531 }
532
533 fn poll(&mut self) -> Result<bool, BackendError> {
539 Ok(false)
540 }
541}
542
543pub struct RadioResources<B, D> {
545 pub backend: B,
547 pub device: D,
549}
550
551pub struct RadioState<const EVENTS: usize> {
553 pub(crate) shared: SharedState<EVENTS>,
554}
555
556impl<const EVENTS: usize> RadioState<EVENTS> {
557 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
571pub struct RadioController<B, D, const EVENTS: usize> {
573 config: RadioConfig,
574 resources: RadioResources<B, D>,
575 state: &'static RadioState<EVENTS>,
576}
577
578pub 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 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
635pub struct RadioParts<B, D, const EVENTS: usize> {
637 pub wifi: WifiParts<D, EVENTS>,
639 pub runner: RadioRunner<B, EVENTS>,
641}
642
643pub struct WifiParts<D, const EVENTS: usize> {
645 pub controller: WifiController<EVENTS>,
647 pub device: WifiDevice<D>,
649}
650
651pub 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 let _ = self.state.shared.cancellations.try_send(self.sequence);
685 }
686 }
687}
688
689impl<const EVENTS: usize> WifiController<EVENTS> {
690 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 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 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 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 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 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 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 #[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
876pub 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 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 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 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
1012pub struct WifiDevice<D> {
1014 inner: D,
1015 l2_capabilities: &'static crate::state::L2CapabilityState,
1016}
1017
1018impl<D> WifiDevice<D> {
1019 pub fn l2_capabilities(&self) -> Option<WifiL2Capabilities> {
1023 self.l2_capabilities.snapshot()
1024 }
1025
1026 pub fn station_mac_address(&self) -> Option<[u8; 6]> {
1028 self.l2_capabilities()
1029 .map(WifiL2Capabilities::station_mac_address)
1030 }
1031
1032 pub fn inner(&self) -> &D {
1034 &self.inner
1035 }
1036
1037 pub fn inner_mut(&mut self) -> &mut D {
1039 &mut self.inner
1040 }
1041
1042 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}