diff --git a/examples/rpi-pico-w/src/main.rs b/examples/rpi-pico-w/src/main.rs
index d075aec2a..944beaac0 100644
--- a/examples/rpi-pico-w/src/main.rs
+++ b/examples/rpi-pico-w/src/main.rs
@@ -143,4 +143,3 @@ async fn main(spawner: Spawner) {
         }
     }
 }
-
diff --git a/src/control.rs b/src/control.rs
index 0c06009b9..934bade23 100644
--- a/src/control.rs
+++ b/src/control.rs
@@ -6,11 +6,11 @@ use embassy_time::{Duration, Timer};
 
 pub use crate::bus::SpiBusCyw43;
 use crate::consts::*;
-use crate::events::{Event, Events};
+use crate::events::{Event, EventSubscriber, Events};
 use crate::fmt::Bytes;
 use crate::ioctl::{IoctlState, IoctlType};
 use crate::structs::*;
-use crate::{countries, PowerManagementMode};
+use crate::{countries, events, PowerManagementMode};
 
 pub struct Control<'a> {
     state_ch: ch::StateRunner<'a>,
@@ -245,9 +245,13 @@ impl<'a> Control<'a> {
     }
 
     async fn set_iovar(&mut self, name: &str, val: &[u8]) {
+        self.set_iovar_v::<64>(name, val).await
+    }
+
+    async fn set_iovar_v<const BUFSIZE: usize>(&mut self, name: &str, val: &[u8]) {
         info!("set {} = {:02x}", name, Bytes(val));
 
-        let mut buf = [0; 64];
+        let mut buf = [0; BUFSIZE];
         buf[..name.len()].copy_from_slice(name.as_bytes());
         buf[name.len()] = 0;
         buf[name.len() + 1..][..val.len()].copy_from_slice(val);
@@ -304,4 +308,69 @@ impl<'a> Control<'a> {
 
         resp_len
     }
+
+    /// Start a wifi scan
+    ///
+    /// Returns a `Stream` of networks found by the device
+    ///
+    /// # Note
+    /// Device events are currently implemented using a bounded queue.
+    /// To not miss any events, you should make sure to always await the stream.
+    pub async fn scan(&mut self) -> Scanner<'_> {
+        const SCANTYPE_PASSIVE: u8 = 1;
+
+        let scan_params = ScanParams {
+            version: 1,
+            action: 1,
+            sync_id: 1,
+            ssid_len: 0,
+            ssid: [0; 32],
+            bssid: [0xff; 6],
+            bss_type: 2,
+            scan_type: SCANTYPE_PASSIVE,
+            nprobes: !0,
+            active_time: !0,
+            passive_time: !0,
+            home_time: !0,
+            channel_num: 0,
+            channel_list: [0; 1],
+        };
+
+        self.events.mask.enable(&[Event::ESCAN_RESULT]);
+        let subscriber = self.events.queue.subscriber().unwrap();
+        self.set_iovar_v::<256>("escan", &scan_params.to_bytes()).await;
+
+        Scanner {
+            subscriber,
+            events: &self.events,
+        }
+    }
+}
+
+pub struct Scanner<'a> {
+    subscriber: EventSubscriber<'a>,
+    events: &'a Events,
+}
+
+impl Scanner<'_> {
+    /// wait for the next found network
+    pub async fn next(&mut self) -> Option<BssInfo> {
+        let event = self.subscriber.next_message_pure().await;
+        if event.header.status != EStatus::PARTIAL {
+            self.events.mask.disable_all();
+            return None;
+        }
+
+        if let events::Payload::BssInfo(bss) = event.payload {
+            Some(bss)
+        } else {
+            None
+        }
+    }
+}
+
+impl Drop for Scanner<'_> {
+    fn drop(&mut self) {
+        self.events.mask.disable_all();
+    }
 }
diff --git a/src/events.rs b/src/events.rs
index d6f114ed9..a94c49a0c 100644
--- a/src/events.rs
+++ b/src/events.rs
@@ -1,10 +1,12 @@
-#![allow(unused)]
+#![allow(dead_code)]
 #![allow(non_camel_case_types)]
 
 use core::cell::RefCell;
 
 use embassy_sync::blocking_mutex::raw::NoopRawMutex;
-use embassy_sync::pubsub::{PubSubChannel, Publisher, Subscriber};
+use embassy_sync::pubsub::{PubSubChannel, Subscriber};
+
+use crate::structs::BssInfo;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive)]
 #[cfg_attr(feature = "defmt", derive(defmt::Format))]
