diff --git a/embassy/demos/common/src/config.rs b/embassy/demos/common/src/config.rs
index 1585ab9629a5be14dee09a63b42b5b9f23c6785e..829b4882e9ddbf448354a32e2478708fd2200488 100644
--- a/embassy/demos/common/src/config.rs
+++ b/embassy/demos/common/src/config.rs
@@ -12,7 +12,9 @@ use defmt::{debug, error, info, panic, trace, warn};
 use hmac::{Hmac, Mac};
 use sha2::Sha256;
 
-use heapless::String;
+use heapless::{String, Vec};
+
+use embassy_net::{StaticConfigV4, Ipv4Cidr, Ipv4Address};
 
 use sunset_sshwire_derive::*;
 
@@ -51,7 +53,11 @@ pub struct SSHConfig {
     /// WPA2 passphrase. None is Open network.
     pub wifi_pw: Option<String<63>>,
 
+    /// For wl5500. cyw43 uses its own internal
     pub mac: [u8; 6],
+
+    /// `None` for DHCP
+    pub ip4_static: Option<StaticConfigV4>,
 }
 
 fn random_mac() -> Result<[u8; 6]> {
@@ -62,15 +68,14 @@ fn random_mac() -> Result<[u8; 6]> {
     Ok(mac)
 }
 
-
 impl SSHConfig {
     /// Bump this when the format changes
-    pub const CURRENT_VERSION: u8 = 5;
+    pub const CURRENT_VERSION: u8 = 6;
     /// A buffer this large will fit any SSHConfig.
     // It can be updated by looking at
     // `cargo test -- roundtrip_config`
     // in the demos/common directory
-    pub const BUF_SIZE: usize = 449;
+    pub const BUF_SIZE: usize = 460;
 
     /// Creates a new config with default parameters.
     ///
@@ -91,6 +96,7 @@ impl SSHConfig {
             wifi_net,
             wifi_pw,
             mac,
+            ip4_static: None,
         })
     }
 
@@ -152,6 +158,38 @@ where
     bool::dec(s)?.then(|| SSHDecode::dec(s)).transpose()
 }
 
