use core::fmt::{Debug, Display};
use crate::dm::clusters::net_comm::{
self, NetCtlError, NetworkCommissioningStatusEnum, NetworkType, Networks, NetworksError,
ThreadCapabilitiesBitmap, WirelessCreds,
};
use crate::dm::clusters::{thread_diag, wifi_diag};
use crate::error::{Error, ErrorCode};
use crate::fmt::Bytes;
use crate::persist::{KvBlobStore, NETWORKS_KEY};
use crate::tlv::{FromTLV, TLVElement, TLVTag, TLVWrite, ToTLV};
use crate::transport::network::btp::Btp;
use crate::utils::cell::RefCell;
use crate::utils::init::{init, Init};
use crate::utils::storage::{Vec, WriteBuf};
use crate::utils::sync::blocking;
use crate::utils::sync::DynBase;
use super::NetChangeNotif;
pub use mgr::*;
pub use thread::*;
pub use wifi::*;
mod mgr;
mod thread;
mod wifi;
pub const MAX_WIRELESS_NETWORK_ID_LEN: usize = 32;
pub type OwnedWirelessNetworkId = Vec<u8, MAX_WIRELESS_NETWORK_ID_LEN>;
pub trait WirelessNetwork: Send + for<'a> FromTLV<'a> + ToTLV {
fn id(&self) -> &[u8];
fn init_from<'a>(creds: &'a WirelessCreds<'a>) -> impl Init<Self, Error> + 'a;
fn update(&mut self, creds: &WirelessCreds<'_>) -> Result<(), Error>;
fn creds(&self) -> WirelessCreds<'_>;
#[cfg(not(feature = "defmt"))]
fn display(&self) -> impl Display {
Self::display_id(self.id())
}
#[cfg(feature = "defmt")]
fn display(&self) -> impl Display + defmt::Format {
Self::display_id(self.id())
}
#[cfg(not(feature = "defmt"))]
fn display_id(id: &[u8]) -> impl Display;
#[cfg(feature = "defmt")]
fn display_id(id: &[u8]) -> impl Display + defmt::Format;
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct WirelessNetworks<const N: usize, T> {
networks: crate::utils::storage::Vec<T, N>,
commissioned: bool,
}
impl<const N: usize, T> Default for WirelessNetworks<N, T>
where
T: WirelessNetwork,
{
fn default() -> Self {
Self::new()
}
}
impl<const N: usize, T> WirelessNetworks<N, T>
where
T: WirelessNetwork,
{
pub const fn new() -> Self {
Self {
networks: crate::utils::storage::Vec::new(),
commissioned: false,
}
}
pub fn init() -> impl Init<Self> {
init!(Self {
networks <- crate::utils::storage::Vec::init(),
commissioned: false,
})
}
pub fn reset(&mut self) {
self.networks.clear();
self.commissioned = false;
}
pub async fn reset_persist<S: KvBlobStore>(
&mut self,
mut kv: S,
buf: &mut [u8],
) -> Result<(), Error> {
self.reset();
kv.remove(NETWORKS_KEY, buf)?;
info!("Removed all wireless networks from storage");
Ok(())
}
pub async fn load_persist<S: KvBlobStore>(
&mut self,
mut kv: S,
buf: &mut [u8],
) -> Result<(), Error> {
self.reset();
if let Some(data) = kv.load(NETWORKS_KEY, buf)? {
self.load(data)?;
info!(
"Loaded {} wireless networks from storage",
self.networks.len()
);
}
Ok(())
}
pub fn load(&mut self, data: &[u8]) -> Result<(), Error> {
let root = TLVElement::new(data);
self.networks.clear();
if let Ok(structure) = root.structure() {
for network in structure.ctx(0)?.array()?.iter() {
let network = network?;
self.networks.push_init(T::init_from_tlv(network), || {
ErrorCode::ResourceExhausted.into()
})?;
}
self.commissioned = structure.ctx(1)?.bool()?;
} else {
for network in root.array()?.iter() {
let network = network?;
self.networks.push_init(T::init_from_tlv(network), || {
ErrorCode::ResourceExhausted.into()
})?;
}
self.commissioned = false;
}
Ok(())
}
pub fn store(&self, buf: &mut [u8]) -> Result<usize, Error> {
let mut wb = WriteBuf::new(buf);
wb.start_struct(&TLVTag::Anonymous)?;
self.networks.to_tlv(&TLVTag::Context(0), &mut wb)?;
self.commissioned.to_tlv(&TLVTag::Context(1), &mut wb)?;
wb.end_container()?;
let tail = wb.get_tail();
Ok(tail)
}
pub fn networks<F>(&self, mut f: F) -> Result<(), Error>
where
F: FnMut(&T) -> Result<(), Error>,
{
for network in self.networks.iter() {
f(network)?;
}
Ok(())
}
pub fn network<F>(&self, network_id: &[u8], f: F) -> Result<u8, NetworksError>
where
F: FnOnce(&T) -> Result<(), Error>,
{
let networks = self
.networks
.iter()
.enumerate()
.find(|(_, network)| network.id() == network_id);
if let Some((index, network)) = networks {
f(network)?;
Ok(index as _)
} else {
Err(NetworksError::NetworkIdNotFound)
}
}
pub fn next_network<F>(&self, last_network_id: Option<&[u8]>, f: F) -> Result<bool, Error>
where
F: FnOnce(&T) -> Result<(), Error>,
{
if let Some(last_network_id) = last_network_id {
info!(
"Looking for network after the one with ID: {}",
T::display_id(last_network_id)
);
let mut networks = self.networks.iter();
for network in &mut networks {
if network.id() == last_network_id {
break;
}
}
let network = networks.next();
if let Some(network) = network {
info!("Trying with next network - ID: {}", network.display());
f(network)?;
return Ok(true);
}
}
info!("Wrapping over");
if let Some(network) = self.networks.first() {
info!("Trying with first network - ID: {}", network.display());
f(network)?;
Ok(true)
} else {
info!("No networks available");
Ok(false)
}
}
pub fn add_or_update<A, U>(
&mut self,
network_id: &[u8],
add: A,
update: U,
) -> Result<u8, NetworksError>
where
A: Init<T, Error>,
U: FnOnce(&mut T) -> Result<(), Error>,
{
let unetwork = self
.networks
.iter_mut()
.enumerate()
.find(|(_, unetwork)| unetwork.id() == network_id);
if let Some((index, unetwork)) = unetwork {
update(unetwork)?;
info!("Updated network with ID {}", unetwork.display());
Ok(index as _)
} else if self.networks.len() >= N {
warn!(
"Adding network with ID {} failed: too many",
T::display_id(network_id)
);
Err(NetworksError::BoundsExceeded)
} else {
self.networks
.push_init(add, || ErrorCode::ResourceExhausted.into())?;
info!("Added network with ID {}", T::display_id(network_id));
Ok((self.networks.len() - 1) as _)
}
}
pub fn reorder(&mut self, index: u8, network_id: &[u8]) -> Result<u8, NetworksError> {
let cur_index = self
.networks
.iter()
.position(|conf| conf.id() == network_id);
if let Some(cur_index) = cur_index {
if index < self.networks.len() as u8 {
let conf = self.networks.remove(cur_index);
unwrap!(self.networks.insert(index as usize, conf).map_err(|_| ()));
info!(
"Network with ID {} reordered to index {}",
T::display_id(network_id),
index
);
} else {
warn!(
"Reordering network with ID {} to index {} failed: out of range",
T::display_id(network_id),
index
);
Err(NetworksError::OutOfRange)?;
}
} else {
warn!("Network with ID {} not found", T::display_id(network_id));
Err(NetworksError::NetworkIdNotFound)?;
}
Ok(index)
}
pub fn remove(&mut self, network_id: &[u8]) -> Result<u8, NetworksError> {
let index = self
.networks
.iter()
.position(|conf| conf.id() == network_id);
if let Some(index) = index {
self.networks.remove(index);
info!("Removed network with ID {}", T::display_id(network_id));
Ok(index as _)
} else {
warn!("Network with ID {} not found", T::display_id(network_id));
Err(NetworksError::NetworkIdNotFound)
}
}
pub fn commissioned(&self) -> bool {
self.commissioned
}
pub fn set_commissioned(&mut self, commissioned: bool) {
self.commissioned = commissioned;
}
}
impl<const N: usize, T> Networks for WirelessNetworks<N, T>
where
T: WirelessNetwork,
{
fn max_networks(&self) -> Result<u8, Error> {
Ok(N as _)
}
fn networks(
&self,
f: &mut dyn FnMut(&net_comm::NetworkInfo) -> Result<(), Error>,
) -> Result<(), Error> {
WirelessNetworks::networks(self, |network| {
let network_id = network.id();
let network_info = net_comm::NetworkInfo {
network_id,
connected: false, };
f(&network_info)
})
}
fn creds(
&self,
network_id: &[u8],
f: &mut dyn FnMut(&net_comm::WirelessCreds) -> Result<(), Error>,
) -> Result<u8, NetworksError> {
WirelessNetworks::network(self, network_id, |network| f(&network.creds()))
}
fn next_creds(
&self,
last_network_id: Option<&[u8]>,
f: &mut dyn FnMut(&WirelessCreds) -> Result<(), Error>,
) -> Result<bool, Error> {
WirelessNetworks::next_network(self, last_network_id, |network| f(&network.creds()))
}
fn enabled(&self) -> Result<bool, Error> {
Ok(true)
}
fn set_enabled(&mut self, _enabled: bool) -> Result<(), Error> {
Ok(())
}
fn add_or_update(
&mut self,
creds: &net_comm::WirelessCreds<'_>,
) -> Result<u8, net_comm::NetworksError> {
WirelessNetworks::add_or_update(self, creds.id()?, T::init_from(creds), |network| {
network.update(creds)
})
}
fn reorder(&mut self, index: u8, network_id: &[u8]) -> Result<u8, NetworksError> {
WirelessNetworks::reorder(self, index, network_id)
}
fn remove(&mut self, network_id: &[u8]) -> Result<u8, NetworksError> {
WirelessNetworks::remove(self, network_id)
}
fn commissioned(&self) -> Result<bool, Error> {
Ok(self.commissioned())
}
fn set_commissioned(&mut self, commissioned: bool) -> Result<(), Error> {
WirelessNetworks::set_commissioned(self, commissioned);
Ok(())
}
fn reset(&mut self) -> Result<(), Error> {
WirelessNetworks::reset(self);
Ok(())
}
fn load(&mut self, data: &[u8]) -> Result<(), Error> {
WirelessNetworks::load(self, data)
}
fn save(&self, buf: &mut [u8]) -> Result<Option<usize>, Error> {
WirelessNetworks::store(self, buf).map(Some)
}
}
#[derive(Debug)]
enum DisplayId<'a> {
Wifi(&'a [u8]),
Thread(&'a [u8]),
}
impl Display for DisplayId<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DisplayId::Wifi(id) => {
if let Ok(str) = core::str::from_utf8(id) {
write!(f, "Wifi SSID({})", str)
} else {
write!(f, "Wifi SSID({:?})", Bytes(id))
}
}
DisplayId::Thread(id) => write!(f, "Thread ExtPanID({:?})", Bytes(id)),
}
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for DisplayId<'_> {
fn format(&self, fmt: defmt::Formatter) {
match self {
DisplayId::Wifi(id) => {
if let Ok(str) = core::str::from_utf8(id) {
defmt::write!(fmt, "Wifi SSID({})", str)
} else {
defmt::write!(fmt, "Wifi SSID({:?})", Bytes(id))
}
}
DisplayId::Thread(id) => defmt::write!(fmt, "Thread ExtPanID({:?})", Bytes(id)),
}
}
}
pub struct NoopWirelessNetCtl(NetworkType);
impl NoopWirelessNetCtl {
pub const fn new(net_type: NetworkType) -> Self {
Self(net_type)
}
}
impl net_comm::NetCtl for NoopWirelessNetCtl {
fn net_type(&self) -> NetworkType {
self.0
}
async fn scan<F>(&self, _network: Option<&[u8]>, _f: F) -> Result<(), NetCtlError>
where
F: FnOnce(&net_comm::NetworkScanInfo) -> Result<(), Error>,
{
Err(NetCtlError::Other(ErrorCode::InvalidAction.into()))
}
async fn connect(&self, creds: &WirelessCreds<'_>) -> Result<(), NetCtlError> {
Ok(creds.check_match(self.0)?)
}
}
impl NetChangeNotif for NoopWirelessNetCtl {
async fn wait_changed(&self) {
core::future::pending().await
}
}
impl DynBase for NoopWirelessNetCtl {}
impl wifi_diag::WirelessDiag for NoopWirelessNetCtl {}
impl wifi_diag::WifiDiag for NoopWirelessNetCtl {}
impl thread_diag::ThreadDiag for NoopWirelessNetCtl {}
pub struct NetCtlState {
pub network_id: OwnedWirelessNetworkId,
pub networking_status: Option<NetworkCommissioningStatusEnum>,
pub connect_error_value: Option<i32>,
}
impl NetCtlState {
pub const fn new() -> Self {
Self {
network_id: OwnedWirelessNetworkId::new(),
networking_status: None,
connect_error_value: None,
}
}
pub fn init() -> impl Init<Self> {
init!(Self {
network_id <- OwnedWirelessNetworkId::init(),
networking_status: None,
connect_error_value: None,
})
}
pub const fn new_with_mutex() -> NetCtlStateMutex {
blocking::Mutex::new(RefCell::new(Self::new()))
}
pub fn init_with_mutex() -> impl Init<NetCtlStateMutex> {
blocking::Mutex::init(RefCell::init(init!(Self {
network_id <- OwnedWirelessNetworkId::init(),
networking_status: None,
connect_error_value: None,
})))
}
pub fn is_prov_ready(&self) -> bool {
!self.network_id.is_empty()
&& matches!(
self.networking_status,
Some(NetworkCommissioningStatusEnum::Success)
)
&& self.connect_error_value.is_none()
}
pub fn update<R>(
&mut self,
network_id: Option<&[u8]>,
result: Result<R, NetCtlError>,
) -> Result<R, NetCtlError> {
self.network_id.clear();
if let Some(network_id) = network_id {
unwrap!(self.network_id.extend_from_slice(network_id));
}
if let Some((status, err_code)) = NetworkCommissioningStatusEnum::map_ctl_status(&result) {
self.networking_status = Some(status);
self.connect_error_value = err_code;
} else {
self.networking_status = None;
self.connect_error_value = None;
}
result
}
pub fn update_with_mutex<R>(
state: &NetCtlStateMutex,
network_id: Option<&[u8]>,
result: Result<R, NetCtlError>,
) -> Result<R, NetCtlError> {
state.lock(|state| state.borrow_mut().update(network_id, result))
}
pub async fn wait_prov_ready(state: &NetCtlStateMutex, _btp: &Btp) {
while !state.lock(|state| state.borrow().is_prov_ready()) {
embassy_time::Timer::after_secs(1).await;
}
}
}
impl Default for NetCtlState {
fn default() -> Self {
Self::new()
}
}
pub type NetCtlStateMutex = blocking::Mutex<RefCell<NetCtlState>>;
pub struct NetCtlWithStatusImpl<'a, T> {
state: &'a NetCtlStateMutex,
net_ctl: T,
}
impl<'a, T> NetCtlWithStatusImpl<'a, T> {
pub const fn new(state: &'a NetCtlStateMutex, net_ctl: T) -> Self {
Self { state, net_ctl }
}
}
impl<T> net_comm::NetCtl for NetCtlWithStatusImpl<'_, T>
where
T: net_comm::NetCtl,
{
fn net_type(&self) -> NetworkType {
self.net_ctl.net_type()
}
fn connect_max_time_seconds(&self) -> u8 {
self.net_ctl.connect_max_time_seconds()
}
fn scan_max_time_seconds(&self) -> u8 {
self.net_ctl.scan_max_time_seconds()
}
fn supported_wifi_bands<F>(&self, f: F) -> Result<(), Error>
where
F: FnMut(net_comm::WiFiBandEnum) -> Result<(), Error>,
{
self.net_ctl.supported_wifi_bands(f)
}
fn supported_thread_features(&self) -> ThreadCapabilitiesBitmap {
self.net_ctl.supported_thread_features()
}
fn thread_version(&self) -> u16 {
self.net_ctl.thread_version()
}
async fn scan<F>(&self, network: Option<&[u8]>, f: F) -> Result<(), NetCtlError>
where
F: FnMut(&net_comm::NetworkScanInfo) -> Result<(), Error>,
{
self.net_ctl.scan(network, f).await
}
async fn connect(&self, creds: &WirelessCreds<'_>) -> Result<(), NetCtlError> {
NetCtlState::update_with_mutex(
self.state,
Some(creds.id()?),
self.net_ctl.connect(creds).await,
)
}
}
impl<T> net_comm::NetCtlStatus for NetCtlWithStatusImpl<'_, T>
where
T: net_comm::NetCtl,
{
fn last_networking_status(&self) -> Result<Option<NetworkCommissioningStatusEnum>, Error> {
Ok(self.state.lock(|state| state.borrow().networking_status))
}
fn last_network_id<F, R>(&self, f: F) -> Result<R, Error>
where
F: FnOnce(Option<&[u8]>) -> Result<R, Error>,
{
self.state.lock(|state| {
let state = state.borrow();
if state.network_id.is_empty() {
f(None)
} else {
f(Some(&state.network_id))
}
})
}
fn last_connect_error_value(&self) -> Result<Option<i32>, Error> {
Ok(self.state.lock(|state| state.borrow().connect_error_value))
}
}
impl<T> NetChangeNotif for NetCtlWithStatusImpl<'_, T>
where
T: NetChangeNotif,
{
async fn wait_changed(&self) {
self.net_ctl.wait_changed().await
}
}
#[cfg(feature = "sync-mutex")]
impl<T> DynBase for NetCtlWithStatusImpl<'_, T> where T: Send + Sync {}
#[cfg(not(feature = "sync-mutex"))]
impl<T> DynBase for NetCtlWithStatusImpl<'_, T> {}
impl<T> wifi_diag::WirelessDiag for NetCtlWithStatusImpl<'_, T>
where
T: wifi_diag::WirelessDiag,
{
fn connected(&self) -> Result<bool, Error> {
self.net_ctl.connected()
}
}
impl<T> wifi_diag::WifiDiag for NetCtlWithStatusImpl<'_, T>
where
T: wifi_diag::WifiDiag,
{
fn bssid(&self, f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>) -> Result<(), Error> {
self.net_ctl.bssid(f)
}
fn security_type(&self) -> Result<crate::tlv::Nullable<wifi_diag::SecurityTypeEnum>, Error> {
self.net_ctl.security_type()
}
fn wi_fi_version(&self) -> Result<crate::tlv::Nullable<wifi_diag::WiFiVersionEnum>, Error> {
self.net_ctl.wi_fi_version()
}
fn channel_number(&self) -> Result<crate::tlv::Nullable<u16>, Error> {
self.net_ctl.channel_number()
}
fn rssi(&self) -> Result<crate::tlv::Nullable<i8>, Error> {
self.net_ctl.rssi()
}
}
impl<T> thread_diag::ThreadDiag for NetCtlWithStatusImpl<'_, T>
where
T: thread_diag::ThreadDiag,
{
fn channel(&self) -> Result<Option<u16>, Error> {
self.net_ctl.channel()
}
fn routing_role(&self) -> Result<Option<thread_diag::RoutingRoleEnum>, Error> {
self.net_ctl.routing_role()
}
fn network_name(
&self,
f: &mut dyn FnMut(Option<&str>) -> Result<(), Error>,
) -> Result<(), Error> {
self.net_ctl.network_name(f)
}
fn pan_id(&self) -> Result<Option<u16>, Error> {
self.net_ctl.pan_id()
}
fn extended_pan_id(&self) -> Result<Option<u64>, Error> {
self.net_ctl.extended_pan_id()
}
fn mesh_local_prefix(
&self,
f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
) -> Result<(), Error> {
self.net_ctl.mesh_local_prefix(f)
}
fn neighbor_table(
&self,
f: &mut dyn FnMut(&thread_diag::NeighborTable) -> Result<(), Error>,
) -> Result<(), Error> {
self.net_ctl.neighbor_table(f)
}
fn route_table(
&self,
f: &mut dyn FnMut(&thread_diag::RouteTable) -> Result<(), Error>,
) -> Result<(), Error> {
self.net_ctl.route_table(f)
}
fn partition_id(&self) -> Result<Option<u32>, Error> {
self.net_ctl.partition_id()
}
fn weighting(&self) -> Result<Option<u16>, Error> {
self.net_ctl.weighting()
}
fn data_version(&self) -> Result<Option<u16>, Error> {
self.net_ctl.data_version()
}
fn stable_data_version(&self) -> Result<Option<u16>, Error> {
self.net_ctl.stable_data_version()
}
fn leader_router_id(&self) -> Result<Option<u8>, Error> {
self.net_ctl.leader_router_id()
}
fn security_policy(&self) -> Result<Option<thread_diag::SecurityPolicy>, Error> {
self.net_ctl.security_policy()
}
fn channel_page0_mask(
&self,
f: &mut dyn FnMut(Option<&[u8]>) -> Result<(), Error>,
) -> Result<(), Error> {
self.net_ctl.channel_page0_mask(f)
}
fn operational_dataset_components(
&self,
f: &mut dyn FnMut(Option<&thread_diag::OperationalDatasetComponents>) -> Result<(), Error>,
) -> Result<(), Error> {
self.net_ctl.operational_dataset_components(f)
}
fn active_network_faults_list(
&self,
f: &mut dyn FnMut(thread_diag::NetworkFaultEnum) -> Result<(), Error>,
) -> Result<(), Error> {
self.net_ctl.active_network_faults_list(f)
}
}
#[cfg(test)]
mod tests {
use crate::dm::clusters::net_comm::{
NetworksAccess, NetworksError, SharedNetworks, WirelessCreds,
};
use super::wifi::{Wifi, WifiNetworks};
use super::WirelessNetwork;
fn wifi_creds<'a>(ssid: &'a [u8], pass: &'a [u8]) -> WirelessCreds<'a> {
WirelessCreds::Wifi { ssid, pass }
}
fn collect_ssids(nets: &WifiNetworks<4>) -> Vec<Vec<u8>> {
let mut ids = Vec::new();
nets.networks(|n| {
ids.push(n.id().to_vec());
Ok(())
})
.unwrap();
ids
}
#[test]
fn add_networks() {
let mut nets = WifiNetworks::<4>::new();
let idx = nets
.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"PassA")), |_| {
Ok(())
})
.unwrap();
assert_eq!(idx, 0);
let idx = nets
.add_or_update(b"B", Wifi::init_from(&wifi_creds(b"B", b"PassB")), |_| {
Ok(())
})
.unwrap();
assert_eq!(idx, 1);
assert_eq!(collect_ssids(&nets), vec![b"A".to_vec(), b"B".to_vec()]);
}
#[test]
fn update_existing_network() {
let mut nets = WifiNetworks::<4>::new();
nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"Old")), |_| Ok(()))
.unwrap();
nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"New")), |wifi| {
wifi.update(&wifi_creds(b"A", b"New"))
})
.unwrap();
assert_eq!(collect_ssids(&nets).len(), 1);
let mut pass = Vec::new();
nets.network(b"A", |w| {
if let WirelessCreds::Wifi { pass: p, .. } = w.creds() {
pass.extend_from_slice(p);
}
Ok(())
})
.unwrap();
assert_eq!(pass, b"New");
}
#[test]
fn add_exceeds_capacity() {
let mut nets = WifiNetworks::<2>::new();
nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"p")), |_| Ok(()))
.unwrap();
nets.add_or_update(b"B", Wifi::init_from(&wifi_creds(b"B", b"p")), |_| Ok(()))
.unwrap();
let err = nets.add_or_update(b"C", Wifi::init_from(&wifi_creds(b"C", b"p")), |_| Ok(()));
assert!(matches!(err, Err(NetworksError::BoundsExceeded)));
}
#[test]
fn remove_network() {
let mut nets = WifiNetworks::<4>::new();
nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"p")), |_| Ok(()))
.unwrap();
nets.add_or_update(b"B", Wifi::init_from(&wifi_creds(b"B", b"p")), |_| Ok(()))
.unwrap();
let idx = nets.remove(b"A").unwrap();
assert_eq!(idx, 0);
assert_eq!(collect_ssids(&nets), vec![b"B".to_vec()]);
}
#[test]
fn remove_nonexistent() {
let mut nets = WifiNetworks::<4>::new();
assert!(matches!(
nets.remove(b"X"),
Err(NetworksError::NetworkIdNotFound)
));
}
#[test]
fn reorder_moves_to_front() {
let mut nets = WifiNetworks::<4>::new();
for id in [b"A", b"B", b"C"] {
nets.add_or_update(
id.as_slice(),
Wifi::init_from(&wifi_creds(id, b"p")),
|_| Ok(()),
)
.unwrap();
}
nets.reorder(0, b"C").unwrap();
assert_eq!(
collect_ssids(&nets),
vec![b"C".to_vec(), b"A".to_vec(), b"B".to_vec()]
);
}
#[test]
fn reorder_out_of_range() {
let mut nets = WifiNetworks::<4>::new();
nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"p")), |_| Ok(()))
.unwrap();
assert!(matches!(
nets.reorder(5, b"A"),
Err(NetworksError::OutOfRange)
));
}
#[test]
fn reorder_nonexistent() {
let mut nets = WifiNetworks::<4>::new();
assert!(matches!(
nets.reorder(0, b"X"),
Err(NetworksError::NetworkIdNotFound)
));
}
#[test]
fn next_network_iterates_and_wraps() {
let mut nets = WifiNetworks::<4>::new();
for id in [b"A", b"B", b"C"] {
nets.add_or_update(
id.as_slice(),
Wifi::init_from(&wifi_creds(id, b"p")),
|_| Ok(()),
)
.unwrap();
}
let get_next = |last: Option<&[u8]>| -> Option<Vec<u8>> {
let mut result = None;
let found = nets
.next_network(last, |w| {
result = Some(w.id().to_vec());
Ok(())
})
.unwrap();
if found {
result
} else {
None
}
};
assert_eq!(get_next(None), Some(b"A".to_vec()));
assert_eq!(get_next(Some(b"A")), Some(b"B".to_vec()));
assert_eq!(get_next(Some(b"B")), Some(b"C".to_vec()));
assert_eq!(get_next(Some(b"C")), Some(b"A".to_vec()));
assert_eq!(get_next(Some(b"Z")), Some(b"A".to_vec()));
}
#[test]
fn next_network_empty_returns_false() {
let nets = WifiNetworks::<4>::new();
let found = nets.next_network(None, |_| Ok(())).unwrap();
assert!(!found);
}
#[test]
fn store_load_round_trip() {
let mut nets = WifiNetworks::<4>::new();
nets.add_or_update(
b"Net1",
Wifi::init_from(&wifi_creds(b"Net1", b"P1")),
|_| Ok(()),
)
.unwrap();
nets.add_or_update(
b"Net2",
Wifi::init_from(&wifi_creds(b"Net2", b"P2")),
|_| Ok(()),
)
.unwrap();
nets.set_commissioned(true);
let mut buf = [0u8; 512];
let len = nets.store(&mut buf).unwrap();
let mut loaded = WifiNetworks::<4>::new();
loaded.load(&buf[..len]).unwrap();
assert_eq!(collect_ssids(&loaded), collect_ssids(&nets));
assert!(loaded.commissioned());
}
#[test]
fn commissioned_default_false() {
let nets = WifiNetworks::<4>::new();
assert!(!nets.commissioned());
}
#[test]
fn set_commissioned() {
let mut nets = WifiNetworks::<4>::new();
nets.set_commissioned(true);
assert!(nets.commissioned());
nets.set_commissioned(false);
assert!(!nets.commissioned());
}
#[test]
fn reset_clears_all() {
let mut nets = WifiNetworks::<4>::new();
nets.add_or_update(b"A", Wifi::init_from(&wifi_creds(b"A", b"p")), |_| Ok(()))
.unwrap();
nets.set_commissioned(true);
nets.reset();
assert!(collect_ssids(&nets).is_empty());
assert!(!nets.commissioned());
}
#[test]
fn shared_networks_access_add_and_read() {
let shared = SharedNetworks::new(WifiNetworks::<4>::new());
shared.access(|networks| {
networks
.add_or_update(&wifi_creds(b"SSID1", b"pass1"))
.unwrap();
networks
.add_or_update(&wifi_creds(b"SSID2", b"pass2"))
.unwrap();
});
let count = shared.access(|networks| {
let mut count = 0u8;
networks
.networks(&mut |_info| {
count += 1;
Ok(())
})
.unwrap();
count
});
assert_eq!(count, 2);
}
#[test]
fn shared_networks_commissioned_via_access() {
let shared = SharedNetworks::new(WifiNetworks::<4>::new());
let commissioned = shared.access(|networks| networks.commissioned().unwrap());
assert!(!commissioned);
shared.access(|networks| networks.set_commissioned(true).unwrap());
let commissioned = shared.access(|networks| networks.commissioned().unwrap());
assert!(commissioned);
}
#[test]
fn shared_networks_next_creds_round_robin() {
let shared = SharedNetworks::new(WifiNetworks::<4>::new());
shared.access(|networks| {
for (ssid, pass) in [(b"A", b"pA"), (b"B", b"pB"), (b"C", b"pC")] {
networks
.add_or_update(&wifi_creds(ssid.as_slice(), pass.as_slice()))
.unwrap();
}
});
let get_next_ssid = |last: Option<&[u8]>| -> Option<Vec<u8>> {
shared.access(|networks| {
let mut result = None;
let found = networks
.next_creds(last, &mut |creds| {
if let WirelessCreds::Wifi { ssid, .. } = creds {
result = Some(ssid.to_vec());
}
Ok(())
})
.unwrap();
if found {
result
} else {
None
}
})
};
assert_eq!(get_next_ssid(None), Some(b"A".to_vec()));
assert_eq!(get_next_ssid(Some(b"A")), Some(b"B".to_vec()));
assert_eq!(get_next_ssid(Some(b"C")), Some(b"A".to_vec()));
}
#[test]
fn load_old_format_bare_array() {
use crate::tlv::{TLVTag, TLVWrite};
use crate::utils::storage::WriteBuf;
let mut buf = [0u8; 512];
let mut wb = WriteBuf::new(&mut buf);
wb.start_array(&TLVTag::Anonymous).unwrap();
wb.start_struct(&TLVTag::Anonymous).unwrap();
wb.str(&TLVTag::Context(0), b"A").unwrap();
wb.str(&TLVTag::Context(1), b"pA").unwrap();
wb.end_container().unwrap();
wb.start_struct(&TLVTag::Anonymous).unwrap();
wb.str(&TLVTag::Context(0), b"B").unwrap();
wb.str(&TLVTag::Context(1), b"pB").unwrap();
wb.end_container().unwrap();
wb.end_container().unwrap();
let len = wb.get_tail();
let mut nets = WifiNetworks::<4>::new();
nets.load(&buf[..len]).unwrap();
assert_eq!(collect_ssids(&nets), vec![b"A".to_vec(), b"B".to_vec()]);
assert!(
!nets.commissioned(),
"Old format should default commissioned to false"
);
}
#[test]
fn shared_networks_save_does_not_trigger_change() {
use core::pin::pin;
use embassy_futures::select::{select, Either};
let shared = SharedNetworks::new(WifiNetworks::<4>::new());
shared.access(|n| n.add_or_update(&wifi_creds(b"A", b"p")).unwrap());
embassy_futures::block_on(shared.wait_state_changed());
shared.access(|n| {
let mut buf = [0u8; 512];
n.save(&mut buf).unwrap();
});
let notified = embassy_futures::block_on(async {
match select(
pin!(shared.wait_state_changed()),
pin!(core::future::ready(())),
)
.await
{
Either::First(_) => true,
Either::Second(_) => false,
}
});
assert!(!notified, "save() must not trigger change notification");
}
}