diff --git a/RELEASE-v0.9.md b/RELEASE-v0.9.md index 75d93feee..679bafba2 100644 --- a/RELEASE-v0.9.md +++ b/RELEASE-v0.9.md @@ -252,6 +252,8 @@ perform the Xous firmware upgrade. This requires running manual update commands, - highly experimental TLS support when the "tls" feature is enabled. Relies on a pure-Rust implementation of `ring` located in the `ring-xous` fork. - performance monitoring framework added. see `tools/perflib` and `services/shellchat/net_cmd.rs` for examples of how to use it. - HOTP support added to `vault`. HOTPs display in the TOTP window with the notation "HOTP" next to them, and they auto-increment on autotype. When creating new items in TOTP you are given the option to make an HOTP record, and you can convert between the two by editing the record and changing the bottom line from totp to hotp and vice-versa. The `timestep` field is re-used from the TOTP record to store the `count` for HOTP. +- `ball` app demo removed from default build +- WLAN submenu added, thanks to a huge effort by @gsora. ## Roadmap to 1.0 diff --git a/services/com/src/lib.rs b/services/com/src/lib.rs index 951639c95..4a5aacda4 100644 --- a/services/com/src/lib.rs +++ b/services/com/src/lib.rs @@ -451,10 +451,11 @@ impl Com { } } + /// This blocks until the COM responds from initializing the Wifi chip pub fn wlan_set_on(&mut self) -> Result { send_message( self.conn, - Message::new_scalar(Opcode::WlanOn.to_usize().unwrap(), 0, 0, 0, 0), + Message::new_blocking_scalar(Opcode::WlanOn.to_usize().unwrap(), 0, 0, 0, 0), ) } @@ -505,7 +506,7 @@ impl Com { /// Note: applications should poll the `NetManager::read_wifi_state()` call for wifi status information, not the COM directly. /// This is because the `wlan_status` call is fairly heavy weight, and the `NetManager::read_wifi_state()` will cache /// this information making the status check lighter-weight overall. - pub fn wlan_status(&mut self) -> Result { + pub fn wlan_status(&self) -> Result { let status = WlanStatusIpc::default(); let mut buf = Buffer::into_buf(status).or(Err(xous::Error::InternalError))?; buf.lend_mut(self.conn, Opcode::WlanStatus.to_u32().unwrap()).or(Err(xous::Error::InternalError))?; @@ -616,6 +617,14 @@ impl Com { Err(xous::Error::InternalError) } } + pub fn wlan_is_reset_hold(&self) -> Result { + let status = self.wlan_status()?; + if status.link_state == com_rs_ref::LinkState::ResetHold { + Ok(true) + } else { + Ok(false) + } + } pub fn ints_enable(&self, int_list: &[ComIntSources]) { let mut mask_val: u16 = 0; diff --git a/services/com/src/main.rs b/services/com/src/main.rs index c99662797..bd1245b04 100644 --- a/services/com/src/main.rs +++ b/services/com/src/main.rs @@ -891,11 +891,37 @@ fn main() -> ! { buffer.replace(ssid_ret).unwrap(); } Some(Opcode::WlanOn) => { - info!("TODO: implement WlanOn"); com.txrx(ComState::WLAN_ON.verb); + // re-sync the link, because the COM will take about a second to reload the Wifi drivers + #[cfg(not(any(windows, unix)))] // avoid errors in hosted mode + { + ticktimer.sleep_ms(100).unwrap(); // give the EC a moment to de-chatter + let mut attempts = 0; + let ping_value = 0xaeedu16; // for various reasons, this is a string that is "unlikely" to happen randomly + loop { + com.txrx(ComState::LINK_PING.verb); + com.txrx(ping_value); + let pong = com.wait_txrx(ComState::LINK_READ.verb, Some(150)); // this should "stall" until the EC comes out of reset + let phase = com.wait_txrx(ComState::LINK_READ.verb, Some(150)); + if pong == !ping_value && + phase == 0x600d { // 0x600d is a hard-coded constant. It's included to confirm that we aren't "wedged" just sending one value back at us + log::info!("Wifi on: link established"); + break; + } else { + log::info!("Wifi on: establishing link sync, attempt {} [{:04x}/{:04x}]", attempts, pong, phase); + com.txrx(ComState::LINK_SYNC.verb); + ticktimer.sleep_ms(200).unwrap(); + attempts += 1; + } + if attempts > 50 { + log::error!("EC didn't sync after wifi on...continuing and praying for the best."); + break; + } + } + } + xous::return_scalar(msg.sender, 1).ok(); } Some(Opcode::WlanOff) => { - info!("TODO: implement WlanOff"); com.txrx(ComState::WLAN_OFF.verb); } Some(Opcode::WlanRssi) => msg_blocking_scalar_unpack!(msg, _, _, _, _, { diff --git a/services/gam/src/lib.rs b/services/gam/src/lib.rs index f9990368a..c53417d7b 100644 --- a/services/gam/src/lib.rs +++ b/services/gam/src/lib.rs @@ -42,6 +42,7 @@ pub const STATUS_BAR_NAME: &'static str = "status"; pub const APP_NAME_SHELLCHAT: &'static str = "shellchat"; pub const APP_MENU_NAME: &'static str = "app menu"; pub const KBD_MENU_NAME: &'static str = "keyboard menu"; +pub const WIFI_MENU_NAME: &'static str = "WLAN menu"; /// UX context registry. Names here are authorized by the GAM to have Canvases. pub const EXPECTED_BOOT_CONTEXTS: &[&'static str] = &[ @@ -55,6 +56,7 @@ pub const EXPECTED_BOOT_CONTEXTS: &[&'static str] = &[ PDDB_MENU_NAME, APP_MENU_NAME, KBD_MENU_NAME, + WIFI_MENU_NAME, ]; #[derive(Copy, Clone, PartialEq, Eq, Debug)] diff --git a/services/net/src/connection_manager.rs b/services/net/src/connection_manager.rs index cb345e77c..372225767 100644 --- a/services/net/src/connection_manager.rs +++ b/services/net/src/connection_manager.rs @@ -24,6 +24,8 @@ pub(crate) enum ConnectionManagerOpcode { Run, Poll, Stop, + DisconnectAndStop, + WifiOnAndRun, SubscribeWifiStats, UnsubWifiStats, FetchSsidList, @@ -48,6 +50,7 @@ enum WifiState { InvalidAuth, Connected, Disconnected, + Off, Error } #[derive(Eq, PartialEq)] @@ -191,6 +194,10 @@ pub(crate) fn connection_manager(sid: xous::SID, activity_interval: Arc { + // wifi was manually forced "off", leave it off; presume connection manager is also stopped. + wifi_state = WifiState::Off; + } _ => { // should approximately be a "disconnected" state. match wifi_state { WifiState::Connected => { @@ -239,10 +246,14 @@ pub(crate) fn connection_manager(sid: xous::SID, activity_interval: Arc { log::info!("{:?}", source); - ssid_list.clear(); // clear the ssid list because a likely cause of disconnect is we've moved out of range - com.set_ssid_scanning(true).unwrap(); - scan_state = SsidScanState::Scanning; - wifi_state = WifiState::Disconnected; + if wifi_state != WifiState::Off { + ssid_list.clear(); // clear the ssid list because a likely cause of disconnect is we've moved out of range + com.set_ssid_scanning(true).unwrap(); + scan_state = SsidScanState::Scanning; + wifi_state = WifiState::Disconnected; + } else { + log::info!("Wifi intent is off. Ignoring disconnect interrupt."); + } }, ComIntSources::WlanSsidScanUpdate => { log::info!("{:?}", source); @@ -273,18 +284,22 @@ pub(crate) fn connection_manager(sid: xous::SID, activity_interval: Arc { log::info!("{:?}", source); @@ -392,6 +407,10 @@ pub(crate) fn connection_manager(sid: xous::SID, activity_interval: Arc { + // this state should not be reachable + log::warn!("Wifi state was manually forced off; did you remember to turn it on before setting the connection manager to RUN?"); + } } } } @@ -467,6 +486,54 @@ pub(crate) fn connection_manager(sid: xous::SID, activity_interval: Arc msg_scalar_unpack!(msg, _, _, _, _, { run.store(false, Ordering::SeqCst); }), + Some(ConnectionManagerOpcode::WifiOnAndRun) => msg_scalar_unpack!(msg, _, _, _, _, { + com.wlan_set_on().expect("couldn't turn on wifi"); + wifi_state = WifiState::Disconnected; + ssid_list.clear(); + com.set_ssid_scanning(true).unwrap(); + scan_state = SsidScanState::Scanning; + intervals_without_activity = 0; + scan_count = 0; + // this will force the UI to transition from 'WiFi Off' -> 'Not connected' + wifi_stats_cache = WlanStatus { + ssid: None, + link_state: com_rs_ref::LinkState::Disconnected, + ipv4: com::Ipv4Conf::default(), + }; + log::debug!("stats update: {:?}", wifi_stats_cache); + for &sub in status_subscribers.keys() { + let buf = Buffer::into_buf(com::WlanStatusIpc::from_status(wifi_stats_cache)).or(Err(xous::Error::InternalError)).unwrap(); + buf.send(sub, WifiStateCallback::Update.to_u32().unwrap()).or(Err(xous::Error::InternalError)).unwrap(); + } + if !run.swap(true, Ordering::SeqCst) { + if !pumping.load(Ordering::SeqCst) { // avoid having multiple pump messages being sent if a user tries to rapidly toggle the run/stop switch + send_message(run_cid, Message::new_scalar(PumpOp::Pump.to_usize().unwrap(), 0, 0, 0, 0)).expect("couldn't kick off next poll"); + } + } + }), + Some(ConnectionManagerOpcode::DisconnectAndStop) => { + run.store(false, Ordering::SeqCst); + wifi_state = WifiState::Off; + com.wlan_leave().expect("couldn't issue leave command"); + ssid_list.clear(); + intervals_without_activity = 0; + scan_count = 0; + com.set_ssid_scanning(false).unwrap(); + scan_state = SsidScanState::Idle; + + tt.sleep_ms(250).unwrap(); // give a moment to clean-up after leave before turning things off + com.wlan_set_off().expect("couldn't turn off wifi"); + wifi_stats_cache = WlanStatus { + ssid: None, + link_state: com_rs_ref::LinkState::ResetHold, + ipv4: com::Ipv4Conf::default(), + }; + log::debug!("stats update: {:?}", wifi_stats_cache); + for &sub in status_subscribers.keys() { + let buf = Buffer::into_buf(com::WlanStatusIpc::from_status(wifi_stats_cache)).or(Err(xous::Error::InternalError)).unwrap(); + buf.send(sub, WifiStateCallback::Update.to_u32().unwrap()).or(Err(xous::Error::InternalError)).unwrap(); + } + } Some(ConnectionManagerOpcode::EcReset) => msg_scalar_unpack!(msg, _, _, _, _, { // this opcode is used by other processes to inform us that the net link was reset by something other than us. // (e.g. an update) diff --git a/services/net/src/lib.rs b/services/net/src/lib.rs index 142722c29..1acda17c1 100644 --- a/services/net/src/lib.rs +++ b/services/net/src/lib.rs @@ -170,6 +170,16 @@ impl NetManager { Message::new_scalar(Opcode::ConnMgrStartStop.to_usize().unwrap(), 1, 0,0, 0) ).map(|_| ()) } + pub fn connection_manager_wifi_off_and_stop(&self) -> Result<(), xous::Error> { + send_message(self.netconn.conn(), + Message::new_scalar(Opcode::ConnMgrStartStop.to_usize().unwrap(), 2, 0,0, 0) + ).map(|_| ()) + } + pub fn connection_manager_wifi_on_and_run(&self) -> Result<(), xous::Error> { + send_message(self.netconn.conn(), + Message::new_scalar(Opcode::ConnMgrStartStop.to_usize().unwrap(), 3, 0,0, 0) + ).map(|_| ()) + } } impl Drop for NetManager { fn drop(&mut self) { diff --git a/services/net/src/main.rs b/services/net/src/main.rs index d5a557c77..b772691c5 100644 --- a/services/net/src/main.rs +++ b/services/net/src/main.rs @@ -2118,6 +2118,34 @@ fn main() -> ! { ), ) .expect("couldn't send stop message"); + } else if code == 2 { + send_message( + cm_cid, + Message::new_scalar( + connection_manager::ConnectionManagerOpcode::DisconnectAndStop + .to_usize() + .unwrap(), + 0, + 0, + 0, + 0, + ), + ) + .expect("couldn't send disconnect and stop message"); + } else if code == 3 { + send_message( + cm_cid, + Message::new_scalar( + connection_manager::ConnectionManagerOpcode::WifiOnAndRun + .to_usize() + .unwrap(), + 0, + 0, + 0, + 0, + ), + ) + .expect("couldn't send disconnect and stop message"); } else { log::error!("Got incorrect start/stop code: {}", code); } diff --git a/services/status/locales/i18n.json b/services/status/locales/i18n.json index a2ee1f21d..30c4b7c89 100644 --- a/services/status/locales/i18n.json +++ b/services/status/locales/i18n.json @@ -65,6 +65,12 @@ "zh": "没有连接", "en-tts": "Not connected" }, + "stats.wifi_off": { + "ja": "WiFiオフ", + "en": "WiFi off", + "zh": "WiFi关闭", + "en-tts": "WiFi off" + }, "stats.uptime": { "translator-note": "This needs to be a very short string, 2 chars max. Trailing space is necessary for English due to proportional font.", "ja": "稼働", @@ -138,6 +144,12 @@ "zh": "睡眠模式", "en-tts": "Sleep now" }, + "mainmenu.wifimenu": { + "en": "WiFi submenu...", + "en-tts": "WiFi submenu...", + "ja": "WiFiメニュー...", + "zh": "无线网络菜单..." + }, "mainmenu.backlighton": { "en": "Backlight on", "ja": "バックライト点灯", @@ -193,10 +205,10 @@ "en-tts": "Set timezone" }, "mainmenu.pddb": { - "en": "PDDB Submenu", + "en": "PDDB submenu...", "ja": "PDDBサブメニュー", "zh": "PDDB子菜单", - "en-tts": "PDDB submenu" + "en-tts": "PDDB submenu..." }, "mainmenu.app": { "en": "Switch to App...", @@ -246,6 +258,120 @@ "zh": "外壳聊天", "en-tts": "Shellchat" }, + "wlan.turnon": { + "en": "Turn WiFi on", + "ja": "Wi-Fiをオンにする", + "zh": "打开wifi", + "en-tts": "Turn WiFi on" + }, + "wlan.turnoff": { + "en": "Turn WiFi off", + "ja": "Wi-Fiをオフにする", + "zh": "关闭无线网络", + "en-tts": "Turn WiFi off" + }, + "wlan.manual_add": { + "en": "Manually add a network", + "ja": "ネットワークを手動で追加する", + "zh": "手动添加网络", + "en-tts": "Manually add a network" + }, + "wlan.scan": { + "en": "Scan for networks", + "ja": "ネットワークのスキャン", + "zh": "扫描网络", + "en-tts": "Scan for networks" + }, + "wlan.status": { + "en": "Network status", + "ja": "ネットワークの状態", + "zh": "网络状态", + "en-tts": "Network status" + }, + "wlan.delete": { + "en": "Delete network", + "ja": "ネットワークを削除", + "zh": "删除网络", + "en-tts": "Delete network" + }, + "wlan.list_known": { + "en": "List known networks", + "ja": "既知のネットワークを一覧表示する", + "zh": "列出已知网络", + "en-tts": "List known networks" + }, + "wlan.ssid_entry": { + "en": "Fill in SSID and password:", + "ja": "SSID とパスワードを入力します:", + "zh": "填写SSID和密码:", + "en-tts": "Fill in SSID and password:" + }, + "wlan.ssid": { + "en": "[ SSID ]", + "ja": "[ SSID ]", + "zh": "[ SSID ]", + "en-tts": "[ SSID ]" + }, + "wlan.password": { + "en": "[ password ]", + "ja": "[ パスワード ]", + "zh": "[ 密码 ]", + "en-tts": "[ password ]" + }, + "wlan.no_networks": { + "en": "No networks available.", + "ja": "利用可能なネットワークがありません。", + "zh": "没有可用的网络。", + "en-tts": "No networks available." + }, + "wlan.ssid_choose": { + "en": "Choose a network:", + "ja": "ネットワークを選択してください:", + "zh": "选择网络:", + "en-tts": "Choose a network:" + }, + "wlan.ssid_password": { + "en": "Please enter the password for {ssid}:", + "ja": "{ssid} のパスワードを入力してください:", + "zh": "请输入 {ssid} 的密码:", + "en-tts": "Please enter the password for {ssid}:" + }, + "wlan.password_empty": { + "en": "Password field cannot be empty", + "ja": "パスワードを空にすることはできません", + "zh": "密码不能为空", + "en-tts": "Password field cannot be empty" + }, + "wlan.no_known_networks": { + "en": "No known networks.", + "ja": "既知のネットワークはありません。", + "zh": "没有已知的网络。", + "en-tts": "No known networks." + }, + "wlan.known_networks": { + "en": "Known networks:\n", + "ja": "既知のネットワーク:\n", + "zh": "已知网络:\n", + "en-tts": "Known networks:\n" + }, + "wlan.cancel": { + "en": "❌ Cancel request ❌", + "ja": "❌ キャンセル ❌", + "zh": "❌ 取消请求 ❌", + "en-tts": "Cancel the requested action" + }, + "wlan.choose_delete": { + "en": "Choose a network to delete:", + "ja": "削除するネットワークを選択してください:", + "zh": "选择要删除的网络:", + "en-tts": "Choose a network to delete:" + }, + "wlan.error": { + "en": "Error", + "ja": "エラー", + "zh": "错误", + "en-tts": "Error" + }, "rtc.try_ntp": { "en": "Attempt to automatically set time with NTP?", "ja": "NTPで時間を設定しようとしますか?", diff --git a/services/status/src/main.rs b/services/status/src/main.rs index a92563b13..a0de16afe 100644 --- a/services/status/src/main.rs +++ b/services/status/src/main.rs @@ -11,6 +11,7 @@ use kbdmenu::*; mod app_autogen; mod time; mod ecup; +mod wifi; use com::api::*; use root_keys::api::{BackupOp, BackupKeyboardLayout}; @@ -87,6 +88,9 @@ pub(crate) enum StatusOpcode { BatteryDisconnect, /// for returning wifi stats WifiStats, + + /// Raise the wifi menu + WifiMenu, Quit, } @@ -633,6 +637,8 @@ fn wrapped_main() -> ! { } }); + wifi::start_background_thread(); + pump_run.store(true, Ordering::Relaxed); // start status thread updating loop { let msg = xous::receive_message(status_sid).unwrap(); @@ -727,11 +733,19 @@ fn wrapped_main() -> ! { ssid.rssi, ).unwrap(); } else { - write!( - &mut battstats_tv, - "{}", - t!("stats.disconnected", xous::LANG) - ).unwrap(); + if wifi_status.link_state == com_rs_ref::LinkState::ResetHold { + write!( + &mut battstats_tv, + "{}", + t!("stats.wifi_off", xous::LANG) + ).unwrap(); + } else { + write!( + &mut battstats_tv, + "{}", + t!("stats.disconnected", xous::LANG) + ).unwrap(); + } } } } @@ -753,6 +767,10 @@ fn wrapped_main() -> ! { }; wifi_status = WlanStatus::from_ipc(buffer.to_original::().unwrap()); }, + Some(StatusOpcode::WifiMenu) => { + ticktimer.sleep_ms(100).ok(); // yield for a moment to allow the previous menu to close + gam.raise_menu(gam::WIFI_MENU_NAME).unwrap(); + }, Some(StatusOpcode::Pump) => { let elapsed_time = ticktimer.elapsed_ms(); { // update the CPU load bar diff --git a/services/status/src/mainmenu.rs b/services/status/src/mainmenu.rs index 11ace8152..ab7d693ac 100644 --- a/services/status/src/mainmenu.rs +++ b/services/status/src/mainmenu.rs @@ -140,6 +140,15 @@ pub fn create_main_menu(keys: Arc>, menu_management_sid: xous::S action_payload: MenuPayload::Scalar([0, 0, 0, 0]), close_on_select: true, }); + + menuitems.push(MenuItem { + name: String::from_str(t!("mainmenu.wifimenu", xous::LANG)), + action_conn: Some(status_conn), + action_opcode: StatusOpcode::WifiMenu.to_u32().unwrap(), + action_payload: MenuPayload::Scalar([0, 0, 0, 0]), + close_on_select: true, + }); + if key_init { menuitems.push(MenuItem { name: String::from_str(t!("mainmenu.prep_backup", xous::LANG)), diff --git a/services/status/src/wifi.rs b/services/status/src/wifi.rs new file mode 100644 index 000000000..fa5d6fc0b --- /dev/null +++ b/services/status/src/wifi.rs @@ -0,0 +1,416 @@ +use core::fmt::Display; + +use num_traits::*; + +use std::io::Write; + +use locales::t; + +#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive, PartialEq, PartialOrd)] +enum WlanManOp { + ScanForNetworks = 3, + Status, + AddNetworkManually, + KnownNetworks, + DeleteNetwork, + TurnWlanOn, + TurnWlanOff = 9, +} + +impl Display for WlanManOp { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::TurnWlanOn => write!(f, "{}", t!("wlan.turnon", xous::LANG)), + Self::TurnWlanOff => write!(f, "{}", t!("wlan.turnoff", xous::LANG)), + Self::AddNetworkManually => write!(f, "{}", t!("wlan.manual_add", xous::LANG)), + Self::ScanForNetworks => write!(f, "{}", t!("wlan.scan", xous::LANG)), + Self::Status => write!(f, "{}", t!("wlan.status", xous::LANG)), + Self::DeleteNetwork => write!(f, "{}", t!("wlan.delete", xous::LANG)), + Self::KnownNetworks => write!(f, "{}", t!("wlan.list_known", xous::LANG)), + } + } +} + +#[derive(Debug)] +enum WLANError { + UnderlyingError(xous::Error), + PDDBWriteError(usize, usize), + PDDBIoError(std::io::Error), +} + +impl Display for WLANError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + WLANError::UnderlyingError(inner_error) => { + write!(f, "Underlying Xous error: {:?}", inner_error) + } + WLANError::PDDBWriteError(written, total) => { + write!( + f, + "PDDB write error, written {} of {} total", + written, total + ) + } + WLANError::PDDBIoError(err) => { + write!(f, "PDDB IO error, {}", err) + } + } + } +} + +impl From for WLANError { + fn from(v: xous::Error) -> Self { + Self::UnderlyingError(v) + } +} + +impl From for WLANError { + fn from(v: std::io::Error) -> Self { + Self::PDDBIoError(v) + } +} + +struct WLANMan { + com: com::Com, + netmgr: net::NetManager, + modals: modals::Modals, + pddb: pddb::Pddb, +} + +impl WLANMan { + fn new(xns: &xous_names::XousNames) -> Self { + Self { + com: com::Com::new(&xns).unwrap(), + netmgr: net::NetManager::new(), + modals: modals::Modals::new(&xns).unwrap(), + pddb: pddb::Pddb::new(), + } + } + + fn actions(&self) -> Vec { + use WlanManOp::*; + + vec![ + ScanForNetworks, + Status, + AddNetworkManually, + KnownNetworks, + DeleteNetwork, + TurnWlanOn, + TurnWlanOff, + ] + } + + fn set_wlan_state(&mut self, state: bool) -> Result<(), WLANError> { + match state { + true => { + // on + self.netmgr.connection_manager_wifi_on_and_run()?; + } + false => { + // off + self.netmgr.connection_manager_wifi_off_and_stop()?; + } + }; + + Ok(()) + } + + fn add_new_ssid(&mut self) -> Result<(), WLANError> { + let connection_data = self + .modals + .alert_builder(t!("wlan.ssid_entry", xous::LANG)) + .field( + Some("SSID".to_string()), + Some(|text| { + if text.as_str().is_empty() { + return Some(xous_ipc::String::from_str("SSID cannot be empty")); + } + + None + }), + ) + .field( + Some(t!("wlan.password", xous::LANG).to_string()), + Some(|text| { + if text.as_str().is_empty() { + return Some(xous_ipc::String::from_str("Password cannot be empty")); + } + + None + }), + ) + .build() + .unwrap(); + + let content = connection_data.content(); + + self.store_connection_info(content[0].as_str(), content[1].as_str()) + } + + fn store_connection_info(&mut self, ssid: &str, pass: &str) -> Result<(), WLANError> { + self.netmgr.connection_manager_stop().unwrap(); + match self.pddb.get( + net::AP_DICT_NAME, + &ssid, + None, + true, + true, + Some(com::api::WF200_PASS_MAX_LEN), + Some(|| {}), + ) { + Ok(mut entry) => { + match entry.write(&pass.as_bytes()) { + Ok(len) => { + if len != pass.len() { + Err(WLANError::PDDBWriteError(len, pass.len())) + } else { + // for now, we should always call flush at the end of a routine; perhaps in the + // future we'll have a timer that automatically syncs the pddb + entry.flush().expect("couldn't sync pddb cache"); + // restart the connection manager now that the key combo has been committed + self.netmgr.connection_manager_run().unwrap(); + Ok(()) + } + } + Err(e) => Err(WLANError::PDDBIoError(e)), + } + } + Err(e) => Err(WLANError::PDDBIoError(e)), + } + } + + fn scan_networks(&self) -> Result, WLANError> { + let scan_result = self.netmgr.wifi_get_ssid_list()?; + + Ok(scan_result + .iter() + .map(|ssid| ssid.name.to_string()) + .collect()) + } + + fn show_available_networks(&mut self) -> Result<(), WLANError> { + let networks = self.scan_networks()?; + let mut networks: Vec<&str> = networks.iter().map(|s| s.as_str()).collect(); + + if networks.is_empty() { + self.modals + .show_notification(t!("wlan.no_networks", xous::LANG), None) + .unwrap(); + return Ok(()); + } + + networks.push(t!("wlan.cancel", xous::LANG)); + + self.modals.add_list(networks).unwrap(); + + let ssid = self.modals.get_radiobutton(t!("wlan.ssid_choose", xous::LANG)).unwrap(); + + if ssid == t!("wlan.cancel", xous::LANG) { + return Ok(()) + } + + self.fill_password_for_ssid(&ssid) + } + + fn fill_password_for_ssid(&mut self, ssid: &str) -> Result<(), WLANError> { + let connection_data = self + .modals + .alert_builder(&t!("wlan.ssid_password", xous::LANG).replace("{ssid}", ssid)) + .field( + Some(t!("wlan.password", xous::LANG).to_string()), + Some(|text| { + if text.as_str().is_empty() { + return Some(xous_ipc::String::from_str(t!("wlan.password_empty", xous::LANG))); + } + + None + }), + ) + .build() + .unwrap(); + + let content = connection_data.content(); + + self.store_connection_info(ssid, content[0].as_str()) + } + + fn network_status(&mut self) -> Result<(), WLANError> { + let status = self.com.wlan_status()?; + let ssid = match status.ssid { + Some(s) => s.name.to_string(), + None => t!("stats.disconnected", xous::LANG).to_string(), + }; + + let ls = status.link_state; + let ip = &status.ipv4; + + // TODO: make a proper translation for this. But, I think for now, this is a fairly + // technical screen that we can leave in English. + let status_str = format!( + "Connection status: \n\n ▪ SSID: {}\n ▪ Link state: {:?}\n ▪ IP: {}\n ▪ Gateway: {}\n ▪ Subnet mask: {}\n ▪ DNS 1: {}\n ▪ DNS 2: {}\n ▪ DHCP state: {:?}", + ssid, + ls, + format_ip(ip.addr), + format_ip(ip.gtwy), + format_ip(ip.mask), + format_ip(ip.dns1), + format_ip(ip.dns2), + ip.dhcp, + ); + + self.modals.show_notification(&status_str, None).unwrap(); + Ok(()) + } + + fn known_networks(&self) -> Result<(), WLANError> { + let networks = match self.pddb.list_keys(net::AP_DICT_NAME, None) { + Ok(list) => list, + Err(_) => Vec::new(), + }; + + let mut networks_string = String::from(t!("wlan.no_known_networks", xous::LANG)); + + if networks.is_empty() { + self.modals + .show_notification(&networks_string, None) + .unwrap(); + return Ok(()); + } + + networks_string = String::from(t!("wlan.known_networks", xous::LANG)); + + networks_string += &networks + .iter() + .map(|s| format!(" ▪ {}", s)) + .collect::>() + .join("\n"); + + self.modals + .show_notification(&networks_string, None) + .unwrap(); + + Ok(()) + } + + fn delete_network(&mut self) -> Result<(), WLANError> { + let networks = match self.pddb.list_keys(net::AP_DICT_NAME, None) { + Ok(list) => list, + Err(_) => Vec::new(), + }; + + if networks.is_empty() { + self.modals + .show_notification(t!("wlan.no_known_networks", xous::LANG), None) + .unwrap(); + return Ok(()); + } + + let cancel_item = t!("wlan.cancel", xous::LANG); + self.modals + .add_list(networks.iter().map(|s| s.as_str()).collect()) + .unwrap(); + self.modals.add_list_item(cancel_item).unwrap(); + + let ssid_to_be_deleted = self + .modals + .get_radiobutton(t!("wlan.choose_delete", xous::LANG)) + .unwrap(); + + if ssid_to_be_deleted.eq(cancel_item) { + return Ok(()); + } + + self + .pddb + .delete_key(net::AP_DICT_NAME, &ssid_to_be_deleted, None).map_err(|e| WLANError::PDDBIoError(e))?; + + self.pddb.sync().map_err(|e| WLANError::PDDBIoError(e)) + } + + fn claim_menumatic_menu(&self, cid: xous::CID) { + let mut menus = self + .actions() + .iter() + .map(|action| gam::MenuItem { + name: xous_ipc::String::from_str(&action.to_string()), + action_conn: Some(cid), + action_opcode: action.to_u32().unwrap(), + action_payload: gam::MenuPayload::Scalar([0, 0, 0, 0]), + close_on_select: true, + }) + .collect::>(); + + menus.push(gam::MenuItem { + name: xous_ipc::String::from_str(t!("mainmenu.closemenu", xous::LANG)), + action_conn: None, + action_opcode: 0, + action_payload: gam::MenuPayload::Scalar([0, 0, 0, 0]), + close_on_select: true, + }); + + gam::menu_matic(menus, gam::WIFI_MENU_NAME, None); + } + + fn consume_menu_action(&mut self, action: WlanManOp) { + let resp = match action { + WlanManOp::TurnWlanOff => self.set_wlan_state(false), + WlanManOp::TurnWlanOn => self.set_wlan_state(true), + WlanManOp::AddNetworkManually => self.add_new_ssid(), + WlanManOp::ScanForNetworks => self.show_available_networks(), + WlanManOp::Status => self.network_status(), + WlanManOp::DeleteNetwork => self.delete_network(), + WlanManOp::KnownNetworks => self.known_networks(), + }; + + resp.unwrap_or_else(|error| self.show_error_modal(error)); + } + + fn show_error_modal(&self, e: WLANError) { + self.modals + .show_notification(format!("{}: {}", t!("wlan.error", xous::LANG), e).as_str(), None) + .unwrap() + } +} + +fn format_ip(src: [u8; 4]) -> String { + src.iter() + .map(|&id| id.to_string()) + .collect::>() + .join(".") +} + +pub fn start_background_thread() { + std::thread::spawn(|| run_menu_thread()); +} + +fn run_menu_thread() { + let xns = xous_names::XousNames::new().unwrap(); + + let sid = xous::create_server().unwrap(); + + let mut hello = WLANMan::new(&xns); + + let menu_conn = xous::connect(sid).unwrap(); + hello.claim_menumatic_menu(menu_conn); + + loop { + let msg = xous::receive_message(sid).unwrap(); + log::debug!("Got message: {:?}", msg); + + match FromPrimitive::from_usize(msg.body.id()) { + Some(other) => { + if other >= WlanManOp::ScanForNetworks && other <= WlanManOp::TurnWlanOff { + hello.consume_menu_action(other); + continue; + } else { + log::warn!("Received out of range opcode. Did WlanManOp get re-ordered, but the bounds check was not updated as well?"); + } + } + + _ => { + log::error!("Got unknown message"); + } + } + } +}