@@ -286,7 +288,6 @@ pub enum Event {
 
 // TODO this PubSub can probably be replaced with shared memory to make it a bit more efficient.
 pub type EventQueue = PubSubChannel<NoopRawMutex, Message, 2, 1, 1>;
-pub type EventPublisher<'a> = Publisher<'a, NoopRawMutex, Message, 2, 1, 1>;
 pub type EventSubscriber<'a> = Subscriber<'a, NoopRawMutex, Message, 2, 1, 1>;
 
 pub struct Events {
@@ -313,6 +314,7 @@ pub struct Status {
 #[derive(Clone, Copy)]
 pub enum Payload {
     None,
+    BssInfo(BssInfo),
 }
 
 #[derive(Clone, Copy)]
@@ -344,7 +346,7 @@ impl EventMask {
         let word = n / u32::BITS;
         let bit = n % u32::BITS;
 
-        self.mask[word as usize] |= (1 << bit);
+        self.mask[word as usize] |= 1 << bit;
     }
 
     fn disable(&mut self, event: Event) {
@@ -378,6 +380,7 @@ impl SharedEventMask {
         }
     }
 
+    #[allow(dead_code)]
     pub fn disable(&self, events: &[Event]) {
         let mut mask = self.mask.borrow_mut();
         for event in events {
diff --git a/src/lib.rs b/src/lib.rs
index f9244bddb..d437a882e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -29,6 +29,7 @@ use crate::bus::Bus;
 pub use crate::bus::SpiBusCyw43;
 pub use crate::control::Control;
 pub use crate::runner::Runner;
+pub use crate::structs::BssInfo;
 
 const MTU: usize = 1514;
 
diff --git a/src/runner.rs b/src/runner.rs
index 806ddfc49..9b99e174f 100644
--- a/src/runner.rs
+++ b/src/runner.rs
@@ -7,7 +7,7 @@ use embedded_hal_1::digital::OutputPin;
 use crate::bus::Bus;
 pub use crate::bus::SpiBusCyw43;
 use crate::consts::*;
-use crate::events::{Events, Status};
+use crate::events::{Event, Events, Status};
 use crate::fmt::Bytes;
 use crate::ioctl::{IoctlState, IoctlType, PendingIoctl};
 use crate::nvram::NVRAM;
@@ -351,6 +351,8 @@ where
                         panic!("IOCTL error {}", cdc_header.status as i32);
                     }
 
+                    info!("IOCTL Response: {:02x}", Bytes(response));
+
                     self.ioctl_state.ioctl_done(response);
                 }
             }
@@ -404,7 +406,15 @@ where
 
                 if self.events.mask.is_enabled(evt_type) {
                     let status = event_packet.msg.status;
-                    let event_payload = events::Payload::None;
+                    let event_payload = match evt_type {
+                        Event::ESCAN_RESULT if status == EStatus::PARTIAL => {
+                            let Some((_, bss_info)) = ScanResults::parse(evt_data) else { return };
+                            let Some(bss_info) = BssInfo::parse(bss_info) else { return };
+                            events::Payload::BssInfo(*bss_info)
+                        }
+                        Event::ESCAN_RESULT => events::Payload::None,
+                        _ => events::Payload::None,
+                    };
 
                     // this intentionally uses the non-blocking publish immediate
                     // publish() is a deadlock risk in the current design as awaiting here prevents ioctls
diff --git a/src/structs.rs b/src/structs.rs
index f54ec7fcf..d01d5a65c 100644
--- a/src/structs.rs
+++ b/src/structs.rs
@@ -404,3 +404,84 @@ impl EventMask {
         self.events[evt / 8] &= !(1 << (evt % 8));
     }
 }
+
+/// Parameters for a wifi scan
+#[derive(Clone, Copy)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[repr(C)]
+pub struct ScanParams {
+    pub version: u32,
+    pub action: u16,
+    pub sync_id: u16,
+    pub ssid_len: u32,
+    pub ssid: [u8; 32],
+    pub bssid: [u8; 6],
+    pub bss_type: u8,
+    pub scan_type: u8,
+    pub nprobes: u32,
+    pub active_time: u32,
+    pub passive_time: u32,
+    pub home_time: u32,
+    pub channel_num: u32,
+    pub channel_list: [u16; 1],
+}
+impl_bytes!(ScanParams);
+
+/// Wifi Scan Results Header, followed by `bss_count` `BssInfo`
+#[derive(Clone, Copy)]
+// #[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[repr(C, packed(2))]
+pub struct ScanResults {
+    pub buflen: u32,
+    pub version: u32,
+    pub sync_id: u16,
+    pub bss_count: u16,
+}
+impl_bytes!(ScanResults);
+
+impl ScanResults {
+    pub fn parse(packet: &mut [u8]) -> Option<(&mut ScanResults, &mut [u8])> {
+        if packet.len() < ScanResults::SIZE {
+            return None;
+        }
+
+        let (scan_results, bssinfo) = packet.split_at_mut(ScanResults::SIZE);
+        let scan_results = ScanResults::from_bytes_mut(scan_results.try_into().unwrap());
+
+        if scan_results.bss_count > 0 && bssinfo.len() < BssInfo::SIZE {
+            warn!("Scan result, incomplete BssInfo");
+            return None;
+        }
+
+        Some((scan_results, bssinfo))
+    }
+}
+
+/// Wifi Scan Result
+#[derive(Clone, Copy)]
+// #[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[repr(C, packed(2))]
+#[non_exhaustive]
+pub struct BssInfo {
+    pub version: u32,
+    pub length: u32,
+    pub bssid: [u8; 6],
+    pub beacon_period: u16,
+    pub capability: u16,
+    pub ssid_len: u8,
+    pub ssid: [u8; 32],
+    // there will be more stuff here
+}
+impl_bytes!(BssInfo);
+
+impl BssInfo {
+    pub fn parse(packet: &mut [u8]) -> Option<&mut Self> {
+        if packet.len() < BssInfo::SIZE {
+            return None;
+        }
+
+        Some(BssInfo::from_bytes_mut(
+            packet[..BssInfo::SIZE].as_mut().try_into().unwrap(),
+        ))
+    }
+}