+fn enc_ip4config(v: &Option<StaticConfigV4>, s: &mut dyn SSHSink) -> WireResult<()> {
+    v.is_some().enc(s)?;
+    if let Some(v) = v {
+        v.address.address().0.enc(s)?;
+        v.address.prefix_len().enc(s)?;
+        // to [u8; 4]
+        let gw = v.gateway.map(|a| a.0);
+        enc_option(&gw, s)?;
+    }
+    Ok(())
+}
+
+fn dec_ip4config<'de, S>(s: &mut S) -> WireResult<Option<StaticConfigV4>>
+where
+    S: SSHSource<'de>,
+{
+    let opt = bool::dec(s)?;
+    opt.then(|| {
+        let ad: [u8; 4] = SSHDecode::dec(s)?;
+        let ad = Ipv4Address::from_bytes(&ad);
+        let prefix = SSHDecode::dec(s)?;
+        let gw: Option<[u8; 4]> = dec_option(s)?;
+        let gateway = gw.map(|gw| Ipv4Address::from_bytes(&gw));
+        Ok(StaticConfigV4 {
+            address: Ipv4Cidr::new(ad, prefix),
+            gateway,
+            dns_servers: Vec::new(),
+        })
+    })
+    .transpose()
+}
+
 impl SSHEncode for SSHConfig {
     fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> {
         info!("enc si");
@@ -176,6 +214,8 @@ impl SSHEncode for SSHConfig {
 
         self.mac.enc(s)?;
 
+        enc_ip4config(&self.ip4_static, s)?;
+
         Ok(())
     }
 }
@@ -208,6 +248,8 @@ impl<'de> SSHDecode<'de> for SSHConfig {
 
         let mac = SSHDecode::dec(s)?;
 
+        let ip4_static = dec_ip4config(s)?;
+
         Ok(Self {
             hostkey,
             console_pw,
@@ -218,6 +260,7 @@ impl<'de> SSHDecode<'de> for SSHConfig {
             wifi_net,
             wifi_pw,
             mac,
+            ip4_static,
         })
     }
 }
@@ -320,7 +363,13 @@ mod tests {
             wifi_pw: Some(
                 core::str::from_utf8([b'f'; 63].as_slice()).unwrap().into(),
             ),
-            mac: [6,2,3,4,5,6],
+            mac: [6, 2, 3, 4, 5, 6],
+            ip4_static: Some(embassy_net::StaticConfigV4 {
+                address: embassy_net::Ipv4Cidr::new(embassy_net::Ipv4Address::UNSPECIFIED, 8),
+                gateway: Some(embassy_net::Ipv4Address::UNSPECIFIED),
+                // no dns servers. may need changing later?
+                dns_servers: heapless::Vec::new(),
+            }),
         };
 
         // test once to determine size to print
diff --git a/embassy/demos/picow/src/main.rs b/embassy/demos/picow/src/main.rs
index 16d292a3e60843db7ce1f786759ecadfeca33242..47d64ea8e8f0d8c2484eb6eba3757d3f0a79b607 100644
--- a/embassy/demos/picow/src/main.rs
+++ b/embassy/demos/picow/src/main.rs
@@ -49,6 +49,9 @@ mod wifi;
 #[cfg(not(any(feature = "cyw43", feature = "w5500")))]
 compile_error!("No network device selected. Use cyw43 or w5500 feature");
 
+#[cfg(all(feature = "cyw43", feature = "w5500"))]
+compile_error!("Select only one of cyw43 or w5500");
+
 use demo_common::{SSHConfig, Shell};
 
 use takepipe::TakePipe;
diff --git a/embassy/demos/picow/src/picowmenu.rs b/embassy/demos/picow/src/picowmenu.rs
index f54296ae30b5fef882ddda830a0a4057dcd29b37..58c54922f1ab80729958ad10d0dee58b5e05c337 100644
--- a/embassy/demos/picow/src/picowmenu.rs
+++ b/embassy/demos/picow/src/picowmenu.rs
@@ -13,23 +13,25 @@ use core::fmt::Write;
 use core::future::{poll_fn, Future};
 use core::ops::DerefMut;
 use core::sync::atomic::Ordering::{Relaxed, SeqCst};
+use core::str::FromStr;
 
 use embedded_io::{asynch, Io};
 
 use embassy_sync::waitqueue::MultiWakerRegistration;
 use embassy_time::Duration;
+use embassy_net::{Ipv4Cidr, Ipv4Address};
 
 use heapless::{String, Vec};
 
-use crate::flashconfig;
 use crate::demo_common;
+use crate::flashconfig;
 use crate::GlobalState;
 use demo_common::{BufOutput, SSHConfig};
 
 use demo_common::menu::*;
 
-use sunset::*;
 use sunset::packets::Ed25519PubKey;
+use sunset::*;
 
 // arbitrary in bytes, for sizing buffers
 const MAX_PW_LEN: usize = 50;
@@ -95,7 +97,10 @@ impl MenuCtx {
                 let _ = writeln!(self.out, "serial can't loop");
             } else {
                 if self.state.usb_pipe.is_in_use() {
-                    let _ = writeln!(self.out, "Opening usb1, stealing existing session");
+                    let _ = writeln!(
+                        self.out,
+                        "Opening usb1, stealing existing session"
+                    );
                 } else {
                     let _ = writeln!(self.out, "Opening usb1");
                 }
@@ -111,7 +116,10 @@ impl MenuCtx {
                 let _ = writeln!(self.out, "serial can't loop");
             } else {
                 if self.state.serial1_pipe.is_in_use() {
-                    let _ = writeln!(self.out, "Opening serial1, stealing existing session");
+                    let _ = writeln!(
+                        self.out,
+                        "Opening serial1, stealing existing session"
+                    );
                 } else {
                     let _ = writeln!(self.out, "Opening serial1");
                 }
@@ -404,6 +412,32 @@ const NET_ITEM: Item<MenuCtx> = Item {
                 },
                 help: None,
             },
+            &Item {
+                command: "dhcp",
+                item_type: ItemType::Callback {
+                    parameters: &[],
+                    function: do_net_dhcp,
+                },
+                help: None,
+            },
+            &Item {
+                command: "static",
+                item_type: ItemType::Callback {
+                    parameters: &[
+                        Parameter::Mandatory {
+                            parameter_name: "address/netmask",
+                            help: None,
+                        },
+                        Parameter::Optional {
+                            parameter_name: "gateway",
+                            help: None,
+                        },
+                    ],
+
+                    function: do_net_static,
+                },
+                help: None,
+            },
         ],
         entry: None,
         exit: None,
@@ -607,12 +641,10 @@ fn do_admin_clear_pw(_item: &Item<MenuCtx>, args: &[&str], context: &mut MenuCtx
 // fn do_gpio_set(_item: &Item<MenuCtx>, _args: &[&str], _context: &mut MenuCtx) {}
 
 fn do_erase_config(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) {
-    context.with_config(|c, out| {
-        match SSHConfig::new() {
-            Ok(n) => *c = n,
-            Err(e) => {
-                let _ = writeln!(out, "failed: {e}");
-            }
+    context.with_config(|c, out| match SSHConfig::new() {
+        Ok(n) => *c = n,
+        Err(e) => {
+            let _ = writeln!(out, "failed: {e}");
         }
     });
     context.need_save = true;
@@ -633,7 +665,8 @@ fn do_bootsel(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) {
 fn do_about(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) {
     let _ = writeln!(
         context,
-        "Sunset SSH, USB serial\nMatt Johnston <matt@ucc.asn.au>\n{}", env!("GIT_REV"),
+        "Sunset SSH, USB serial\nMatt Johnston <matt@ucc.asn.au>\n{}",
+        env!("GIT_REV"),
     );
 }
 
@@ -696,6 +729,34 @@ fn do_net_info(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) {
     });
 }
 
+fn do_net_dhcp(_item: &Item<MenuCtx>, _args: &[&str], context: &mut MenuCtx) {
+    context.with_config(|c, out| {
+        c.ip4_static = None;
+    });
+}
+
+fn do_net_static(_item: &Item<MenuCtx>, args: &[&str], context: &mut MenuCtx) {
+    context.with_config(|c, out| {
+        let ip = Ipv4Cidr::from_str(args[0]);
+        let gw = if args[1].is_empty() {
+            Ok(None)
+        } else {
+            Some(Ipv4Address::from_str(args[1])).transpose()
+        };
+        match (ip, gw) {
+            (Ok(address), Ok(gateway)) => {
+                c.ip4_static = Some(embassy_net::StaticConfigV4 {
+                    address,
+                    gateway,
+                    dns_servers: Vec::new(),
+                })
+            }
+            _ => {
+                let _ = write!(out, "Bad args");
+            }
+        }
+    });
+}
 
 // Returns an error on EOF etc.
 pub(crate) async fn request_pw<E>(
diff --git a/embassy/demos/picow/src/w5500.rs b/embassy/demos/picow/src/w5500.rs
index dc9600dbd69abf37fd298f7d822f661e8b66fa1c..324ebec8ef3c474d7ae9691210d643ef8e68ca50 100644
--- a/embassy/demos/picow/src/w5500.rs
+++ b/embassy/demos/picow/src/w5500.rs
@@ -72,13 +72,19 @@ pub(crate) async fn w5500_stack(
     .await;
     spawner.spawn(ethernet_task(runner)).unwrap();
 
+    let config = if let Some(ref s) = config.lock().await.ip4_static {
+        embassy_net::Config::ipv4_static(s.clone())
+    } else {
+        embassy_net::Config::dhcpv4(Default::default())
+    };
+
     // Generate random seed
     let seed = OsRng.next_u64();
 
     // Init network stack
     let stack = &*make_static!(Stack::new(
         device,
-        embassy_net::Config::dhcpv4(Default::default()),
+        config,
         make_static!(StackResources::<{ crate::NUM_SOCKETS }>::new()),
         seed
     ));
diff --git a/embassy/demos/picow/src/wifi.rs b/embassy/demos/picow/src/wifi.rs
index 1a02d2e7891190a41473da664a340c19783e60af..a455ffd3134906b37199bda2e87384f1c5277ded 100644
--- a/embassy/demos/picow/src/wifi.rs
+++ b/embassy/demos/picow/src/wifi.rs
@@ -86,7 +86,11 @@ pub(crate) async fn wifi_stack(spawner: &Spawner,
         }
     }
 
-    let config = embassy_net::Config::dhcpv4(Default::default());
+    let config = if let Some(ref s) = config.lock().await.ip4_static {
+        embassy_net::Config::ipv4_static(s.clone())
+    } else {
+        embassy_net::Config::dhcpv4(Default::default())
+    };
 
     let seed = OsRng.next_u64();