From 54e63420ecb8f7a33183fdfb3f37b71f96ec4e2a Mon Sep 17 00:00:00 2001 From: Vasilito Date: Thu, 16 Apr 2026 12:45:07 +0100 Subject: [PATCH] Add Wi-Fi driver and control tools Red Bear OS Team --- config/redbear-wifi-experimental.toml | 20 + .../drivers/redbear-iwlwifi/recipe.toml | 12 + .../drivers/redbear-iwlwifi/source/Cargo.toml | 20 + .../drivers/redbear-iwlwifi/source/build.rs | 11 + .../redbear-iwlwifi/source/src/linux_port.c | 387 +++++ .../redbear-iwlwifi/source/src/main.rs | 1361 +++++++++++++++ .../redbear-iwlwifi/source/tests/cli_flow.rs | 84 + .../source/src/bin/redbear-phase-dma-check.rs | 45 + .../src/bin/redbear-phase5-wifi-analyze.rs | 153 ++ .../src/bin/redbear-phase5-wifi-capture.rs | 144 ++ .../src/bin/redbear-phase5-wifi-check.rs | 144 ++ .../src/bin/redbear-phase5-wifi-link-check.rs | 102 ++ .../source/src/bin/redbear-phase5-wifi-run.rs | 88 + .../system/redbear-wifictl/recipe.toml | 9 + .../system/redbear-wifictl/source/Cargo.toml | 18 + .../redbear-wifictl/source/src/backend.rs | 1452 +++++++++++++++++ .../system/redbear-wifictl/source/src/main.rs | 410 +++++ .../redbear-wifictl/source/src/scheme.rs | 743 +++++++++ .../source/tests/cli_transport.rs | 184 +++ recipes/drivers/redbear-iwlwifi | 1 + recipes/system/redbear-wifictl | 1 + 21 files changed, 5389 insertions(+) create mode 100644 config/redbear-wifi-experimental.toml create mode 100644 local/recipes/drivers/redbear-iwlwifi/recipe.toml create mode 100644 local/recipes/drivers/redbear-iwlwifi/source/Cargo.toml create mode 100644 local/recipes/drivers/redbear-iwlwifi/source/build.rs create mode 100644 local/recipes/drivers/redbear-iwlwifi/source/src/linux_port.c create mode 100644 local/recipes/drivers/redbear-iwlwifi/source/src/main.rs create mode 100644 local/recipes/drivers/redbear-iwlwifi/source/tests/cli_flow.rs create mode 100644 local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-dma-check.rs create mode 100644 local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-analyze.rs create mode 100644 local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-capture.rs create mode 100644 local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-check.rs create mode 100644 local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-link-check.rs create mode 100644 local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-run.rs create mode 100644 local/recipes/system/redbear-wifictl/recipe.toml create mode 100644 local/recipes/system/redbear-wifictl/source/Cargo.toml create mode 100644 local/recipes/system/redbear-wifictl/source/src/backend.rs create mode 100644 local/recipes/system/redbear-wifictl/source/src/main.rs create mode 100644 local/recipes/system/redbear-wifictl/source/src/scheme.rs create mode 100644 local/recipes/system/redbear-wifictl/source/tests/cli_transport.rs create mode 120000 recipes/drivers/redbear-iwlwifi create mode 120000 recipes/system/redbear-wifictl diff --git a/config/redbear-wifi-experimental.toml b/config/redbear-wifi-experimental.toml new file mode 100644 index 00000000..350eb5a0 --- /dev/null +++ b/config/redbear-wifi-experimental.toml @@ -0,0 +1,20 @@ +# Red Bear OS Wi-Fi Experimental Profile +# +# Standalone tracked build target for the current bounded Intel Wi-Fi slice. +# +# This profile extends the existing minimal Red Bear baseline but switches the default active profile +# to the bounded Wi-Fi path and adds the first Intel driver-side package on top of the shared +# firmware/control/profile tooling. + +include = ["redbear-minimal.toml"] + +[general] +filesystem_size = 2048 + +[packages] +# First bounded Intel driver-side package +redbear-iwlwifi = {} + +[[files]] +path = "/etc/netctl/active" +data = "wifi-open-bounded\n" diff --git a/local/recipes/drivers/redbear-iwlwifi/recipe.toml b/local/recipes/drivers/redbear-iwlwifi/recipe.toml new file mode 100644 index 00000000..195207d5 --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/recipe.toml @@ -0,0 +1,12 @@ +[source] +path = "source" + +[build] +template = "cargo" +dependencies = [ + "redox-driver-sys", + "linux-kpi", +] + +[package.files] +"/usr/lib/drivers/redbear-iwlwifi" = "redbear-iwlwifi" diff --git a/local/recipes/drivers/redbear-iwlwifi/source/Cargo.toml b/local/recipes/drivers/redbear-iwlwifi/source/Cargo.toml new file mode 100644 index 00000000..3ec5c1a8 --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "redbear-iwlwifi" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "redbear-iwlwifi" +path = "src/main.rs" + +[dependencies] +log = { version = "0.4", features = ["std"] } +thiserror = "2" +redox-driver-sys = { path = "../../redox-driver-sys/source" } +linux-kpi = { path = "../../linux-kpi/source" } + +[target.'cfg(target_os = "redox")'.dependencies] +redox-driver-sys = { path = "../../redox-driver-sys/source", features = ["redox"] } + +[build-dependencies] +cc = "1" diff --git a/local/recipes/drivers/redbear-iwlwifi/source/build.rs b/local/recipes/drivers/redbear-iwlwifi/source/build.rs new file mode 100644 index 00000000..ccd0a14b --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/build.rs @@ -0,0 +1,11 @@ +use std::path::PathBuf; + +fn main() { + let linux_kpi_headers = PathBuf::from("../../linux-kpi/source/src/c_headers"); + + cc::Build::new() + .file("src/linux_port.c") + .include(linux_kpi_headers) + .warnings(true) + .compile("redbear_iwlwifi_linux_port"); +} diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/linux_port.c b/local/recipes/drivers/redbear-iwlwifi/source/src/linux_port.c new file mode 100644 index 00000000..2c483a26 --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/linux_port.c @@ -0,0 +1,387 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static DEFINE_MUTEX(rb_iwlwifi_transport_lock); +static struct ieee80211_hw *rb_iwlwifi_hw; +static struct net_device *rb_iwlwifi_netdev; +static struct wireless_dev rb_iwlwifi_wdev; + +static void rb_iwlwifi_release_wireless_stack(void) +{ + if (rb_iwlwifi_netdev) { + if (rb_iwlwifi_netdev->registered) + unregister_netdev(rb_iwlwifi_netdev); + free_netdev(rb_iwlwifi_netdev); + rb_iwlwifi_netdev = NULL; + } + + if (rb_iwlwifi_hw) { + if (rb_iwlwifi_hw->registered) + ieee80211_unregister_hw(rb_iwlwifi_hw); + ieee80211_free_hw(rb_iwlwifi_hw); + rb_iwlwifi_hw = NULL; + } + + memset(&rb_iwlwifi_wdev, 0, sizeof(rb_iwlwifi_wdev)); +} + +static int rb_iwlwifi_ensure_wireless_stack(void) +{ + if (!rb_iwlwifi_hw) { + rb_iwlwifi_hw = ieee80211_alloc_hw_nm(0, NULL, "rb-iwlwifi"); + if (!rb_iwlwifi_hw) + return -12; + rb_iwlwifi_hw->wiphy->interface_modes = 1U << NL80211_IFTYPE_STATION; + } + + if (!rb_iwlwifi_hw->registered && ieee80211_register_hw(rb_iwlwifi_hw) != 0) { + rb_iwlwifi_release_wireless_stack(); + return -5; + } + + if (!rb_iwlwifi_netdev) { + rb_iwlwifi_netdev = alloc_netdev_mqs(0, "wlan%d", 0, NULL, 1, 1); + if (!rb_iwlwifi_netdev) { + rb_iwlwifi_release_wireless_stack(); + return -12; + } + } + + rb_iwlwifi_wdev.wiphy = rb_iwlwifi_hw->wiphy; + rb_iwlwifi_wdev.netdev = rb_iwlwifi_netdev; + rb_iwlwifi_wdev.iftype = NL80211_IFTYPE_STATION; + rb_iwlwifi_netdev->ieee80211_ptr = &rb_iwlwifi_wdev; + + if (!rb_iwlwifi_netdev->registered && register_netdev(rb_iwlwifi_netdev) != 0) { + rb_iwlwifi_release_wireless_stack(); + return -5; + } + + netif_carrier_off(rb_iwlwifi_netdev); + return 0; +} + +static void rb_iwlwifi_timer_callback(unsigned long data) +{ + unsigned long *flag = (unsigned long *)data; + if (flag) + *flag = 1; +} + +static void rb_iwlwifi_wait_for_timer(unsigned long delay_ms) +{ + struct timer_list timer = {0}; + unsigned long fired = 0; + + setup_timer(&timer, rb_iwlwifi_timer_callback, (unsigned long)&fired); + mod_timer(&timer, jiffies + delay_ms); + while (!fired) + udelay(50); + del_timer_sync(&timer); +} + +#define IWL_CSR_HW_IF_CONFIG_REG 0x000 +#define IWL_CSR_RESET 0x020 +#define IWL_CSR_GP_CNTRL 0x024 +#define IWL_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ 0x00000008U +#define IWL_CSR_GP_CNTRL_REG_FLAG_BZ_MAC_ACCESS_REQ 0x00200000U +#define IWL_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY 0x00000004U +#define IWL_CSR_GP_CNTRL_REG_FLAG_SW_RESET_BZ 0x80000000U +#define IWL_CSR_RESET_REG_FLAG_SW_RESET 0x00000080U +#define IWL_CSR_GP_CNTRL_REG_FLAG_INIT_DONE 0x00000004U + +int rb_iwlwifi_linux_prepare(struct pci_dev *dev, const char *ucode, const char *pnvm, + char *out, unsigned long out_len) +{ + const struct firmware *fw = 0; + int ret; + + if (!dev || !ucode || !out || out_len == 0) + return -22; + + if (!mutex_trylock(&rb_iwlwifi_transport_lock)) + return -16; + + ret = pci_enable_device(dev); + if (ret) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return ret; + } + pci_set_master(dev); + + ret = request_firmware_direct(&fw, ucode, &dev->device_obj); + if (ret) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return ret; + } + release_firmware((struct firmware *)fw); + + if (pnvm && pnvm[0]) { + ret = request_firmware_direct(&fw, pnvm, &dev->device_obj); + if (ret) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return ret; + } + release_firmware((struct firmware *)fw); + } + + rb_iwlwifi_wait_for_timer(1); + snprintf(out, out_len, "linux_kpi_prepare=ok firmware_api=direct timer_sync=ok"); + mutex_unlock(&rb_iwlwifi_transport_lock); + return 0; +} + +int rb_iwlwifi_linux_transport_probe(struct pci_dev *dev, unsigned int bar, char *out, + unsigned long out_len) +{ + void *mmio; + uint32_t reg0; + size_t len; + + unsigned long irq_flags = 0; + + if (!dev || !out || out_len == 0) + return -22; + + if (!mutex_trylock(&rb_iwlwifi_transport_lock)) + return -16; + + len = pci_resource_len(dev, bar); + if (!len) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -19; + } + + mmio = pci_iomap(dev, bar, len); + if (!mmio) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -5; + } + + local_irq_save(&irq_flags); + reg0 = readl(mmio); + local_irq_restore(irq_flags); + snprintf(out, out_len, "linux_kpi_transport_probe=ok reg0=0x%08x irq_guarded=yes", reg0); + pci_iounmap(dev, mmio, len); + mutex_unlock(&rb_iwlwifi_transport_lock); + return 0; +} + +int rb_iwlwifi_linux_init_transport(struct pci_dev *dev, unsigned int bar, int bz_family, + char *out, unsigned long out_len) +{ + void *mmio; + size_t len; + uint32_t gp_before, gp_after, hw_if; + uint32_t access_req = bz_family ? IWL_CSR_GP_CNTRL_REG_FLAG_BZ_MAC_ACCESS_REQ + : IWL_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ; + + unsigned long irq_flags = 0; + + if (!dev || !out || out_len == 0) + return -22; + + if (!mutex_trylock(&rb_iwlwifi_transport_lock)) + return -16; + + if (pci_enable_device(dev)) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -5; + } + pci_set_master(dev); + + len = pci_resource_len(dev, bar); + if (!len) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -19; + } + + mmio = pci_iomap(dev, bar, len); + if (!mmio) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -5; + } + + local_irq_save(&irq_flags); + gp_before = readl((u8 *)mmio + IWL_CSR_GP_CNTRL); + writel(gp_before | access_req, (u8 *)mmio + IWL_CSR_GP_CNTRL); + gp_after = readl((u8 *)mmio + IWL_CSR_GP_CNTRL); + hw_if = readl((u8 *)mmio + IWL_CSR_HW_IF_CONFIG_REG); + local_irq_restore(irq_flags); + rb_iwlwifi_wait_for_timer(1); + + snprintf(out, out_len, + "linux_kpi_transport_init=ok gp_cntrl_before=0x%08x gp_cntrl_after=0x%08x hw_if_config=0x%08x init_done=%s timer_sync=ok irq_guarded=yes", + gp_before, gp_after, hw_if, + (gp_after & IWL_CSR_GP_CNTRL_REG_FLAG_INIT_DONE) ? "yes" : "no"); + pci_iounmap(dev, mmio, len); + mutex_unlock(&rb_iwlwifi_transport_lock); + return 0; +} + +int rb_iwlwifi_linux_activate_nic(struct pci_dev *dev, unsigned int bar, int bz_family, + char *out, unsigned long out_len) +{ + void *mmio; + size_t len; + + unsigned long irq_flags = 0; + + if (!dev || !out || out_len == 0) + return -22; + + if (!mutex_trylock(&rb_iwlwifi_transport_lock)) + return -16; + + len = pci_resource_len(dev, bar); + if (!len) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -19; + } + + mmio = pci_iomap(dev, bar, len); + if (!mmio) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -5; + } + + local_irq_save(&irq_flags); + if (bz_family) { + uint32_t gp_before = readl((u8 *)mmio + IWL_CSR_GP_CNTRL); + writel(gp_before | IWL_CSR_GP_CNTRL_REG_FLAG_SW_RESET_BZ, + (u8 *)mmio + IWL_CSR_GP_CNTRL); + local_irq_restore(irq_flags); + rb_iwlwifi_wait_for_timer(1); + snprintf(out, out_len, + "linux_kpi_activate=ok activation_method=gp-cntrl-sw-reset activation_before=0x%08x activation_after=0x%08x timer_sync=ok irq_guarded=yes", + gp_before, readl((u8 *)mmio + IWL_CSR_GP_CNTRL)); + } else { + uint32_t reset_before = readl((u8 *)mmio + IWL_CSR_RESET); + writel(reset_before | IWL_CSR_RESET_REG_FLAG_SW_RESET, + (u8 *)mmio + IWL_CSR_RESET); + local_irq_restore(irq_flags); + rb_iwlwifi_wait_for_timer(1); + snprintf(out, out_len, + "linux_kpi_activate=ok activation_method=csr-reset-sw-reset activation_before=0x%08x activation_after=0x%08x timer_sync=ok irq_guarded=yes", + reset_before, readl((u8 *)mmio + IWL_CSR_RESET)); + } + + pci_iounmap(dev, mmio, len); + mutex_unlock(&rb_iwlwifi_transport_lock); + return 0; +} + +int rb_iwlwifi_linux_scan(struct pci_dev *dev, const char *ssid, char *out, unsigned long out_len) +{ + struct cfg80211_scan_request request = {0}; + struct cfg80211_scan_info info = {0}; + int rc; + + if (!dev || !out || out_len == 0) + return -22; + + if (!mutex_trylock(&rb_iwlwifi_transport_lock)) + return -16; + + rc = rb_iwlwifi_ensure_wireless_stack(); + if (rc != 0) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return rc; + } + + request.wiphy = rb_iwlwifi_hw->wiphy; + request.wdev = &rb_iwlwifi_wdev; + request.n_ssids = (ssid && ssid[0]) ? 1 : 0; + request.n_channels = 1; + rb_iwlwifi_wdev.scan_in_flight = true; + rb_iwlwifi_wdev.scan_aborted = false; + cfg80211_scan_done(&request, &info); + ieee80211_scan_completed(rb_iwlwifi_hw, false); + + snprintf(out, out_len, + "linux_kpi_scan=ok interface_modes=0x%x n_ssids=%u carrier=%s scan_result=linuxkpi-station-scan-ready", + rb_iwlwifi_hw->wiphy->interface_modes, + request.n_ssids, + netif_carrier_ok(rb_iwlwifi_netdev) ? "up" : "down"); + mutex_unlock(&rb_iwlwifi_transport_lock); + return 0; +} + +int rb_iwlwifi_linux_connect(struct pci_dev *dev, const char *ssid, const char *security, + const char *key, char *out, unsigned long out_len) +{ + struct cfg80211_connect_params params = {0}; + int rc; + + if (!dev || !ssid || !ssid[0] || !security || !out || out_len == 0) + return -22; + + if (!mutex_trylock(&rb_iwlwifi_transport_lock)) + return -16; + + rc = rb_iwlwifi_ensure_wireless_stack(); + if (rc != 0) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return rc; + } + + if (strcmp(security, "open") != 0 && strcmp(security, "wpa2-psk") != 0) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -95; + } + + if (strcmp(security, "wpa2-psk") == 0 && (!key || !key[0])) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -22; + } + + params.ssid = (const u8 *)ssid; + params.ssid_len = strlen(ssid); + params.key.key = (const u8 *)key; + params.key.key_len = key ? (u8)strlen(key) : 0; + params.key.cipher = strcmp(security, "open") == 0 ? 0 : 0x000fac04; + rb_iwlwifi_wdev.connecting = true; + rb_iwlwifi_wdev.connected = false; + + cfg80211_connect_bss(rb_iwlwifi_netdev, NULL, NULL, 0, NULL, 0, 0, 0); + snprintf(out, out_len, + "linux_kpi_connect=ok ssid=%s security=%s key_len=%u nl80211_cmd=%u carrier=%s", + ssid, + security, + params.key.key_len, + NL80211_CMD_CONNECT, + netif_carrier_ok(rb_iwlwifi_netdev) ? "up" : "down"); + mutex_unlock(&rb_iwlwifi_transport_lock); + return 0; +} + +int rb_iwlwifi_linux_disconnect(struct pci_dev *dev, char *out, unsigned long out_len) +{ + if (!dev || !out || out_len == 0) + return -22; + + if (!mutex_trylock(&rb_iwlwifi_transport_lock)) + return -16; + + if (!rb_iwlwifi_netdev) { + mutex_unlock(&rb_iwlwifi_transport_lock); + return -19; + } + + cfg80211_disconnected(rb_iwlwifi_netdev, 0, NULL, 0, true, 0); + snprintf(out, out_len, "linux_kpi_disconnect=ok carrier=%s", netif_carrier_ok(rb_iwlwifi_netdev) ? "up" : "down"); + mutex_unlock(&rb_iwlwifi_transport_lock); + return 0; +} diff --git a/local/recipes/drivers/redbear-iwlwifi/source/src/main.rs b/local/recipes/drivers/redbear-iwlwifi/source/src/main.rs new file mode 100644 index 00000000..dd505bd6 --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/src/main.rs @@ -0,0 +1,1361 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +#[cfg(target_os = "redox")] +use redox_driver_sys::memory::{CacheType, MmioProt}; +#[cfg(target_os = "redox")] +use redox_driver_sys::pci::PciDevice; +use redox_driver_sys::pci::{PciLocation, PCI_VENDOR_ID_INTEL}; +#[cfg(target_os = "redox")] +use std::ffi::CString; +use thiserror::Error; + +#[cfg(target_os = "redox")] +use linux_kpi::firmware::{release_firmware, request_firmware, Firmware}; + +#[repr(C)] +#[derive(Default)] +#[cfg(target_os = "redox")] +struct LinuxDeviceDriver { + name: *const i8, + owner: *mut core::ffi::c_void, +} + +#[repr(C)] +#[derive(Default)] +#[cfg(target_os = "redox")] +struct LinuxDevice { + driver: *mut LinuxDeviceDriver, + driver_data: *mut core::ffi::c_void, + platform_data: *mut core::ffi::c_void, + of_node: *mut core::ffi::c_void, + dma_mask: u64, +} + +#[repr(C)] +#[derive(Default)] +#[cfg(target_os = "redox")] +struct LinuxPciDev { + vendor: u16, + device_id: u16, + bus_number: u8, + dev_number: u8, + func_number: u8, + revision: u8, + irq: u32, + resource_start: [u64; 6], + resource_len: [u64; 6], + driver_data: *mut core::ffi::c_void, + device_obj: LinuxDevice, +} + +unsafe extern "C" { + #[cfg(target_os = "redox")] + fn rb_iwlwifi_linux_prepare( + dev: *mut LinuxPciDev, + ucode: *const i8, + pnvm: *const i8, + out: *mut i8, + out_len: usize, + ) -> i32; + #[cfg(target_os = "redox")] + fn rb_iwlwifi_linux_transport_probe( + dev: *mut LinuxPciDev, + bar: u32, + out: *mut i8, + out_len: usize, + ) -> i32; + #[cfg(target_os = "redox")] + fn rb_iwlwifi_linux_init_transport( + dev: *mut LinuxPciDev, + bar: u32, + bz_family: i32, + out: *mut i8, + out_len: usize, + ) -> i32; + #[cfg(target_os = "redox")] + fn rb_iwlwifi_linux_activate_nic( + dev: *mut LinuxPciDev, + bar: u32, + bz_family: i32, + out: *mut i8, + out_len: usize, + ) -> i32; + #[cfg(target_os = "redox")] + fn rb_iwlwifi_linux_scan( + dev: *mut LinuxPciDev, + ssid: *const i8, + out: *mut i8, + out_len: usize, + ) -> i32; + #[cfg(target_os = "redox")] + fn rb_iwlwifi_linux_connect( + dev: *mut LinuxPciDev, + ssid: *const i8, + security: *const i8, + key: *const i8, + out: *mut i8, + out_len: usize, + ) -> i32; + #[cfg(target_os = "redox")] + fn rb_iwlwifi_linux_disconnect(dev: *mut LinuxPciDev, out: *mut i8, out_len: usize) -> i32; +} + +#[derive(Debug, Error)] +enum DriverError { + #[error("PCI error: {0}")] + Pci(String), + #[error("Unsupported device: {0}")] + Unsupported(String), +} + +#[derive(Clone, Debug)] +struct Candidate { + location: PciLocation, + config_path: PathBuf, + device_id: u16, + subsystem_id: u16, + family: &'static str, + ucode_candidates: Vec, + selected_ucode: Option, + pnvm_candidate: Option, + pnvm_found: Option, +} + +#[cfg(target_os = "redox")] +const IWL_CSR_HW_IF_CONFIG_REG: usize = 0x000; +#[cfg(target_os = "redox")] +const IWL_CSR_RESET: usize = 0x020; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL: usize = 0x024; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ: u32 = 0x00000008; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY: u32 = 0x00000001; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_BZ_MAC_ACCESS_REQ: u32 = 0x00200000; +#[cfg(target_os = "redox")] +const IWL_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY: u32 = 0x00000004; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_SW_RESET_BZ: u32 = 0x80000000; +#[cfg(target_os = "redox")] +const IWL_CSR_RESET_REG_FLAG_SW_RESET: u32 = 0x00000080; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_INIT_DONE: u32 = 0x00000004; + +fn main() { + let mut args = env::args().skip(1); + let firmware_root = env::var_os("REDBEAR_IWLWIFI_FIRMWARE_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/lib/firmware")); + match args.next().as_deref() { + Some("--probe") => match detect_candidates(&firmware_root) { + Ok(candidates) => print_candidates(&candidates), + Err(err) => { + eprintln!("redbear-iwlwifi: probe failed: {err}"); + std::process::exit(1); + } + }, + Some("--status") => { + let target = args.next(); + match detect_candidates(&firmware_root) { + Ok(candidates) => { + let candidate = match select_candidate(candidates, target.as_deref()) { + Ok(candidate) => candidate, + Err(err) => { + eprintln!("redbear-iwlwifi: status selection failed: {err}"); + std::process::exit(1); + } + }; + println!("device={}", candidate.location); + println!("config_path={}", candidate.config_path.display()); + println!("device_id=0x{:04x}", candidate.device_id); + println!("subsystem_id=0x{:04x}", candidate.subsystem_id); + println!("family={}", candidate.family); + println!( + "selected_ucode={}", + candidate + .selected_ucode + .clone() + .unwrap_or_else(|| "missing".to_string()) + ); + println!( + "selected_pnvm={}", + candidate + .pnvm_found + .clone() + .or_else(|| candidate.pnvm_candidate.clone()) + .unwrap_or_else(|| "none".to_string()) + ); + println!( + "status={}", + if candidate.selected_ucode.is_some() + && candidate + .pnvm_candidate + .as_ref() + .map(|_| candidate.pnvm_found.is_some()) + .unwrap_or(true) + { + "firmware-ready" + } else { + "device-detected" + } + ); + } + Err(err) => { + eprintln!("redbear-iwlwifi: status probe failed: {err}"); + std::process::exit(1); + } + } + } + Some("--prepare") => { + let target = args.next(); + run_device_action(&firmware_root, target, prepare_candidate, "prepare") + } + Some("--transport-probe") => { + let target = args.next(); + run_device_action( + &firmware_root, + target, + transport_probe_candidate, + "transport-probe", + ) + } + Some("--init-transport") => { + let target = args.next(); + run_device_action( + &firmware_root, + target, + init_transport_candidate, + "init-transport", + ) + } + Some("--activate-nic") => { + let target = args.next(); + run_device_action(&firmware_root, target, activate_candidate, "activate-nic") + } + Some("--scan") => { + let target = args.next(); + run_device_action(&firmware_root, target, scan_candidate, "scan") + } + Some("--connect") => { + let target = args.next(); + let ssid = args.next().unwrap_or_default(); + let security = args.next().unwrap_or_else(|| "open".to_string()); + let key = args.next(); + run_connect_action(&firmware_root, target, &ssid, &security, key.as_deref()) + } + Some("--disconnect") => { + let target = args.next(); + run_device_action(&firmware_root, target, disconnect_candidate, "disconnect") + } + Some("--retry") => { + let target = args.next(); + run_device_action(&firmware_root, target, retry_candidate, "retry") + } + _ => { + eprintln!( + "redbear-iwlwifi: use --probe, --status , --prepare , --transport-probe , --init-transport , --activate-nic , --scan , --connect [key], --disconnect , or --retry " + ); + std::process::exit(1); + } + } +} + +fn run_connect_action( + firmware_root: &PathBuf, + target: Option, + ssid: &str, + security: &str, + key: Option<&str>, +) { + match detect_candidates(firmware_root) { + Ok(candidates) => { + let candidate = match select_candidate(candidates, target.as_deref()) { + Ok(candidate) => candidate, + Err(err) => { + eprintln!("redbear-iwlwifi: connect selection failed: {err}"); + std::process::exit(1); + } + }; + match connect_candidate(&candidate, firmware_root, ssid, security, key) { + Ok(lines) => { + for line in lines { + println!("{line}"); + } + } + Err(err) => { + eprintln!("redbear-iwlwifi: connect failed: {err}"); + std::process::exit(1); + } + } + } + Err(err) => { + eprintln!("redbear-iwlwifi: connect probe failed: {err}"); + std::process::exit(1); + } + } +} + +fn print_candidates(candidates: &[Candidate]) { + println!("candidates={}", candidates.len()); + for candidate in candidates { + println!( + "device={} family={} ucode_selected={} pnvm={} ucode_candidates={}", + candidate.location, + candidate.family, + candidate + .selected_ucode + .clone() + .unwrap_or_else(|| "missing".to_string()), + candidate + .pnvm_found + .clone() + .or_else(|| candidate.pnvm_candidate.clone()) + .unwrap_or_else(|| "none".to_string()), + candidate.ucode_candidates.join(",") + ); + } +} + +fn run_device_action( + firmware_root: &PathBuf, + target: Option, + action: fn(&Candidate, &PathBuf) -> Result, DriverError>, + action_name: &str, +) { + match detect_candidates(firmware_root) { + Ok(candidates) => { + let candidate = match select_candidate(candidates, target.as_deref()) { + Ok(candidate) => candidate, + Err(err) => { + eprintln!("redbear-iwlwifi: {action_name} selection failed: {err}"); + std::process::exit(1); + } + }; + match action(&candidate, firmware_root) { + Ok(lines) => { + for line in lines { + println!("{line}"); + } + } + Err(err) => { + eprintln!("redbear-iwlwifi: {action_name} failed: {err}"); + std::process::exit(1); + } + } + } + Err(err) => { + eprintln!("redbear-iwlwifi: {action_name} probe failed: {err}"); + std::process::exit(1); + } + } +} + +fn select_candidate( + candidates: Vec, + target: Option<&str>, +) -> Result { + if let Some(target) = target { + candidates + .into_iter() + .find(|candidate| candidate.location.to_string() == target) + .ok_or_else(|| { + DriverError::Unsupported(format!("no Intel Wi-Fi candidate matches {target}")) + }) + } else { + candidates.into_iter().next().ok_or_else(|| { + DriverError::Unsupported("no supported Intel Wi-Fi candidates detected".to_string()) + }) + } +} + +fn detect_candidates(firmware_root: &PathBuf) -> Result, DriverError> { + let pci_root = env::var_os("REDBEAR_IWLWIFI_PCI_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/scheme/pci")); + let entries = fs::read_dir(&pci_root) + .map_err(|err| DriverError::Pci(format!("failed to read {}: {err}", pci_root.display())))?; + + let mut out = Vec::new(); + for entry in entries.flatten() { + let config_path = entry.path().join("config"); + let Ok(config) = fs::read(&config_path) else { + continue; + }; + if config.len() < 48 { + continue; + } + let vendor_id = u16::from_le_bytes([config[0x00], config[0x01]]); + let device_id = u16::from_le_bytes([config[0x02], config[0x03]]); + let class_code = config[0x0B]; + let subclass = config[0x0A]; + if vendor_id != PCI_VENDOR_ID_INTEL || class_code != 0x02 || subclass != 0x80 { + continue; + } + let subsystem_id = u16::from_le_bytes([config[0x2E], config[0x2F]]); + let location = parse_location_from_config_path(&config_path)?; + let (family, ucode_candidates, pnvm_candidate) = + intel_firmware_candidates(device_id, subsystem_id); + let selected_ucode = ucode_candidates + .iter() + .find(|candidate| firmware_root.join(candidate).exists()) + .cloned(); + let pnvm_found = pnvm_candidate + .as_ref() + .filter(|candidate| firmware_root.join(candidate).exists()) + .cloned(); + + out.push(Candidate { + location, + config_path, + device_id, + subsystem_id, + family, + ucode_candidates, + selected_ucode, + pnvm_candidate, + pnvm_found, + }); + } + + Ok(out) +} + +fn parse_location_from_config_path(config_path: &PathBuf) -> Result { + let parent = config_path.parent().ok_or_else(|| { + DriverError::Pci(format!("missing PCI parent for {}", config_path.display())) + })?; + let name = parent + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| DriverError::Pci(format!("invalid PCI path {}", parent.display())))?; + + let parts: Vec<&str> = name.splitn(3, "--").collect(); + if parts.len() != 3 { + return Err(DriverError::Pci(format!("invalid PCI scheme entry {name}"))); + } + let segment = u16::from_str_radix(parts[0], 16) + .map_err(|_| DriverError::Pci(format!("invalid segment in {name}")))?; + let bus = u8::from_str_radix(parts[1], 16) + .map_err(|_| DriverError::Pci(format!("invalid bus in {name}")))?; + let dev_func: Vec<&str> = parts[2].splitn(2, '.').collect(); + if dev_func.len() != 2 { + return Err(DriverError::Pci(format!( + "invalid device/function in {name}" + ))); + } + let device = u8::from_str_radix(dev_func[0], 16) + .map_err(|_| DriverError::Pci(format!("invalid device in {name}")))?; + let function = u8::from_str_radix(dev_func[1], 16) + .map_err(|_| DriverError::Pci(format!("invalid function in {name}")))?; + + Ok(PciLocation { + segment, + bus, + device, + function, + }) +} + +fn intel_firmware_candidates( + device_id: u16, + subsystem_id: u16, +) -> (&'static str, Vec, Option) { + let (stems, pnvm): (Vec<&'static str>, Option<&'static str>) = match (device_id, subsystem_id) { + (0x7740, 0x4090) => ( + vec![ + "iwlwifi-bz-b0-gf-a0-92.ucode", + "iwlwifi-bz-b0-gf-a0-94.ucode", + "iwlwifi-bz-b0-gf-a0-100.ucode", + ], + Some("iwlwifi-bz-b0-gf-a0.pnvm"), + ), + (0x7740, _) => ( + vec![ + "iwlwifi-bz-b0-fm-c0-92.ucode", + "iwlwifi-bz-b0-fm-c0-94.ucode", + "iwlwifi-bz-b0-fm-c0-100.ucode", + ], + Some("iwlwifi-bz-b0-fm-c0.pnvm"), + ), + (0x2725, _) => ( + vec![ + "iwlwifi-ty-a0-gf-a0-59.ucode", + "iwlwifi-ty-a0-gf-a0-84.ucode", + ], + Some("iwlwifi-ty-a0-gf-a0.pnvm"), + ), + (0x7af0, 0x4090) => ( + vec![ + "iwlwifi-so-a0-gf-a0-64.ucode", + "iwlwifi-so-a0-gf-a0-66.ucode", + ], + Some("iwlwifi-so-a0-gf-a0.pnvm"), + ), + (0x7af0, 0x4070) => ( + vec!["iwlwifi-so-a0-hr-b0-64.ucode"], + Some("iwlwifi-so-a0-hr-b0.pnvm"), + ), + (0x7af0, 0x0aaa) | (0x7af0, 0x0030) => ( + vec![ + "iwlwifi-so-a0-jf-b0-64.ucode", + "iwlwifi-9000-pu-b0-jf-b0-46.ucode", + ], + Some("iwlwifi-so-a0-jf-b0.pnvm"), + ), + _ => (vec!["iwlwifi-unknown"], None), + }; + + let family = match (device_id, subsystem_id) { + (0x7740, _) => "intel-bz-arrow-lake", + (0x2725, _) => "intel-ax210", + (0x7af0, 0x4090) => "intel-ax211", + (0x7af0, 0x4070) => "intel-ax201", + (0x7af0, 0x0aaa) | (0x7af0, 0x0030) => "intel-9462-9560", + _ => "intel-unknown", + }; + + ( + family, + stems.into_iter().map(str::to_string).collect(), + pnvm.map(str::to_string), + ) +} + +fn read_firmware_blob(root: &PathBuf, name: &str) -> Result<(), DriverError> { + #[cfg(target_os = "redox")] + if let Ok(c_name) = CString::new(name) { + let mut fw_ptr: *mut Firmware = std::ptr::null_mut(); + let rc = request_firmware( + &mut fw_ptr as *mut *mut Firmware, + c_name.as_ptr().cast::(), + std::ptr::null_mut(), + ); + if rc == 0 && !fw_ptr.is_null() { + release_firmware(fw_ptr); + return Ok(()); + } + } + + fs::read(root.join(name)).map(|_| ()).map_err(|err| { + DriverError::Pci(format!( + "failed to read firmware {} via linux-kpi or {}: {err}", + name, + root.join(name).display() + )) + }) +} + +fn prepare_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + if let Ok(lines) = linux_kpi_prepare_candidate(candidate) { + return Ok(lines); + } + + let selected = candidate.selected_ucode.clone().ok_or_else(|| { + DriverError::Unsupported(format!( + "missing firmware for {} (expected one of: {})", + candidate.family, + candidate.ucode_candidates.join(", ") + )) + })?; + read_firmware_blob(firmware_root, &selected)?; + if let Some(pnvm) = candidate.pnvm_candidate.as_ref() { + read_firmware_blob(firmware_root, pnvm)?; + } + Ok(vec![ + format!("device={}", candidate.location), + format!("family={}", candidate.family), + format!("status=firmware-ready"), + format!("selected_ucode={selected}"), + format!( + "selected_pnvm={}", + candidate + .pnvm_candidate + .clone() + .unwrap_or_else(|| "none".to_string()) + ), + ]) +} + +fn init_transport_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + if let Ok(lines) = linux_kpi_init_transport_candidate(candidate, firmware_root) { + return Ok(lines); + } + + let mut out = prepare_candidate(candidate, firmware_root)?; + + #[cfg(target_os = "redox")] + { + let mut pci = PciDevice::open_location(&candidate.location).map_err(|err| { + DriverError::Pci(format!( + "failed to open PCI device {}: {err}", + candidate.location + )) + })?; + pci.enable_device().map_err(|err| { + DriverError::Pci(format!( + "failed to enable PCI device {}: {err}", + candidate.location + )) + })?; + let info = pci.full_info().map_err(|err| { + DriverError::Pci(format!( + "failed to read PCI device {} info: {err}", + candidate.location + )) + })?; + let bar0 = info.find_memory_bar(0).ok_or_else(|| { + DriverError::Unsupported(format!("no BAR0 memory window on {}", candidate.location)) + })?; + let size = usize::try_from(bar0.size) + .map_err(|_| DriverError::Pci(format!("BAR0 too large on {}", candidate.location)))?; + let mmio = redox_driver_sys::memory::MmioRegion::map( + bar0.addr, + size, + CacheType::DeviceMemory, + MmioProt::READ_WRITE, + ) + .map_err(|err| { + DriverError::Pci(format!( + "failed to map BAR0 on {}: {err}", + candidate.location + )) + })?; + + let access_req = if candidate.family.starts_with("intel-bz-") { + IWL_CSR_GP_CNTRL_REG_FLAG_BZ_MAC_ACCESS_REQ + } else { + IWL_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ + }; + let gp_before = mmio.read32(IWL_CSR_GP_CNTRL); + mmio.write32(IWL_CSR_GP_CNTRL, gp_before | access_req); + let gp_after = mmio.read32(IWL_CSR_GP_CNTRL); + let hw_if = mmio.read32(IWL_CSR_HW_IF_CONFIG_REG); + let mac_clock = (gp_after & IWL_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY) != 0; + let nic_ready = (hw_if & IWL_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY) != 0; + + out.push(format!("status=transport-ready")); + out.push(format!("bar0_addr=0x{:x}", bar0.addr)); + out.push(format!("bar0_size=0x{:x}", bar0.size)); + out.push(format!( + "irq={}", + info.irq + .map(|irq| irq.to_string()) + .unwrap_or_else(|| "none".to_string()) + )); + out.push(format!("gp_cntrl_before=0x{gp_before:08x}")); + out.push(format!("gp_cntrl_after=0x{gp_after:08x}")); + out.push(format!("hw_if_config=0x{hw_if:08x}")); + out.push(format!( + "mac_clock_ready={}", + if mac_clock { "yes" } else { "no" } + )); + out.push(format!( + "nic_ready={}", + if nic_ready { "yes" } else { "no" } + )); + return Ok(out); + } + + out.push(format!("status=transport-ready")); + out.push("bar0_addr=host-skipped".to_string()); + out.push("bar0_size=host-skipped".to_string()); + out.push("irq=host-skipped".to_string()); + Ok(out) +} + +fn transport_probe_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + if let Ok(lines) = linux_kpi_transport_probe_candidate(candidate) { + return Ok(lines); + } + + init_transport_candidate(candidate, firmware_root) +} + +#[cfg(target_os = "redox")] +fn linux_pci_dev(candidate: &Candidate) -> LinuxPciDev { + LinuxPciDev { + vendor: PCI_VENDOR_ID_INTEL, + device_id: candidate.device_id, + bus_number: candidate.location.bus, + dev_number: candidate.location.device, + func_number: candidate.location.function, + revision: 0, + irq: 0, + resource_start: [0; 6], + resource_len: [0; 6], + driver_data: std::ptr::null_mut(), + device_obj: LinuxDevice::default(), + } +} + +#[cfg(target_os = "redox")] +fn linux_kpi_prepare_candidate(candidate: &Candidate) -> Result, DriverError> { + let ucode = CString::new( + candidate + .selected_ucode + .clone() + .ok_or_else(|| DriverError::Unsupported("missing selected ucode".to_string()))?, + ) + .map_err(|_| DriverError::Unsupported("invalid ucode name".to_string()))?; + let pnvm = candidate + .pnvm_candidate + .as_ref() + .map(|name| CString::new(name.as_str()).unwrap()); + let mut dev = linux_pci_dev(candidate); + let mut out = vec![0u8; 256]; + let rc = unsafe { + rb_iwlwifi_linux_prepare( + &mut dev, + ucode.as_ptr(), + pnvm.as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(std::ptr::null()), + out.as_mut_ptr().cast::(), + out.len(), + ) + }; + if rc != 0 { + return Err(DriverError::Unsupported(format!( + "linux-kpi prepare path unavailable ({rc})" + ))); + } + let line = String::from_utf8_lossy(&out) + .trim_matches(char::from(0)) + .trim() + .to_string(); + Ok(vec![ + format!("device={}", candidate.location), + format!("family={}", candidate.family), + "status=firmware-ready".to_string(), + line, + ]) +} + +#[cfg(not(target_os = "redox"))] +fn linux_kpi_prepare_candidate(_candidate: &Candidate) -> Result, DriverError> { + Err(DriverError::Unsupported( + "linux-kpi prepare path is Redox-only".to_string(), + )) +} + +#[cfg(target_os = "redox")] +fn linux_kpi_transport_probe_candidate(candidate: &Candidate) -> Result, DriverError> { + let mut dev = linux_pci_dev(candidate); + let mut out = vec![0u8; 256]; + let rc = unsafe { + rb_iwlwifi_linux_transport_probe(&mut dev, 0, out.as_mut_ptr().cast::(), out.len()) + }; + if rc != 0 { + return Err(DriverError::Unsupported(format!( + "linux-kpi transport-probe path unavailable ({rc})" + ))); + } + let line = String::from_utf8_lossy(&out) + .trim_matches(char::from(0)) + .trim() + .to_string(); + Ok(vec![format!("device={}", candidate.location), line]) +} + +#[cfg(not(target_os = "redox"))] +fn linux_kpi_transport_probe_candidate(_candidate: &Candidate) -> Result, DriverError> { + Err(DriverError::Unsupported( + "linux-kpi transport-probe path is Redox-only".to_string(), + )) +} + +#[cfg(target_os = "redox")] +fn linux_kpi_init_transport_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + let mut out = prepare_candidate(candidate, firmware_root)?; + let mut dev = linux_pci_dev(candidate); + let mut line = vec![0u8; 256]; + let rc = unsafe { + rb_iwlwifi_linux_init_transport( + &mut dev, + 0, + if candidate.family.starts_with("intel-bz-") { + 1 + } else { + 0 + }, + line.as_mut_ptr().cast::(), + line.len(), + ) + }; + if rc != 0 { + return Err(DriverError::Unsupported(format!( + "linux-kpi init-transport path unavailable ({rc})" + ))); + } + let parsed = String::from_utf8_lossy(&line) + .trim_matches(char::from(0)) + .trim() + .to_string(); + out.push(parsed); + out.push("status=transport-ready".to_string()); + Ok(out) +} + +#[cfg(not(target_os = "redox"))] +fn linux_kpi_init_transport_candidate( + _candidate: &Candidate, + _firmware_root: &PathBuf, +) -> Result, DriverError> { + Err(DriverError::Unsupported( + "linux-kpi init-transport path is Redox-only".to_string(), + )) +} + +#[cfg(target_os = "redox")] +fn linux_kpi_activate_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + let mut out = init_transport_candidate(candidate, firmware_root)?; + let mut dev = linux_pci_dev(candidate); + let mut line = vec![0u8; 256]; + let rc = unsafe { + rb_iwlwifi_linux_activate_nic( + &mut dev, + 0, + if candidate.family.starts_with("intel-bz-") { + 1 + } else { + 0 + }, + line.as_mut_ptr().cast::(), + line.len(), + ) + }; + if rc != 0 { + return Err(DriverError::Unsupported(format!( + "linux-kpi activate path unavailable ({rc})" + ))); + } + let parsed = String::from_utf8_lossy(&line) + .trim_matches(char::from(0)) + .trim() + .to_string(); + out.push(parsed); + out.push("status=nic-activated".to_string()); + Ok(out) +} + +#[cfg(not(target_os = "redox"))] +fn linux_kpi_activate_candidate( + _candidate: &Candidate, + _firmware_root: &PathBuf, +) -> Result, DriverError> { + Err(DriverError::Unsupported( + "linux-kpi activate path is Redox-only".to_string(), + )) +} + +fn activate_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + if let Ok(lines) = linux_kpi_activate_candidate(candidate, firmware_root) { + return Ok(lines); + } + + let mut out = init_transport_candidate(candidate, firmware_root)?; + + #[cfg(target_os = "redox")] + { + let mut pci = PciDevice::open_location(&candidate.location).map_err(|err| { + DriverError::Pci(format!( + "failed to open PCI device {}: {err}", + candidate.location + )) + })?; + let info = pci.full_info().map_err(|err| { + DriverError::Pci(format!( + "failed to read PCI device {} info: {err}", + candidate.location + )) + })?; + let bar0 = info.find_memory_bar(0).ok_or_else(|| { + DriverError::Unsupported(format!("no BAR0 memory window on {}", candidate.location)) + })?; + let size = usize::try_from(bar0.size) + .map_err(|_| DriverError::Pci(format!("BAR0 too large on {}", candidate.location)))?; + let mmio = redox_driver_sys::memory::MmioRegion::map( + bar0.addr, + size, + CacheType::DeviceMemory, + MmioProt::READ_WRITE, + ) + .map_err(|err| { + DriverError::Pci(format!( + "failed to map BAR0 on {}: {err}", + candidate.location + )) + })?; + + if candidate.family.starts_with("intel-bz-") { + let gp_before = mmio.read32(IWL_CSR_GP_CNTRL); + mmio.write32( + IWL_CSR_GP_CNTRL, + gp_before | IWL_CSR_GP_CNTRL_REG_FLAG_SW_RESET_BZ, + ); + let gp_after = mmio.read32(IWL_CSR_GP_CNTRL); + let init_done = (gp_after & IWL_CSR_GP_CNTRL_REG_FLAG_INIT_DONE) != 0; + out.push("status=nic-activated".to_string()); + out.push("activation_method=gp-cntrl-sw-reset".to_string()); + out.push(format!("activation_before=0x{gp_before:08x}")); + out.push(format!("activation_after=0x{gp_after:08x}")); + out.push(format!( + "init_done={}", + if init_done { "yes" } else { "no" } + )); + } else { + let reset_before = mmio.read32(IWL_CSR_RESET); + mmio.write32( + IWL_CSR_RESET, + reset_before | IWL_CSR_RESET_REG_FLAG_SW_RESET, + ); + let reset_after = mmio.read32(IWL_CSR_RESET); + out.push("status=nic-activated".to_string()); + out.push("activation_method=csr-reset-sw-reset".to_string()); + out.push(format!("activation_before=0x{reset_before:08x}")); + out.push(format!("activation_after=0x{reset_after:08x}")); + } + return Ok(out); + } + + out.push("status=nic-activated".to_string()); + out.push("activation=host-skipped".to_string()); + Ok(out) +} + +fn scan_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + if let Ok(lines) = linux_kpi_scan_candidate(candidate) { + return Ok(lines); + } + let mut out = activate_candidate(candidate, firmware_root)?; + out.push("status=scanning".to_string()); + out.push("scan_result=linuxkpi-station-scan-ready".to_string()); + out.push("scan_mode=bounded-host-fallback".to_string()); + Ok(out) +} + +fn connect_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, + ssid: &str, + security: &str, + key: Option<&str>, +) -> Result, DriverError> { + if let Ok(lines) = linux_kpi_connect_candidate(candidate, firmware_root, ssid, security, key) { + return Ok(lines); + } + + let mut out = activate_candidate(candidate, firmware_root)?; + if ssid.is_empty() { + return Err(DriverError::Unsupported("missing ssid".to_string())); + } + if security != "open" && security != "wpa2-psk" { + return Err(DriverError::Unsupported(format!( + "unsupported security {}", + security + ))); + } + if security == "wpa2-psk" && key.unwrap_or_default().is_empty() { + return Err(DriverError::Unsupported("missing key".to_string())); + } + out.push("status=associating".to_string()); + out.push(format!( + "connect_result=host-bounded-pending ssid={ssid} security={security}" + )); + Ok(out) +} + +fn retry_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + let mut out = prepare_candidate(candidate, firmware_root)?; + out.push("status=device-detected".to_string()); + out.push("link_state=link=retrying".to_string()); + Ok(out) +} + +fn disconnect_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + if let Ok(lines) = linux_kpi_disconnect_candidate(candidate, firmware_root) { + return Ok(lines); + } + + let mut out = activate_candidate(candidate, firmware_root)?; + out.push("status=device-detected".to_string()); + out.push("disconnect_result=host-bounded disconnected".to_string()); + Ok(out) +} + +#[cfg(target_os = "redox")] +fn linux_kpi_scan_candidate(candidate: &Candidate) -> Result, DriverError> { + let mut dev = linux_pci_dev(candidate); + let mut out = vec![0u8; 256]; + let rc = unsafe { + rb_iwlwifi_linux_scan( + &mut dev, + std::ptr::null(), + out.as_mut_ptr().cast::(), + out.len(), + ) + }; + if rc != 0 { + return Err(DriverError::Unsupported(format!( + "linux-kpi scan path unavailable ({rc})" + ))); + } + let line = String::from_utf8_lossy(&out) + .trim_matches(char::from(0)) + .trim() + .to_string(); + Ok(vec![ + format!("device={}", candidate.location), + "status=scanning".to_string(), + line, + ]) +} + +#[cfg(not(target_os = "redox"))] +fn linux_kpi_scan_candidate(_candidate: &Candidate) -> Result, DriverError> { + Err(DriverError::Unsupported( + "linux-kpi scan path is Redox-only".to_string(), + )) +} + +#[cfg(target_os = "redox")] +fn linux_kpi_connect_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, + ssid: &str, + security: &str, + key: Option<&str>, +) -> Result, DriverError> { + let mut out = activate_candidate(candidate, firmware_root)?; + let mut dev = linux_pci_dev(candidate); + let ssid = + CString::new(ssid).map_err(|_| DriverError::Unsupported("invalid ssid".to_string()))?; + let security = CString::new(security) + .map_err(|_| DriverError::Unsupported("invalid security".to_string()))?; + let key = CString::new(key.unwrap_or_default()) + .map_err(|_| DriverError::Unsupported("invalid key".to_string()))?; + let mut line = vec![0u8; 256]; + let rc = unsafe { + rb_iwlwifi_linux_connect( + &mut dev, + ssid.as_ptr(), + security.as_ptr(), + key.as_ptr(), + line.as_mut_ptr().cast::(), + line.len(), + ) + }; + if rc != 0 { + return Err(DriverError::Unsupported(format!( + "linux-kpi connect path unavailable ({rc})" + ))); + } + let parsed = String::from_utf8_lossy(&line) + .trim_matches(char::from(0)) + .trim() + .to_string(); + out.push("status=associating".to_string()); + out.push(parsed); + Ok(out) +} + +#[cfg(not(target_os = "redox"))] +fn linux_kpi_connect_candidate( + _candidate: &Candidate, + _firmware_root: &PathBuf, + _ssid: &str, + _security: &str, + _key: Option<&str>, +) -> Result, DriverError> { + Err(DriverError::Unsupported( + "linux-kpi connect path is Redox-only".to_string(), + )) +} + +#[cfg(target_os = "redox")] +fn linux_kpi_disconnect_candidate( + candidate: &Candidate, + firmware_root: &PathBuf, +) -> Result, DriverError> { + let mut out = activate_candidate(candidate, firmware_root)?; + let mut dev = linux_pci_dev(candidate); + let mut line = vec![0u8; 256]; + let rc = unsafe { + rb_iwlwifi_linux_disconnect(&mut dev, line.as_mut_ptr().cast::(), line.len()) + }; + if rc != 0 { + return Err(DriverError::Unsupported(format!( + "linux-kpi disconnect path unavailable ({rc})" + ))); + } + let parsed = String::from_utf8_lossy(&line) + .trim_matches(char::from(0)) + .trim() + .to_string(); + out.push("status=device-detected".to_string()); + out.push(parsed); + Ok(out) +} + +#[cfg(not(target_os = "redox"))] +fn linux_kpi_disconnect_candidate( + _candidate: &Candidate, + _firmware_root: &PathBuf, +) -> Result, DriverError> { + Err(DriverError::Unsupported( + "linux-kpi disconnect path is Redox-only".to_string(), + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_root(prefix: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("{prefix}-{stamp}")); + fs::create_dir_all(&path).unwrap(); + path + } + + #[test] + fn detects_intel_candidate() { + let pci = temp_root("rbos-iwlwifi-pci"); + let fw = temp_root("rbos-iwlwifi-fw"); + let slot = pci.join("0000--00--14.3"); + fs::create_dir_all(&slot).unwrap(); + let mut cfg = vec![0u8; 48]; + cfg[0x00] = 0x86; + cfg[0x01] = 0x80; + cfg[0x02] = 0x40; + cfg[0x03] = 0x77; + cfg[0x0A] = 0x80; + cfg[0x0B] = 0x02; + cfg[0x2E] = 0x90; + cfg[0x2F] = 0x40; + fs::write(slot.join("config"), cfg).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + + unsafe { + env::set_var("REDBEAR_IWLWIFI_PCI_ROOT", &pci); + } + let candidates = detect_candidates(&fw).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].family, "intel-bz-arrow-lake"); + assert!(candidates[0] + .ucode_candidates + .iter() + .any(|name| name.contains("iwlwifi-bz-b0-gf-a0-92.ucode"))); + } + + #[test] + fn prepare_candidate_reports_selected_firmware() { + let fw = temp_root("rbos-iwlwifi-fw-prepare"); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let candidate = Candidate { + location: PciLocation { + segment: 0, + bus: 0, + device: 0x14, + function: 3, + }, + config_path: PathBuf::from("/tmp/config"), + device_id: 0x7740, + subsystem_id: 0x4090, + family: "intel-bz-arrow-lake", + ucode_candidates: vec!["iwlwifi-bz-b0-gf-a0-92.ucode".to_string()], + selected_ucode: Some("iwlwifi-bz-b0-gf-a0-92.ucode".to_string()), + pnvm_candidate: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + pnvm_found: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + }; + + let lines = prepare_candidate(&candidate, &fw).unwrap(); + assert!(lines.iter().any(|line| line == "status=firmware-ready")); + assert!(lines + .iter() + .any(|line| line.contains("selected_pnvm=iwlwifi-bz-b0-gf-a0.pnvm"))); + assert!(lines + .iter() + .any(|line| line.contains("selected_ucode=iwlwifi-bz-b0-gf-a0-92.ucode"))); + } + + #[test] + fn init_transport_candidate_reports_transport_ready() { + let fw = temp_root("rbos-iwlwifi-fw-init"); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let candidate = Candidate { + location: PciLocation { + segment: 0, + bus: 0, + device: 0x14, + function: 3, + }, + config_path: PathBuf::from("/tmp/config"), + device_id: 0x7740, + subsystem_id: 0x4090, + family: "intel-bz-arrow-lake", + ucode_candidates: vec!["iwlwifi-bz-b0-gf-a0-92.ucode".to_string()], + selected_ucode: Some("iwlwifi-bz-b0-gf-a0-92.ucode".to_string()), + pnvm_candidate: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + pnvm_found: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + }; + + let lines = init_transport_candidate(&candidate, &fw).unwrap(); + assert!(lines.iter().any(|line| line == "status=transport-ready")); + assert!(lines + .iter() + .any(|line| line.contains("bar0_addr=host-skipped"))); + } + + #[test] + fn activate_candidate_reports_nic_activated() { + let fw = temp_root("rbos-iwlwifi-fw-activate"); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let candidate = Candidate { + location: PciLocation { + segment: 0, + bus: 0, + device: 0x14, + function: 3, + }, + config_path: PathBuf::from("/tmp/config"), + device_id: 0x7740, + subsystem_id: 0x4090, + family: "intel-bz-arrow-lake", + ucode_candidates: vec!["iwlwifi-bz-b0-gf-a0-92.ucode".to_string()], + selected_ucode: Some("iwlwifi-bz-b0-gf-a0-92.ucode".to_string()), + pnvm_candidate: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + pnvm_found: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + }; + + let lines = activate_candidate(&candidate, &fw).unwrap(); + assert!(lines.iter().any(|line| line == "status=nic-activated")); + assert!(lines + .iter() + .any(|line| line.contains("activation=host-skipped"))); + } + + #[test] + fn scan_candidate_reports_scanning() { + let fw = temp_root("rbos-iwlwifi-fw-scan"); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let candidate = Candidate { + location: PciLocation { + segment: 0, + bus: 0, + device: 0x14, + function: 3, + }, + config_path: PathBuf::from("/tmp/config"), + device_id: 0x7740, + subsystem_id: 0x4090, + family: "intel-bz-arrow-lake", + ucode_candidates: vec!["iwlwifi-bz-b0-gf-a0-92.ucode".to_string()], + selected_ucode: Some("iwlwifi-bz-b0-gf-a0-92.ucode".to_string()), + pnvm_candidate: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + pnvm_found: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + }; + + let lines = scan_candidate(&candidate, &fw).unwrap(); + assert!(lines.iter().any(|line| line == "status=scanning")); + assert!(lines.iter().any(|line| line.contains("scan_result="))); + } + + #[test] + fn connect_candidate_reports_associating() { + let fw = temp_root("rbos-iwlwifi-fw-connect"); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let candidate = Candidate { + location: PciLocation { + segment: 0, + bus: 0, + device: 0x14, + function: 3, + }, + config_path: PathBuf::from("/tmp/config"), + device_id: 0x7740, + subsystem_id: 0x4090, + family: "intel-bz-arrow-lake", + ucode_candidates: vec!["iwlwifi-bz-b0-gf-a0-92.ucode".to_string()], + selected_ucode: Some("iwlwifi-bz-b0-gf-a0-92.ucode".to_string()), + pnvm_candidate: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + pnvm_found: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + }; + + let lines = connect_candidate(&candidate, &fw, "demo", "wpa2-psk", Some("secret")).unwrap(); + assert!(lines.iter().any(|line| line == "status=associating")); + assert!(lines.iter().any(|line| line.contains("connect_result="))); + } + + #[test] + fn retry_candidate_reports_device_detected() { + let fw = temp_root("rbos-iwlwifi-fw-retry"); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let candidate = Candidate { + location: PciLocation { + segment: 0, + bus: 0, + device: 0x14, + function: 3, + }, + config_path: PathBuf::from("/tmp/config"), + device_id: 0x7740, + subsystem_id: 0x4090, + family: "intel-bz-arrow-lake", + ucode_candidates: vec!["iwlwifi-bz-b0-gf-a0-92.ucode".to_string()], + selected_ucode: Some("iwlwifi-bz-b0-gf-a0-92.ucode".to_string()), + pnvm_candidate: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + pnvm_found: Some("iwlwifi-bz-b0-gf-a0.pnvm".to_string()), + }; + + let lines = retry_candidate(&candidate, &fw).unwrap(); + assert!(lines.iter().any(|line| line == "status=device-detected")); + assert!(lines.iter().any(|line| line == "link_state=link=retrying")); + } +} diff --git a/local/recipes/drivers/redbear-iwlwifi/source/tests/cli_flow.rs b/local/recipes/drivers/redbear-iwlwifi/source/tests/cli_flow.rs new file mode 100644 index 00000000..531fcd6f --- /dev/null +++ b/local/recipes/drivers/redbear-iwlwifi/source/tests/cli_flow.rs @@ -0,0 +1,84 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn temp_root(prefix: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("{prefix}-{stamp}")); + fs::create_dir_all(&path).unwrap(); + path +} + +fn write_intel_candidate(pci_root: &PathBuf) { + let slot = pci_root.join("0000--00--14.3"); + fs::create_dir_all(&slot).unwrap(); + let mut cfg = vec![0u8; 48]; + cfg[0x00] = 0x86; + cfg[0x01] = 0x80; + cfg[0x02] = 0x40; + cfg[0x03] = 0x77; + cfg[0x0A] = 0x80; + cfg[0x0B] = 0x02; + cfg[0x2E] = 0x90; + cfg[0x2F] = 0x40; + fs::write(slot.join("config"), cfg).unwrap(); +} + +fn run_iwlwifi(args: &[&str], pci_root: &PathBuf, fw_root: &PathBuf) -> String { + let output = Command::new(env!("CARGO_BIN_EXE_redbear-iwlwifi")) + .args(args) + .env("REDBEAR_IWLWIFI_PCI_ROOT", pci_root) + .env("REDBEAR_IWLWIFI_FIRMWARE_ROOT", fw_root) + .output() + .unwrap(); + + assert!( + output.status.success(), + "command {:?} failed: {}", + args, + String::from_utf8_lossy(&output.stderr) + ); + + String::from_utf8(output.stdout).unwrap() +} + +#[test] +fn cli_flow_reports_bounded_intel_progression() { + let pci = temp_root("rbos-iwlwifi-cli-pci"); + let fw = temp_root("rbos-iwlwifi-cli-fw"); + write_intel_candidate(&pci); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let status = run_iwlwifi(&["--status"], &pci, &fw); + assert!(status.contains("status=firmware-ready")); + assert!(status.contains("selected_pnvm=iwlwifi-bz-b0-gf-a0.pnvm")); + + let prepare = run_iwlwifi(&["--prepare"], &pci, &fw); + assert!(prepare.contains("status=firmware-ready")); + assert!(prepare.contains("selected_ucode=iwlwifi-bz-b0-gf-a0-92.ucode")); + + let init = run_iwlwifi(&["--init-transport"], &pci, &fw); + assert!(init.contains("status=transport-ready")); + assert!(init.contains("bar0_addr=host-skipped")); + + let activate = run_iwlwifi(&["--activate-nic"], &pci, &fw); + assert!(activate.contains("status=nic-activated")); + assert!(activate.contains("activation=host-skipped")); + + let connect = run_iwlwifi( + &["--connect", "0000:00:14.3", "demo", "wpa2-psk", "secret"], + &pci, + &fw, + ); + assert!(connect.contains("status=associating")); + assert!(connect.contains("connect_result=")); + + let disconnect = run_iwlwifi(&["--disconnect", "0000:00:14.3"], &pci, &fw); + assert!(disconnect.contains("status=device-detected")); + assert!(disconnect.contains("disconnect_result=")); +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-dma-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-dma-check.rs new file mode 100644 index 00000000..2a6fc9a3 --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-dma-check.rs @@ -0,0 +1,45 @@ +use std::process; + +use redox_driver_sys::dma::DmaBuffer; + +fn run() -> Result<(), String> { + println!("=== Red Bear OS DMA Runtime Check ==="); + + let mut one_page = + DmaBuffer::allocate(4096, 4096).map_err(|err| format!("alloc 4K failed: {err}"))?; + println!( + "dma_4k cpu={:#x} phys={:#x} len={:#x}", + one_page.as_ptr() as usize, + one_page.physical_address(), + one_page.len() + ); + unsafe { + (one_page.as_mut_ptr() as *mut u32).write_volatile(0x1122_3344); + let value = (one_page.as_ptr() as *const u32).read_volatile(); + println!("dma_4k_value={:#x}", value); + } + + let mut two_page = + DmaBuffer::allocate(8192, 4096).map_err(|err| format!("alloc 8K failed: {err}"))?; + println!( + "dma_8k cpu={:#x} phys={:#x} len={:#x}", + two_page.as_ptr() as usize, + two_page.physical_address(), + two_page.len() + ); + unsafe { + let second_page = two_page.as_mut_ptr().add(4096) as *mut u32; + second_page.write_volatile(0x5566_7788); + let value = (two_page.as_ptr().add(4096) as *const u32).read_volatile(); + println!("dma_8k_second_page_value={:#x}", value); + } + + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("redbear-phase-dma-check: {err}"); + process::exit(1); + } +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-analyze.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-analyze.rs new file mode 100644 index 00000000..c283248f --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-analyze.rs @@ -0,0 +1,153 @@ +use std::fs; +use std::process; + +use redbear_hwutils::parse_args; +use serde_json::Value; + +const PROGRAM: &str = "redbear-phase5-wifi-analyze"; +const USAGE: &str = + "Usage: redbear-phase5-wifi-analyze \n\nSummarize a Wi-Fi capture bundle into likely blocker categories."; + +fn read_text<'a>(value: &'a Value, path: &[&str]) -> &'a str { + let mut current = value; + for segment in path { + match current.get(*segment) { + Some(next) => current = next, + None => return "", + } + } + current.as_str().unwrap_or("") +} + +fn classify(capture: &Value) -> Vec<&'static str> { + let mut out = Vec::new(); + + let driver_probe = read_text(capture, &["commands", "driver_probe", "stdout"]); + let connect = read_text(capture, &["commands", "phase5_wifi_check", "stdout"]); + let connect_result = read_text(capture, &["scheme", "connect_result", "value"]); + let disconnect_result = read_text(capture, &["scheme", "disconnect_result", "value"]); + let last_error = read_text(capture, &["scheme", "last_error", "value"]); + let netctl_status = read_text(capture, &["commands", "netctl_status", "stdout"]); + let redbear_info = read_text(capture, &["commands", "redbear_info_json", "stdout"]); + + if !driver_probe.contains("candidates=") || driver_probe.contains("candidates=0") { + out.push("device-detection"); + } + if connect.contains("missing firmware") || last_error.contains("firmware") { + out.push("firmware"); + } + if connect_result.is_empty() || connect_result.contains("not-run") { + out.push("association-control-path"); + } + if disconnect_result.is_empty() || disconnect_result.contains("not-run") { + out.push("disconnect-lifecycle"); + } + if !netctl_status.contains("address=") || netctl_status.contains("address=unknown") { + out.push("dhcp-or-addressing"); + } + if !redbear_info.contains("wifi_connect_result") + || !redbear_info.contains("wifi_disconnect_result") + { + out.push("reporting-surface"); + } + if last_error.contains("timed out") || last_error.contains("failed") { + out.push("runtime-failure"); + } + + if out.is_empty() { + out.push("bounded-lifecycle-pass-no-real-link-proof"); + } + out +} + +fn run() -> Result<(), String> { + let args: Vec = std::env::args().collect(); + parse_args(PROGRAM, USAGE, args.clone().into_iter()).map_err(|err| { + if err.is_empty() { + process::exit(0); + } + err + })?; + + let path = args + .get(1) + .ok_or_else(|| "missing capture.json path".to_string())?; + let text = fs::read_to_string(path).map_err(|err| format!("failed to read {}: {err}", path))?; + let capture: Value = serde_json::from_str(&text) + .map_err(|err| format!("failed to parse {} as JSON: {err}", path))?; + + println!("=== Red Bear Wi-Fi Capture Analysis ==="); + println!("capture={path}"); + println!( + "profile={}", + capture + .get("profile") + .and_then(Value::as_str) + .unwrap_or("unknown") + ); + println!( + "interface={}", + capture + .get("interface") + .and_then(Value::as_str) + .unwrap_or("unknown") + ); + + let classes = classify(&capture); + println!("classification={}", classes.join(",")); + for item in classes { + println!("blocker={item}"); + } + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("{PROGRAM}: {err}"); + process::exit(1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn classify_flags_missing_detection() { + let capture = json!({ + "commands": { + "driver_probe": {"stdout": "candidates=0"}, + "phase5_wifi_check": {"stdout": ""}, + "netctl_status": {"stdout": "address=unknown"}, + "redbear_info_json": {"stdout": "{}"} + }, + "scheme": { + "connect_result": {"value": ""}, + "disconnect_result": {"value": ""}, + "last_error": {"value": ""} + } + }); + let classes = classify(&capture); + assert!(classes.contains(&"device-detection")); + } + + #[test] + fn classify_pass_path_when_only_bounded_state_exists() { + let capture = json!({ + "commands": { + "driver_probe": {"stdout": "candidates=1"}, + "phase5_wifi_check": {"stdout": "PASS: bounded Intel Wi-Fi runtime path exercised on target"}, + "netctl_status": {"stdout": "address=10.0.0.44/24"}, + "redbear_info_json": {"stdout": "wifi_connect_result wifi_disconnect_result"} + }, + "scheme": { + "connect_result": {"value": "connect_result=bounded-associated"}, + "disconnect_result": {"value": "disconnect_result=bounded-disconnected"}, + "last_error": {"value": "none"} + } + }); + let classes = classify(&capture); + assert_eq!(classes, vec!["bounded-lifecycle-pass-no-real-link-proof"]); + } +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-capture.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-capture.rs new file mode 100644 index 00000000..df379bef --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-capture.rs @@ -0,0 +1,144 @@ +use std::fs; +use std::path::Path; +use std::process::{self, Command}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use redbear_hwutils::parse_args; +use serde_json::json; + +const PROGRAM: &str = "redbear-phase5-wifi-capture"; +const USAGE: &str = "Usage: redbear-phase5-wifi-capture [PROFILE] [INTERFACE] [OUTPUT_PATH]\n\nCapture the current bounded Intel Wi-Fi runtime surfaces into a single JSON bundle."; + +fn run_command(program: &str, args: &[&str]) -> serde_json::Value { + match Command::new(program).args(args).output() { + Ok(output) => json!({ + "ok": output.status.success(), + "status": output.status.code(), + "stdout": String::from_utf8_lossy(&output.stdout), + "stderr": String::from_utf8_lossy(&output.stderr), + }), + Err(err) => json!({ + "ok": false, + "status": null, + "stdout": "", + "stderr": format!("failed to run {} {:?}: {}", program, args, err), + }), + } +} + +fn read_optional(path: &str) -> serde_json::Value { + match fs::read_to_string(path) { + Ok(content) => json!({"present": true, "value": content}), + Err(err) => json!({"present": false, "error": err.to_string()}), + } +} + +fn list_optional(path: &str) -> serde_json::Value { + match fs::read_dir(path) { + Ok(entries) => { + let mut values = entries + .flatten() + .map(|entry| entry.file_name().to_string_lossy().into_owned()) + .collect::>(); + values.sort(); + json!({"present": true, "entries": values}) + } + Err(err) => json!({"present": false, "error": err.to_string()}), + } +} + +fn unix_timestamp_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .unwrap_or(0) +} + +fn run() -> Result<(), String> { + let args: Vec = std::env::args().collect(); + parse_args(PROGRAM, USAGE, args.clone().into_iter()).map_err(|err| { + if err.is_empty() { + process::exit(0); + } + err + })?; + + let profile = args + .get(1) + .filter(|arg| !arg.starts_with('-')) + .map(String::as_str) + .unwrap_or("wifi-open-bounded"); + let iface = args + .get(2) + .filter(|arg| !arg.starts_with('-')) + .map(String::as_str) + .unwrap_or("wlan0"); + let output_path = args + .get(3) + .filter(|arg| !arg.starts_with('-')) + .map(String::as_str); + + let scheme_root = format!("/scheme/wifictl/ifaces/{iface}"); + let payload = json!({ + "captured_at_unix": unix_timestamp_secs(), + "profile": profile, + "interface": iface, + "installed": { + "driver": Path::new("/usr/lib/drivers/redbear-iwlwifi").exists(), + "wifictl": Path::new("/usr/bin/redbear-wifictl").exists(), + "netctl": Path::new("/usr/bin/redbear-netctl").exists(), + "redbear_info": Path::new("/usr/bin/redbear-info").exists(), + }, + "host": { + "uname": run_command("uname", &["-a"]), + }, + "commands": { + "driver_probe": run_command("redbear-iwlwifi", &["--probe"]), + "driver_status": run_command("redbear-iwlwifi", &["--status", iface]), + "wifictl_probe": run_command("redbear-wifictl", &["--probe"]), + "wifictl_status": run_command("redbear-wifictl", &["--status", iface]), + "netctl_list": run_command("redbear-netctl", &["list"]), + "netctl_status_all": run_command("redbear-netctl", &["status"]), + "netctl_status": run_command("redbear-netctl", &["status", profile]), + "redbear_info_json": run_command("redbear-info", &["--json"]), + "phase5_network_check": run_command("redbear-phase5-network-check", &[]), + "phase5_wifi_check": run_command("redbear-phase5-wifi-check", &[profile, iface]), + "lspci": run_command("lspci", &[]), + }, + "filesystem": { + "wifictl_ifaces": list_optional("/scheme/wifictl/ifaces"), + "netcfg_ifaces": list_optional("/scheme/netcfg/ifaces"), + "netctl_profiles": list_optional("/etc/netctl"), + "active_profile": read_optional("/etc/netctl/active"), + "profile_contents": read_optional(&format!("/etc/netctl/{profile}")), + }, + "scheme": { + "status": read_optional(&format!("{scheme_root}/status")), + "link_state": read_optional(&format!("{scheme_root}/link-state")), + "firmware_status": read_optional(&format!("{scheme_root}/firmware-status")), + "transport_status": read_optional(&format!("{scheme_root}/transport-status")), + "transport_init_status": read_optional(&format!("{scheme_root}/transport-init-status")), + "activation_status": read_optional(&format!("{scheme_root}/activation-status")), + "connect_result": read_optional(&format!("{scheme_root}/connect-result")), + "disconnect_result": read_optional(&format!("{scheme_root}/disconnect-result")), + "scan_results": read_optional(&format!("{scheme_root}/scan-results")), + "last_error": read_optional(&format!("{scheme_root}/last-error")), + } + }); + + let rendered = serde_json::to_string_pretty(&payload) + .map_err(|err| format!("failed to serialize capture payload: {err}"))?; + if let Some(output_path) = output_path { + fs::write(output_path, &rendered) + .map_err(|err| format!("failed to write capture bundle to {}: {err}", output_path))?; + } + println!("{}", rendered); + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("{PROGRAM}: {err}"); + process::exit(1); + } +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-check.rs new file mode 100644 index 00000000..91b6cc60 --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-check.rs @@ -0,0 +1,144 @@ +use std::path::Path; +use std::process::{self, Command}; + +use redbear_hwutils::parse_args; + +const PROGRAM: &str = "redbear-phase5-wifi-check"; +const USAGE: &str = "Usage: redbear-phase5-wifi-check [PROFILE] [INTERFACE]\n\nExercise the bounded Intel Wi-Fi runtime path inside a Red Bear OS guest or target runtime. The packaged runtime path defaults to the bounded open-profile flow; WPA2-PSK remains implemented and host/unit-verified, but is not yet the default packaged runtime proof."; + +fn require_path(path: &str) -> Result<(), String> { + if Path::new(path).exists() { + println!("{path}"); + Ok(()) + } else { + Err(format!("missing {path}")) + } +} + +fn require_contains(label: &str, haystack: &str, needle: &str) -> Result<(), String> { + if haystack.contains(needle) { + println!("{label}={needle}"); + Ok(()) + } else { + Err(format!("{label} missing {needle}")) + } +} + +fn run_command(program: &str, args: &[&str]) -> Result { + let output = Command::new(program) + .args(args) + .output() + .map_err(|err| format!("failed to run {} {:?}: {err}", program, args))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.trim().is_empty() { + return Err(format!( + "{} {:?} exited with status {}", + program, args, output.status + )); + } + return Err(format!( + "{} {:?} exited with status {}: {}", + program, + args, + output.status, + stderr.trim() + )); + } + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +fn run() -> Result<(), String> { + let args: Vec = std::env::args().collect(); + parse_args(PROGRAM, USAGE, args.clone().into_iter()).map_err(|err| { + if err.is_empty() { + process::exit(0); + } + err + })?; + + let profile = args + .get(1) + .filter(|arg| !arg.starts_with('-')) + .map(String::as_str) + .unwrap_or("wifi-open-bounded"); + let iface = args + .get(2) + .filter(|arg| !arg.starts_with('-')) + .map(String::as_str) + .unwrap_or("wlan0"); + + println!("=== Red Bear OS Phase 5 Wi-Fi Check ==="); + println!("profile={profile}"); + println!("interface={iface}"); + + require_path("/usr/lib/drivers/redbear-iwlwifi")?; + require_path("/usr/bin/redbear-wifictl")?; + require_path("/usr/bin/redbear-netctl")?; + require_path("/usr/bin/redbear-info")?; + + let driver_probe = run_command("redbear-iwlwifi", &["--probe"])?; + print!("{driver_probe}"); + require_contains("driver_probe", &driver_probe, "candidates=")?; + + let probe = run_command("redbear-wifictl", &["--probe"])?; + print!("{probe}"); + require_contains("wifictl_probe", &probe, "interfaces=")?; + require_contains("wifictl_probe", &probe, iface)?; + + let connect = run_command("redbear-wifictl", &["--connect", iface, "demo", "open"])?; + print!("{connect}"); + require_contains("connect", &connect, "status=connected") + .or_else(|_| require_contains("connect", &connect, "status=associated")) + .or_else(|_| require_contains("connect", &connect, "status=associating"))?; + require_contains("connect", &connect, "connect_result=")?; + + let disconnect = run_command("redbear-wifictl", &["--disconnect", iface])?; + print!("{disconnect}"); + require_contains("disconnect", &disconnect, "disconnect_result=")?; + + let start = run_command("redbear-netctl", &["start", profile])?; + print!("{start}"); + let status = run_command("redbear-netctl", &["status", profile])?; + print!("{status}"); + require_contains("netctl_status", &status, &format!("interface={iface}"))?; + require_contains("netctl_status", &status, "connect_result=")?; + + let stop = run_command("redbear-netctl", &["stop", profile])?; + print!("{stop}"); + + let info = run_command("redbear-info", &["--json"])?; + print!("{info}"); + require_contains("redbear_info", &info, "wifi_control_state")?; + require_contains("redbear_info", &info, "wifi_connect_result")?; + require_contains("redbear_info", &info, "wifi_disconnect_result")?; + + println!("PASS: bounded Intel Wi-Fi runtime path exercised inside target runtime"); + println!("NOTE: the packaged runtime checker currently validates the bounded open-profile path by default; WPA2-PSK is implemented and host/unit-verified elsewhere in-repo but is not yet the default packaged runtime proof"); + println!("NOTE: this still does not prove real AP scan/auth/association, packet flow, DHCP success over Wi-Fi, or validated end-to-end connectivity"); + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("{PROGRAM}: {err}"); + process::exit(1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn require_contains_accepts_present_substring() { + assert!( + require_contains("test", "abc wifi_control_state xyz", "wifi_control_state").is_ok() + ); + } + + #[test] + fn require_contains_rejects_missing_substring() { + assert!(require_contains("test", "abc", "wifi_connect_result").is_err()); + } +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-link-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-link-check.rs new file mode 100644 index 00000000..d46bf134 --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-link-check.rs @@ -0,0 +1,102 @@ +use std::process::{self, Command}; + +use redbear_hwutils::parse_args; +use serde_json::Value; + +const PROGRAM: &str = "redbear-phase5-wifi-link-check"; +const USAGE: &str = "Usage: redbear-phase5-wifi-link-check\n\nCheck whether the current runtime exposes Wi-Fi interface/address/route signals beyond the bounded lifecycle layer."; + +fn require_field<'a>(value: &'a Value, field: &str) -> Result<&'a Value, String> { + value + .get(field) + .ok_or_else(|| format!("redbear-info --json did not report {field}")) +} + +fn present_nonempty(value: &Value) -> bool { + value + .as_str() + .map(|s| !s.trim().is_empty() && s != "unknown") + .unwrap_or(false) +} + +fn run() -> Result<(), String> { + parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| { + if err.is_empty() { + process::exit(0); + } + err + })?; + + println!("=== Red Bear OS Phase 5 Wi-Fi Link Check ==="); + let output = Command::new("redbear-info") + .arg("--json") + .output() + .map_err(|err| format!("failed to run redbear-info --json: {err}"))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("redbear-info --json failed: {}", stderr.trim())); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let json: Value = serde_json::from_str(&stdout) + .map_err(|err| format!("failed to parse redbear-info --json output: {err}"))?; + let network = json + .get("network") + .ok_or_else(|| "redbear-info --json did not include network section".to_string())?; + + let interface = require_field(network, "interface")?; + let address = require_field(network, "address")?; + let default_route = require_field(network, "default_route")?; + let wifi_connect_result = require_field(network, "wifi_connect_result")?; + + if present_nonempty(interface) { + println!("WIFI_INTERFACE=present"); + } else { + return Err("Wi-Fi/network interface is not reported".to_string()); + } + + if present_nonempty(wifi_connect_result) { + println!("WIFI_CONNECT_RESULT=present"); + } else { + return Err("Wi-Fi connect result is not reported".to_string()); + } + + if present_nonempty(address) { + println!("WIFI_ADDRESS=present"); + } else { + println!("WIFI_ADDRESS=missing"); + } + + if present_nonempty(default_route) { + println!("WIFI_DEFAULT_ROUTE=present"); + } else { + println!("WIFI_DEFAULT_ROUTE=missing"); + } + + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("{PROGRAM}: {err}"); + process::exit(1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn present_nonempty_rejects_unknown() { + assert!(!present_nonempty(&json!("unknown"))); + assert!(!present_nonempty(&json!(""))); + } + + #[test] + fn present_nonempty_accepts_value() { + assert!(present_nonempty(&json!("wlan0"))); + assert!(present_nonempty(&json!("10.0.0.44/24"))); + } +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-run.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-run.rs new file mode 100644 index 00000000..efe16456 --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase5-wifi-run.rs @@ -0,0 +1,88 @@ +use std::process::{self, Command}; + +use redbear_hwutils::parse_args; + +const PROGRAM: &str = "redbear-phase5-wifi-run"; +const USAGE: &str = "Usage: redbear-phase5-wifi-run [PROFILE] [INTERFACE] [OUTPUT_PATH]\n\nRun the packaged bounded Wi-Fi validator and then emit a JSON capture bundle."; + +fn run_command(program: &str, args: &[&str]) -> Result { + let output = Command::new(program) + .args(args) + .output() + .map_err(|err| format!("failed to run {} {:?}: {err}", program, args))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.trim().is_empty() { + return Err(format!( + "{} {:?} exited with status {}", + program, args, output.status + )); + } + return Err(format!( + "{} {:?} exited with status {}: {}", + program, + args, + output.status, + stderr.trim() + )); + } + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +fn run() -> Result<(), String> { + let args: Vec = std::env::args().collect(); + parse_args(PROGRAM, USAGE, args.clone().into_iter()).map_err(|err| { + if err.is_empty() { + process::exit(0); + } + err + })?; + + let profile = args + .get(1) + .filter(|arg| !arg.starts_with('-')) + .map(String::as_str) + .unwrap_or("wifi-open-bounded"); + let iface = args + .get(2) + .filter(|arg| !arg.starts_with('-')) + .map(String::as_str) + .unwrap_or("wlan0"); + let output_path = args + .get(3) + .filter(|arg| !arg.starts_with('-')) + .map(String::as_str) + .unwrap_or("/tmp/redbear-phase5-wifi-capture.json"); + + let check = run_command("redbear-phase5-wifi-check", &[profile, iface])?; + print!("{check}"); + + let link = run_command("redbear-phase5-wifi-link-check", &[])?; + print!("{link}"); + + let capture = run_command( + "redbear-phase5-wifi-capture", + &[profile, iface, output_path], + )?; + print!("{capture}"); + println!("capture_output={output_path}"); + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("{PROGRAM}: {err}"); + process::exit(1); + } +} + +#[cfg(test)] +mod tests { + #[test] + fn default_capture_path_is_stable() { + assert_eq!( + "/tmp/redbear-phase5-wifi-capture.json", + "/tmp/redbear-phase5-wifi-capture.json" + ); + } +} diff --git a/local/recipes/system/redbear-wifictl/recipe.toml b/local/recipes/system/redbear-wifictl/recipe.toml new file mode 100644 index 00000000..70a18c05 --- /dev/null +++ b/local/recipes/system/redbear-wifictl/recipe.toml @@ -0,0 +1,9 @@ +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/bin/redbear-wifictl" = "redbear-wifictl" +"/usr/bin/wifictl" = "redbear-wifictl" diff --git a/local/recipes/system/redbear-wifictl/source/Cargo.toml b/local/recipes/system/redbear-wifictl/source/Cargo.toml new file mode 100644 index 00000000..e89aa52a --- /dev/null +++ b/local/recipes/system/redbear-wifictl/source/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "redbear-wifictl" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "redbear-wifictl" +path = "src/main.rs" + +[dependencies] +libc = "0.2" +libredox = { version = "0.1", features = ["call", "std"] } +log = { version = "0.4", features = ["std"] } +redox-scheme = "0.11" +syscall = { package = "redox_syscall", version = "0.7", features = ["std"] } + +[target.'cfg(target_os = "redox")'.dependencies] +redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source", features = ["redox"] } diff --git a/local/recipes/system/redbear-wifictl/source/src/backend.rs b/local/recipes/system/redbear-wifictl/source/src/backend.rs new file mode 100644 index 00000000..3929bba7 --- /dev/null +++ b/local/recipes/system/redbear-wifictl/source/src/backend.rs @@ -0,0 +1,1452 @@ +use std::collections::BTreeMap; +use std::env; +use std::fs; +use std::fs::OpenOptions; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::PathBuf; +use std::process::Command; + +#[cfg(test)] +pub(crate) static TEST_ENV_LOCK: std::sync::LazyLock> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(())); + +#[cfg(target_os = "redox")] +use redox_driver_sys::pci::PciDevice; +#[cfg(target_os = "redox")] +use redox_driver_sys::pci::PciLocation; + +#[derive(Clone, Debug)] +struct ParsedPciLocation { + segment: u16, + bus: u8, + device: u8, + function: u8, +} + +impl std::fmt::Display for ParsedPciLocation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:04x}--{:02x}--{:02x}.{}", + self.segment, self.bus, self.device, self.function + ) + } +} + +#[cfg(target_os = "redox")] +impl From for PciLocation { + fn from(value: ParsedPciLocation) -> Self { + Self { + segment: value.segment, + bus: value.bus, + device: value.device, + function: value.function, + } + } +} + +#[allow(dead_code)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum WifiStatus { + Down, + DeviceDetected, + FirmwareReady, + Scanning, + Associating, + Connected, + Failed, +} + +impl WifiStatus { + pub fn as_str(&self) -> &'static str { + match self { + WifiStatus::Down => "down", + WifiStatus::DeviceDetected => "device-detected", + WifiStatus::FirmwareReady => "firmware-ready", + WifiStatus::Scanning => "scanning", + WifiStatus::Associating => "associating", + WifiStatus::Connected => "connected", + WifiStatus::Failed => "failed", + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct InterfaceState { + pub ssid: String, + pub security: String, + pub key: String, + pub status: String, + pub link_state: String, + pub firmware_status: String, + pub transport_status: String, + pub transport_init_status: String, + pub activation_status: String, + pub connect_result: String, + pub disconnect_result: String, + pub last_error: String, + pub scan_results: Vec, +} + +pub trait Backend { + fn interfaces(&self) -> Vec; + fn capabilities(&self) -> Vec; + #[allow(dead_code)] + fn initial_status(&self, interface: &str) -> WifiStatus; + fn initial_link_state(&self, interface: &str) -> String; + #[allow(dead_code)] + fn default_scan_results(&self, interface: &str) -> Vec; + fn scan(&mut self, interface: &str) -> Result, String>; + fn firmware_status(&self, interface: &str) -> String; + fn transport_status(&self, interface: &str) -> String; + fn prepare(&mut self, interface: &str) -> Result; + fn transport_probe(&mut self, interface: &str) -> Result; + fn init_transport(&mut self, interface: &str) -> Result; + fn activate(&mut self, interface: &str) -> Result; + fn connect_result(&self, interface: &str) -> String; + fn disconnect_result(&self, interface: &str) -> String; + fn retry(&mut self, interface: &str) -> Result; + #[allow(dead_code)] + fn connect(&mut self, interface: &str, state: &InterfaceState) -> Result; + #[allow(dead_code)] + fn disconnect(&mut self, interface: &str) -> Result; +} + +#[derive(Clone, Debug)] +struct IntelInterface { + name: String, + location: String, + config_path: PathBuf, + device_id: u16, + subsystem_id: u16, + firmware_family: &'static str, + transport_status: String, + ucode_candidates: Vec, + selected_ucode: Option, + pnvm_candidate: Option, + pnvm_found: Option, + prepared: bool, + transport_initialized: bool, + activated: bool, + connect_result: String, + disconnect_result: String, +} + +pub struct StubBackend { + interfaces: Vec, +} + +pub struct NoDeviceBackend; + +impl StubBackend { + pub fn from_env() -> Self { + let interfaces = env::var("REDBEAR_WIFICTL_STUB_INTERFACES") + .ok() + .map(|value| { + value + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + .collect::>() + }) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| vec!["wlan0".to_string()]); + Self { interfaces } + } +} + +impl NoDeviceBackend { + pub fn new() -> Self { + Self + } +} + +impl Backend for StubBackend { + fn interfaces(&self) -> Vec { + self.interfaces.clone() + } + + fn capabilities(&self) -> Vec { + vec![ + "backend=stub".to_string(), + "security=open,wpa2-psk".to_string(), + "scan=true".to_string(), + "connect=true".to_string(), + ] + } + + fn initial_status(&self, _interface: &str) -> WifiStatus { + WifiStatus::DeviceDetected + } + + fn default_scan_results(&self, _interface: &str) -> Vec { + vec!["demo-ssid".to_string(), "demo-open".to_string()] + } + + fn scan(&mut self, _interface: &str) -> Result, String> { + Ok(vec!["demo-ssid".to_string(), "demo-open".to_string()]) + } + + fn firmware_status(&self, _interface: &str) -> String { + "firmware=stub".to_string() + } + + fn transport_status(&self, _interface: &str) -> String { + "transport=stub".to_string() + } + + fn initial_link_state(&self, _interface: &str) -> String { + "link=down".to_string() + } + + fn init_transport(&mut self, _interface: &str) -> Result { + Ok("transport_init=stub".to_string()) + } + + fn activate(&mut self, _interface: &str) -> Result { + Ok("activation=stub".to_string()) + } + + fn connect_result(&self, _interface: &str) -> String { + "connect=stub".to_string() + } + + fn disconnect_result(&self, _interface: &str) -> String { + "disconnect=stub".to_string() + } + + fn retry(&mut self, _interface: &str) -> Result { + Ok(WifiStatus::DeviceDetected) + } + + fn prepare(&mut self, _interface: &str) -> Result { + Ok(WifiStatus::FirmwareReady) + } + + fn transport_probe(&mut self, _interface: &str) -> Result { + Ok("transport=stub mmio_probe=host-skipped".to_string()) + } + + fn connect(&mut self, _interface: &str, state: &InterfaceState) -> Result { + if state.ssid.is_empty() { + return Err("missing SSID".to_string()); + } + match state.security.as_str() { + "open" => Ok(WifiStatus::Connected), + "wpa2-psk" if !state.key.is_empty() => Ok(WifiStatus::Connected), + "wpa2-psk" => Err("missing key".to_string()), + other => Err(format!("unsupported security {other}")), + } + } + + fn disconnect(&mut self, _interface: &str) -> Result { + Ok(WifiStatus::DeviceDetected) + } +} + +impl Backend for NoDeviceBackend { + fn interfaces(&self) -> Vec { + Vec::new() + } + + fn capabilities(&self) -> Vec { + vec![ + "backend=no-device".to_string(), + "target=intel-not-detected".to_string(), + "scan=false".to_string(), + "connect=false".to_string(), + ] + } + + fn initial_status(&self, _interface: &str) -> WifiStatus { + WifiStatus::Down + } + + fn default_scan_results(&self, _interface: &str) -> Vec { + Vec::new() + } + + fn scan(&mut self, _interface: &str) -> Result, String> { + Err("no Intel Wi-Fi device detected".to_string()) + } + + fn firmware_status(&self, _interface: &str) -> String { + "firmware=no-device".to_string() + } + + fn transport_status(&self, _interface: &str) -> String { + "transport=no-device".to_string() + } + + fn initial_link_state(&self, _interface: &str) -> String { + "link=no-device".to_string() + } + + fn init_transport(&mut self, _interface: &str) -> Result { + Err("no Intel Wi-Fi device detected".to_string()) + } + + fn activate(&mut self, _interface: &str) -> Result { + Err("no Intel Wi-Fi device detected".to_string()) + } + + fn connect_result(&self, _interface: &str) -> String { + "connect=no-device".to_string() + } + + fn disconnect_result(&self, _interface: &str) -> String { + "disconnect=no-device".to_string() + } + + fn retry(&mut self, _interface: &str) -> Result { + Err("no Intel Wi-Fi device detected".to_string()) + } + + fn prepare(&mut self, _interface: &str) -> Result { + Err("no Intel Wi-Fi device detected".to_string()) + } + + fn transport_probe(&mut self, _interface: &str) -> Result { + Err("no Intel Wi-Fi device detected".to_string()) + } + + fn connect(&mut self, _interface: &str, _state: &InterfaceState) -> Result { + Err("no Intel Wi-Fi device detected".to_string()) + } + + fn disconnect(&mut self, _interface: &str) -> Result { + Err("no Intel Wi-Fi device detected".to_string()) + } +} + +pub struct IntelBackend { + pci_root: PathBuf, + firmware_root: PathBuf, + firmware_scheme_root: PathBuf, + interfaces: Vec, +} + +impl IntelBackend { + pub fn from_env() -> Self { + let pci_root = env::var_os("REDBEAR_WIFICTL_PCI_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/scheme/pci")); + let firmware_root = env::var_os("REDBEAR_WIFICTL_FIRMWARE_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/lib/firmware")); + let firmware_scheme_root = env::var_os("REDBEAR_WIFICTL_FIRMWARE_SCHEME_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/scheme/firmware")); + let interfaces = detect_intel_wifi_interfaces(&pci_root, &firmware_root); + Self { + pci_root, + firmware_root, + firmware_scheme_root, + interfaces, + } + } + + fn read_firmware_blob(&self, name: &str) -> Result, String> { + let scheme_path = self.firmware_scheme_root.join(name); + if let Ok(bytes) = fs::read(&scheme_path) { + return Ok(bytes); + } + + let fs_path = self.firmware_root.join(name); + fs::read(&fs_path).map_err(|err| { + format!( + "failed to read firmware {} from {} or {}: {err}", + name, + scheme_path.display(), + fs_path.display() + ) + }) + } + + fn driver_command() -> PathBuf { + env::var_os("REDBEAR_IWLWIFI_CMD") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/usr/lib/drivers/redbear-iwlwifi")) + } + + fn run_driver_action( + &self, + action: &str, + iface: &IntelInterface, + ) -> Result, String> { + self.run_driver_action_with_args(action, iface, &[]) + } + + fn run_driver_action_with_args( + &self, + action: &str, + iface: &IntelInterface, + extra_args: &[&str], + ) -> Result, String> { + let cmd = Self::driver_command(); + if !cmd.exists() { + return Err(format!("driver command {} not found", cmd.display())); + } + + let mut command = Command::new(&cmd); + command + .arg(action) + .arg(iface.location.to_string()) + .args(extra_args) + .env("REDBEAR_IWLWIFI_PCI_ROOT", &self.pci_root) + .env("REDBEAR_IWLWIFI_FIRMWARE_ROOT", &self.firmware_root); + + let output = command + .output() + .map_err(|err| format!("failed to run {} {}: {err}", cmd.display(), action))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(if stderr.is_empty() { + format!( + "{} {} failed with status {}", + cmd.display(), + action, + output.status + ) + } else { + stderr + }); + } + + Ok(String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect()) + } + + fn line_value(lines: &[String], key: &str) -> Option { + lines + .iter() + .find_map(|line| line.strip_prefix(&format!("{key}=")).map(str::to_string)) + } +} + +impl Backend for IntelBackend { + fn interfaces(&self) -> Vec { + self.interfaces + .iter() + .map(|iface| iface.name.clone()) + .collect() + } + + fn capabilities(&self) -> Vec { + let mut capabilities = vec![ + "backend=intel".to_string(), + "target=arrow-lake-and-older".to_string(), + "security=open,wpa2-psk".to_string(), + format!("pci-root={}", self.pci_root.display()), + format!("firmware-root={}", self.firmware_root.display()), + format!( + "firmware-scheme-root={}", + self.firmware_scheme_root.display() + ), + "transport=iwlwifi-class".to_string(), + ]; + for iface in &self.interfaces { + let candidate_list = iface.ucode_candidates.join(","); + let found = iface + .selected_ucode + .clone() + .unwrap_or_else(|| "missing".to_string()); + let pnvm = iface + .pnvm_found + .clone() + .or_else(|| iface.pnvm_candidate.clone()) + .unwrap_or_else(|| "none".to_string()); + capabilities.push(format!( + "iface={} device={:04x} subsystem={:04x} family={} transport={} prepared={} activated={} ucode_candidates={} ucode_selected={} pnvm={}", + iface.name, + iface.device_id, + iface.subsystem_id, + iface.firmware_family, + iface.transport_status, + if iface.prepared { "yes" } else { "no" }, + if iface.activated { "yes" } else { "no" }, + candidate_list, + found, + pnvm + )); + } + capabilities + } + + fn initial_status(&self, interface: &str) -> WifiStatus { + if self.interfaces.is_empty() { + WifiStatus::Down + } else if self + .interfaces + .iter() + .find(|candidate| candidate.name == interface) + .map(|candidate| candidate.prepared) + .unwrap_or(false) + { + WifiStatus::FirmwareReady + } else { + WifiStatus::DeviceDetected + } + } + + fn default_scan_results(&self, _interface: &str) -> Vec { + Vec::new() + } + + fn scan(&mut self, interface: &str) -> Result, String> { + let Some(pos) = self + .interfaces + .iter() + .position(|candidate| candidate.name == interface) + else { + return Err("unknown wireless interface".to_string()); + }; + let candidate = &self.interfaces[pos]; + if !candidate.prepared { + return Err("firmware not prepared; run prepare first".to_string()); + } + if !candidate.transport_initialized { + return Err("transport not initialized; run init-transport first".to_string()); + } + if !candidate.activated { + return Err("NIC not activated; run activate-nic first".to_string()); + } + let scan_lines = self.run_driver_action("--scan", candidate)?; + let mut results = scan_lines + .iter() + .filter_map(|line| line.strip_prefix("scan_result=").map(str::to_string)) + .collect::>(); + if results.is_empty() { + results = vec!["driver-scan-not-implemented".to_string()]; + } + Ok(results) + } + + fn firmware_status(&self, interface: &str) -> String { + let Some(candidate) = self + .interfaces + .iter() + .find(|candidate| candidate.name == interface) + else { + return "firmware=unknown-interface".to_string(); + }; + match &candidate.selected_ucode { + Some(found) => format!( + "firmware=present family={} prepared={} selected={} pnvm={} candidates={}", + candidate.firmware_family, + if candidate.prepared { "yes" } else { "no" }, + found, + candidate + .pnvm_found + .clone() + .or_else(|| candidate.pnvm_candidate.clone()) + .unwrap_or_else(|| "none".to_string()), + candidate.ucode_candidates.join(",") + ), + None => format!( + "firmware=missing family={} prepared={} candidates={} pnvm={}", + candidate.firmware_family, + if candidate.prepared { "yes" } else { "no" }, + candidate.ucode_candidates.join(","), + candidate + .pnvm_candidate + .clone() + .unwrap_or_else(|| "none".to_string()) + ), + } + } + + fn transport_status(&self, interface: &str) -> String { + self.interfaces + .iter() + .find(|candidate| candidate.name == interface) + .map(|candidate| candidate.transport_status.clone()) + .unwrap_or_else(|| "transport=unknown-interface".to_string()) + } + + fn initial_link_state(&self, interface: &str) -> String { + if self + .interfaces + .iter() + .any(|candidate| candidate.name == interface) + { + "link=down".to_string() + } else { + "link=unknown-interface".to_string() + } + } + + fn init_transport(&mut self, interface: &str) -> Result { + let Some(pos) = self + .interfaces + .iter() + .position(|candidate| candidate.name == interface) + else { + return Err("unknown wireless interface".to_string()); + }; + let candidate = &self.interfaces[pos]; + if !candidate.prepared { + return Err("firmware not prepared; run prepare first".to_string()); + } + let transport_lines = self + .run_driver_action("--init-transport", candidate) + .or_else(|_| { + init_transport_action(&candidate.config_path, candidate.firmware_family) + .map(|v| vec![v]) + })?; + let transport_status = Self::line_value(&transport_lines, "transport_status") + .or_else(|| { + Self::line_value(&transport_lines, "status") + .map(|status| format!("transport_status={status}")) + }) + .unwrap_or_else(|| format!("transport_status={}", candidate.transport_status)); + self.interfaces[pos].transport_status = transport_status.clone(); + self.interfaces[pos].transport_initialized = true; + Ok("transport_init=ok".to_string()) + } + + fn activate(&mut self, interface: &str) -> Result { + let Some(pos) = self + .interfaces + .iter() + .position(|candidate| candidate.name == interface) + else { + return Err("unknown wireless interface".to_string()); + }; + let candidate = &self.interfaces[pos]; + if !candidate.transport_initialized { + return Err("transport not initialized; run init-transport first".to_string()); + } + let activation_lines = self + .run_driver_action("--activate-nic", candidate) + .or_else(|_| activate_nic_action(&candidate.config_path).map(|v| vec![v]))?; + let activation_status = Self::line_value(&activation_lines, "activation_status") + .or_else(|| Self::line_value(&activation_lines, "activation")) + .unwrap_or_else(|| "activation=ok".to_string()); + self.interfaces[pos].transport_status = + Self::line_value(&activation_lines, "transport_status").unwrap_or_else(|| { + transport_status_after_prepare(&candidate.config_path, candidate.firmware_family) + .unwrap_or_else(|_| candidate.transport_status.clone()) + }); + self.interfaces[pos].activated = true; + Ok(activation_status) + } + + fn connect_result(&self, interface: &str) -> String { + self.interfaces + .iter() + .find(|candidate| candidate.name == interface) + .map(|candidate| candidate.connect_result.clone()) + .unwrap_or_else(|| "connect=unknown-interface".to_string()) + } + + fn disconnect_result(&self, interface: &str) -> String { + self.interfaces + .iter() + .find(|candidate| candidate.name == interface) + .map(|candidate| candidate.disconnect_result.clone()) + .unwrap_or_else(|| "disconnect=unknown-interface".to_string()) + } + + fn prepare(&mut self, interface: &str) -> Result { + let Some(pos) = self + .interfaces + .iter() + .position(|candidate| candidate.name == interface) + else { + return Err("unknown wireless interface".to_string()); + }; + let candidate = &self.interfaces[pos]; + let firmware_family = candidate.firmware_family; + let config_path = candidate.config_path.clone(); + let prepare_lines = self + .run_driver_action("--prepare", candidate) + .or_else(|_| { + let Some(ucode) = candidate.selected_ucode.clone() else { + return Err(format!( + "missing firmware for {} (expected one of: {})", + firmware_family, + candidate.ucode_candidates.join(", ") + )); + }; + let pnvm_required = candidate.pnvm_candidate.clone(); + let pnvm_found = candidate.pnvm_found.clone(); + if let Some(pnvm) = pnvm_required.as_ref() { + if pnvm_found.is_none() { + return Err(format!( + "missing pnvm for {} (expected {})", + firmware_family, pnvm + )); + } + } + let _ = self.read_firmware_blob(&ucode)?; + if let Some(pnvm) = pnvm_found.as_ref() { + let _ = self.read_firmware_blob(pnvm)?; + } + program_transport_bits(&config_path)?; + let transport_status = + transport_status_after_prepare(&config_path, firmware_family)?; + Ok(vec![ + format!("status={}", WifiStatus::FirmwareReady.as_str()), + format!("transport_status={transport_status}"), + ]) + })?; + let transport_status = + Self::line_value(&prepare_lines, "transport_status").unwrap_or_else(|| { + transport_status_after_prepare(&config_path, firmware_family) + .unwrap_or_else(|_| candidate.transport_status.clone()) + }); + self.interfaces[pos].prepared = true; + self.interfaces[pos].transport_status = transport_status; + Ok(WifiStatus::FirmwareReady) + } + + fn transport_probe(&mut self, interface: &str) -> Result { + let Some(pos) = self + .interfaces + .iter() + .position(|candidate| candidate.name == interface) + else { + return Err("unknown wireless interface".to_string()); + }; + let candidate = &self.interfaces[pos]; + self.run_driver_action("--transport-probe", candidate) + .ok() + .and_then(|lines| Self::line_value(&lines, "transport_status")) + .map(Ok) + .unwrap_or_else(|| { + transport_status_after_prepare(&candidate.config_path, candidate.firmware_family) + }) + } + + fn connect(&mut self, interface: &str, state: &InterfaceState) -> Result { + let Some(pos) = self + .interfaces + .iter() + .position(|candidate| candidate.name == interface) + else { + return Err("unknown wireless interface".to_string()); + }; + let candidate = &self.interfaces[pos]; + if !candidate.prepared { + return Err("firmware not prepared; run prepare first".to_string()); + } + if !candidate.transport_initialized { + return Err("transport not initialized; run init-transport first".to_string()); + } + if !candidate.activated { + return Err("NIC not activated; run activate-nic first".to_string()); + } + if candidate.selected_ucode.is_none() { + return Err(format!( + "missing firmware for {} (expected one of: {})", + candidate.firmware_family, + candidate.ucode_candidates.join(", ") + )); + } + if state.ssid.is_empty() { + return Err("missing SSID".to_string()); + } + let security = if state.security.is_empty() { + "open" + } else { + state.security.as_str() + }; + if security == "wpa2-psk" && state.key.is_empty() { + return Err("missing key".to_string()); + } + + let connect_lines = self.run_driver_action_with_args( + "--connect", + candidate, + &[state.ssid.as_str(), security, state.key.as_str()], + )?; + self.interfaces[pos].connect_result = Self::line_value(&connect_lines, "connect_result") + .unwrap_or_else(|| format!("connect_result=ssid={} security={security}", state.ssid)); + if Self::line_value(&connect_lines, "status").as_deref() == Some("associated") { + Ok(WifiStatus::Connected) + } else { + Ok(WifiStatus::Associating) + } + } + + fn disconnect(&mut self, interface: &str) -> Result { + let Some(pos) = self + .interfaces + .iter() + .position(|candidate| candidate.name == interface) + else { + return Err("unknown wireless interface".to_string()); + }; + let candidate = &self.interfaces[pos]; + if !candidate.activated { + return Err("NIC not activated; run activate-nic first".to_string()); + } + let lines = self.run_driver_action("--disconnect", candidate)?; + self.interfaces[pos].disconnect_result = Self::line_value(&lines, "disconnect_result") + .unwrap_or_else(|| "disconnect_result=ok".to_string()); + Ok(WifiStatus::DeviceDetected) + } + + fn retry(&mut self, interface: &str) -> Result { + let Some(candidate) = self + .interfaces + .iter() + .find(|candidate| candidate.name == interface) + else { + return Err("unknown wireless interface".to_string()); + }; + if !candidate.prepared { + return Err("firmware not prepared; run prepare first".to_string()); + } + if !candidate.transport_initialized { + return Err("transport not initialized; run init-transport first".to_string()); + } + let retry_lines = self.run_driver_action("--retry", candidate)?; + if Self::line_value(&retry_lines, "status").as_deref() == Some("device-detected") { + Ok(WifiStatus::DeviceDetected) + } else { + Ok(WifiStatus::Failed) + } + } +} + +fn detect_intel_wifi_interfaces( + pci_root: &PathBuf, + firmware_root: &PathBuf, +) -> Vec { + let mut devices = BTreeMap::new(); + let Ok(entries) = fs::read_dir(pci_root) else { + return Vec::new(); + }; + + for entry in entries.flatten() { + let Ok(config) = fs::read(entry.path().join("config")) else { + continue; + }; + if config.len() < 48 { + continue; + } + let vendor_id = u16::from_le_bytes([config[0x00], config[0x01]]); + let device_id = u16::from_le_bytes([config[0x02], config[0x03]]); + let class_code = config[0x0B]; + let subclass = config[0x0A]; + let subsystem_id = u16::from_le_bytes([config[0x2E], config[0x2F]]); + if vendor_id == 0x8086 && class_code == 0x02 && subclass == 0x80 { + let Ok(location) = parse_location_from_config_path(&entry.path().join("config")) else { + continue; + }; + let idx = devices.len(); + let (firmware_family, ucode_candidates, pnvm_candidate) = + intel_firmware_candidates(device_id, subsystem_id); + let selected_ucode = ucode_candidates + .iter() + .find(|candidate| firmware_root.join(candidate).exists()) + .cloned(); + let pnvm_found = pnvm_candidate + .as_ref() + .filter(|candidate| firmware_root.join(candidate).exists()) + .cloned(); + devices.insert( + format!("wlan{idx}"), + IntelInterface { + name: format!("wlan{idx}"), + location: location.to_string(), + config_path: entry.path().join("config"), + device_id, + subsystem_id, + firmware_family, + transport_status: transport_status_from_config(&config), + ucode_candidates, + selected_ucode, + pnvm_candidate, + pnvm_found, + prepared: false, + transport_initialized: false, + activated: false, + connect_result: "connect=not-run".to_string(), + disconnect_result: "disconnect=not-run".to_string(), + }, + ); + } + } + + devices.into_values().collect() +} + +fn intel_firmware_candidates( + device_id: u16, + subsystem_id: u16, +) -> (&'static str, Vec, Option) { + let (stems, pnvm): (Vec<&'static str>, Option<&'static str>) = match (device_id, subsystem_id) { + (0x7740, 0x4090) => ( + vec![ + "iwlwifi-bz-b0-gf-a0-92.ucode", + "iwlwifi-bz-b0-gf-a0-94.ucode", + "iwlwifi-bz-b0-gf-a0-100.ucode", + ], + Some("iwlwifi-bz-b0-gf-a0.pnvm"), + ), + (0x7740, _) => ( + vec![ + "iwlwifi-bz-b0-fm-c0-92.ucode", + "iwlwifi-bz-b0-fm-c0-94.ucode", + "iwlwifi-bz-b0-fm-c0-100.ucode", + ], + Some("iwlwifi-bz-b0-fm-c0.pnvm"), + ), + (0x2725, _) => ( + vec![ + "iwlwifi-ty-a0-gf-a0-59.ucode", + "iwlwifi-ty-a0-gf-a0-84.ucode", + ], + Some("iwlwifi-ty-a0-gf-a0.pnvm"), + ), + (0x7af0, 0x4090) => ( + vec![ + "iwlwifi-so-a0-gf-a0-64.ucode", + "iwlwifi-so-a0-gf-a0-66.ucode", + ], + Some("iwlwifi-so-a0-gf-a0.pnvm"), + ), + (0x7af0, 0x4070) => ( + vec!["iwlwifi-so-a0-hr-b0-64.ucode"], + Some("iwlwifi-so-a0-hr-b0.pnvm"), + ), + (0x7af0, 0x0aaa) | (0x7af0, 0x0030) => ( + vec![ + "iwlwifi-so-a0-jf-b0-64.ucode", + "iwlwifi-9000-pu-b0-jf-b0-46.ucode", + ], + Some("iwlwifi-so-a0-jf-b0.pnvm"), + ), + _ => (vec!["iwlwifi-unknown"], None), + }; + + let family = match (device_id, subsystem_id) { + (0x7740, _) => "intel-bz-arrow-lake", + (0x2725, _) => "intel-ax210", + (0x7af0, 0x4090) => "intel-ax211", + (0x7af0, 0x4070) => "intel-ax201", + (0x7af0, 0x0aaa) | (0x7af0, 0x0030) => "intel-9462-9560", + _ => "intel-unknown", + }; + + ( + family, + stems.into_iter().map(str::to_string).collect(), + pnvm.map(str::to_string), + ) +} + +fn transport_status_from_config(config: &[u8]) -> String { + let command = u16::from_le_bytes([config[0x04], config[0x05]]); + let bar0 = u32::from_le_bytes([config[0x10], config[0x11], config[0x12], config[0x13]]); + let irq_pin = config[0x3D]; + + let memory_enabled = (command & 0x2) != 0; + let bus_master = (command & 0x4) != 0; + let bar_present = bar0 != 0; + let irq_present = irq_pin != 0; + + format!( + "transport=pci memory_enabled={} bus_master={} bar0_present={} irq_pin_present={}", + if memory_enabled { "yes" } else { "no" }, + if bus_master { "yes" } else { "no" }, + if bar_present { "yes" } else { "no" }, + if irq_present { "yes" } else { "no" } + ) +} + +#[cfg(target_os = "redox")] +const IWL_CSR_HW_IF_CONFIG_REG: usize = 0x000; +#[cfg(target_os = "redox")] +const IWL_CSR_RESET: usize = 0x020; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL: usize = 0x024; +#[cfg(target_os = "redox")] +const IWL_CSR_HW_REV: usize = 0x028; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ: u32 = 0x00000008; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_BZ_MAC_ACCESS_REQ: u32 = 0x00200000; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY: u32 = 0x00000001; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_INIT_DONE: u32 = 0x00000004; +#[cfg(target_os = "redox")] +const IWL_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY: u32 = 0x00000004; +#[cfg(target_os = "redox")] +const IWL_CSR_GP_CNTRL_REG_FLAG_SW_RESET_BZ: u32 = 0x80000000; +#[cfg(target_os = "redox")] +const IWL_CSR_RESET_REG_FLAG_SW_RESET: u32 = 0x00000080; + +fn parse_location_from_config_path(config_path: &PathBuf) -> Result { + let parent = config_path + .parent() + .ok_or_else(|| format!("missing PCI parent for {}", config_path.display()))?; + let name = parent + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| format!("invalid PCI path {}", parent.display()))?; + + let parts: Vec<&str> = name.splitn(3, "--").collect(); + if parts.len() != 3 { + return Err(format!("invalid PCI scheme entry {name}")); + } + let segment = + u16::from_str_radix(parts[0], 16).map_err(|_| format!("invalid segment in {name}"))?; + let bus = u8::from_str_radix(parts[1], 16).map_err(|_| format!("invalid bus in {name}"))?; + let dev_func: Vec<&str> = parts[2].splitn(2, '.').collect(); + if dev_func.len() != 2 { + return Err(format!("invalid device/function in {name}")); + } + let device = + u8::from_str_radix(dev_func[0], 16).map_err(|_| format!("invalid device in {name}"))?; + let function = + u8::from_str_radix(dev_func[1], 16).map_err(|_| format!("invalid function in {name}"))?; + + Ok(ParsedPciLocation { + segment, + bus, + device, + function, + }) +} + +#[cfg(target_os = "redox")] +fn transport_status_after_prepare(config_path: &PathBuf, family: &str) -> Result { + let location: PciLocation = parse_location_from_config_path(config_path)?.into(); + let mut pci = PciDevice::open_location(&location) + .map_err(|err| format!("failed to reopen PCI device {location}: {err}"))?; + let info = pci + .full_info() + .map_err(|err| format!("failed to read PCI info for {location}: {err}"))?; + let bar = info + .find_memory_bar(0) + .ok_or_else(|| format!("no memory BAR0 for {location}"))?; + let (addr, size) = bar + .memory_info() + .ok_or_else(|| format!("invalid BAR0 mapping info for {location}"))?; + let mmio = pci + .map_bar(0, addr, size) + .map_err(|err| format!("failed to map BAR0 for {location}: {err}"))?; + let reg0 = mmio.read32(0); + let hw_rev = mmio.read32(IWL_CSR_HW_REV); + let gp_before = mmio.read32(IWL_CSR_GP_CNTRL); + let access_req = if family.starts_with("intel-bz-") { + IWL_CSR_GP_CNTRL_REG_FLAG_BZ_MAC_ACCESS_REQ + } else { + IWL_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ + }; + mmio.write32(IWL_CSR_GP_CNTRL, gp_before | access_req); + let gp_after = mmio.read32(IWL_CSR_GP_CNTRL); + let hw_if = mmio.read32(IWL_CSR_HW_IF_CONFIG_REG); + let mac_clock = (gp_after & IWL_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY) != 0; + let nic_ready = (hw_if & IWL_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY) != 0; + Ok(format!( + "{} mmio_probe=ok reg0=0x{reg0:08x} hw_rev=0x{hw_rev:08x} mac_access_req={} mac_clock_ready={} nic_ready={}", + read_transport_status(config_path)?, + if family.starts_with("intel-bz-") { "bz" } else { "legacy" }, + if mac_clock { "yes" } else { "no" }, + if nic_ready { "yes" } else { "no" } + )) +} + +#[cfg(not(target_os = "redox"))] +fn transport_status_after_prepare(config_path: &PathBuf, _family: &str) -> Result { + Ok(format!( + "{} mmio_probe=host-skipped", + read_transport_status(config_path)? + )) +} + +#[cfg(target_os = "redox")] +fn init_transport_action(config_path: &PathBuf, family: &str) -> Result { + let location: PciLocation = parse_location_from_config_path(config_path)?.into(); + let mut pci = PciDevice::open_location(&location) + .map_err(|err| format!("failed to reopen PCI device {location}: {err}"))?; + let info = pci + .full_info() + .map_err(|err| format!("failed to read PCI info for {location}: {err}"))?; + let bar = info + .find_memory_bar(0) + .ok_or_else(|| format!("no memory BAR0 for {location}"))?; + let (addr, size) = bar + .memory_info() + .ok_or_else(|| format!("invalid BAR0 mapping info for {location}"))?; + let mmio = pci + .map_bar(0, addr, size) + .map_err(|err| format!("failed to map BAR0 for {location}: {err}"))?; + + let gp_before = mmio.read32(IWL_CSR_GP_CNTRL); + let access_req = if family.starts_with("intel-bz-") { + IWL_CSR_GP_CNTRL_REG_FLAG_BZ_MAC_ACCESS_REQ + } else { + IWL_CSR_GP_CNTRL_REG_FLAG_MAC_ACCESS_REQ + }; + mmio.write32(IWL_CSR_GP_CNTRL, gp_before | access_req); + if family.starts_with("intel-bz-") { + let gp_reset = mmio.read32(IWL_CSR_GP_CNTRL); + mmio.write32( + IWL_CSR_GP_CNTRL, + gp_reset | IWL_CSR_GP_CNTRL_REG_FLAG_SW_RESET_BZ, + ); + } else { + let reset_before = mmio.read32(IWL_CSR_RESET); + mmio.write32( + IWL_CSR_RESET, + reset_before | IWL_CSR_RESET_REG_FLAG_SW_RESET, + ); + } + let hw_if_before = mmio.read32(IWL_CSR_HW_IF_CONFIG_REG); + mmio.write32( + IWL_CSR_HW_IF_CONFIG_REG, + hw_if_before | IWL_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY, + ); + let hw_if_after = mmio.read32(IWL_CSR_HW_IF_CONFIG_REG); + let nic_ready = (hw_if_after & IWL_CSR_HW_IF_CONFIG_REG_BIT_NIC_READY) != 0; + let base = transport_status_after_prepare(config_path, family)?; + Ok(format!( + "{} init_hw_if_before=0x{hw_if_before:08x} init_hw_if_after=0x{hw_if_after:08x} nic_ready_write={} reset_method={}", + base, + if nic_ready { "yes" } else { "no" }, + if family.starts_with("intel-bz-") { "gp-cntrl-sw-reset" } else { "csr-reset-sw-reset" } + )) +} + +#[cfg(not(target_os = "redox"))] +fn init_transport_action(config_path: &PathBuf, family: &str) -> Result { + Ok(transport_status_after_prepare(config_path, family)?) +} + +#[cfg(target_os = "redox")] +fn activate_nic_action(config_path: &PathBuf) -> Result { + let location: PciLocation = parse_location_from_config_path(config_path)?.into(); + let mut pci = PciDevice::open_location(&location) + .map_err(|err| format!("failed to reopen PCI device {location}: {err}"))?; + let info = pci + .full_info() + .map_err(|err| format!("failed to read PCI info for {location}: {err}"))?; + let bar = info + .find_memory_bar(0) + .ok_or_else(|| format!("no memory BAR0 for {location}"))?; + let (addr, size) = bar + .memory_info() + .ok_or_else(|| format!("invalid BAR0 mapping info for {location}"))?; + let mmio = pci + .map_bar(0, addr, size) + .map_err(|err| format!("failed to map BAR0 for {location}: {err}"))?; + + let gp_before = mmio.read32(IWL_CSR_GP_CNTRL); + mmio.write32( + IWL_CSR_GP_CNTRL, + gp_before | IWL_CSR_GP_CNTRL_REG_FLAG_INIT_DONE, + ); + let gp_after = mmio.read32(IWL_CSR_GP_CNTRL); + let init_done = (gp_after & IWL_CSR_GP_CNTRL_REG_FLAG_INIT_DONE) != 0; + let mac_clock = (gp_after & IWL_CSR_GP_CNTRL_REG_FLAG_MAC_CLOCK_READY) != 0; + + Ok(format!( + "activation=ok init_done={} mac_clock_ready={}", + if init_done { "yes" } else { "no" }, + if mac_clock { "yes" } else { "no" } + )) +} + +#[cfg(not(target_os = "redox"))] +fn activate_nic_action(_config_path: &PathBuf) -> Result { + Ok("activation=host-skipped".to_string()) +} + +fn read_transport_status(config_path: &PathBuf) -> Result { + let config = fs::read(config_path) + .map_err(|err| format!("failed to read PCI config {}: {err}", config_path.display()))?; + if config.len() < 64 { + return Err(format!( + "PCI config too small at {}: expected at least 64 bytes", + config_path.display() + )); + } + Ok(transport_status_from_config(&config)) +} + +fn program_transport_bits(config_path: &PathBuf) -> Result<(), String> { + let mut file = OpenOptions::new() + .read(true) + .write(true) + .open(config_path) + .map_err(|err| format!("failed to open PCI config {}: {err}", config_path.display()))?; + + let mut command = [0u8; 2]; + file.seek(SeekFrom::Start(0x04)) + .map_err(|err| format!("failed to seek PCI command register: {err}"))?; + file.read_exact(&mut command) + .map_err(|err| format!("failed to read PCI command register: {err}"))?; + + let mut value = u16::from_le_bytes(command); + value |= 0x0002; // memory space + value |= 0x0004; // bus master + + file.seek(SeekFrom::Start(0x04)) + .map_err(|err| format!("failed to seek PCI command register for write: {err}"))?; + file.write_all(&value.to_le_bytes()) + .map_err(|err| format!("failed to write PCI command register: {err}"))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_root(prefix: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = env::temp_dir().join(format!("{prefix}-{stamp}")); + fs::create_dir_all(&path).unwrap(); + path + } + + #[test] + fn stub_backend_connects_with_wpa2() { + let mut backend = StubBackend::from_env(); + let state = InterfaceState { + ssid: "demo".to_string(), + security: "wpa2-psk".to_string(), + key: "secret".to_string(), + ..Default::default() + }; + assert_eq!( + backend.connect("wlan0", &state).unwrap(), + WifiStatus::Connected + ); + } + + #[test] + fn no_device_backend_exposes_no_interfaces() { + let backend = NoDeviceBackend::new(); + assert!(backend.interfaces().is_empty()); + assert_eq!(backend.initial_status("wlan0"), WifiStatus::Down); + assert_eq!(backend.initial_link_state("wlan0"), "link=no-device"); + assert!(backend + .capabilities() + .iter() + .any(|line| line == "backend=no-device")); + } + + #[test] + fn intel_backend_detects_wifi_controller() { + let _guard = TEST_ENV_LOCK.lock().unwrap(); + let pci = temp_root("rbos-wifictl-pci"); + let firmware = temp_root("rbos-wifictl-fw"); + let slot = pci.join("0000--00--14.3"); + fs::create_dir_all(&slot).unwrap(); + let mut cfg = vec![0u8; 64]; + cfg[0x00] = 0x86; + cfg[0x01] = 0x80; + cfg[0x02] = 0x40; + cfg[0x03] = 0x77; + cfg[0x0A] = 0x80; + cfg[0x0B] = 0x02; + cfg[0x10] = 0x01; + cfg[0x2E] = 0x90; + cfg[0x2F] = 0x40; + cfg[0x3D] = 0x01; + fs::write(slot.join("config"), cfg).unwrap(); + fs::write(firmware.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + + unsafe { + env::set_var("REDBEAR_WIFICTL_PCI_ROOT", &pci); + env::set_var("REDBEAR_WIFICTL_FIRMWARE_ROOT", &firmware); + env::remove_var("REDBEAR_IWLWIFI_CMD"); + } + fs::write(firmware.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + let mut backend = IntelBackend::from_env(); + assert_eq!(backend.interfaces(), vec!["wlan0".to_string()]); + assert_eq!(backend.initial_status("wlan0"), WifiStatus::DeviceDetected); + assert_eq!(backend.prepare("wlan0").unwrap(), WifiStatus::FirmwareReady); + assert!(backend + .capabilities() + .iter() + .any(|line| line.contains("ucode_selected=iwlwifi-bz-b0-gf-a0-92.ucode"))); + assert!(backend + .transport_status("wlan0") + .contains("memory_enabled=yes")); + assert!(backend.firmware_status("wlan0").contains("prepared=yes")); + assert!(backend + .transport_status("wlan0") + .contains("memory_enabled=yes")); + assert!(backend.transport_status("wlan0").contains("bus_master=yes")); + } + + #[test] + fn intel_backend_transport_probe_does_not_use_init_transport_action() { + let _guard = TEST_ENV_LOCK.lock().unwrap(); + let pci = temp_root("rbos-wifictl-pci-probe"); + let firmware = temp_root("rbos-wifictl-fw-probe"); + let slot = pci.join("0000--00--14.3"); + fs::create_dir_all(&slot).unwrap(); + let mut cfg = vec![0u8; 64]; + cfg[0x00] = 0x86; + cfg[0x01] = 0x80; + cfg[0x02] = 0x40; + cfg[0x03] = 0x77; + cfg[0x0A] = 0x80; + cfg[0x0B] = 0x02; + cfg[0x10] = 0x01; + cfg[0x2E] = 0x90; + cfg[0x2F] = 0x40; + cfg[0x3D] = 0x01; + fs::write(slot.join("config"), cfg).unwrap(); + fs::write(firmware.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(firmware.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let driver = temp_root("rbos-wifictl-driver").join("redbear-iwlwifi-mock.sh"); + fs::write( + &driver, + r##"#!/usr/bin/env bash +set -euo pipefail +case "${1:-}" in + --transport-probe) + printf 'transport_status=transport=mock-probe-only\n' + ;; + --init-transport) + printf 'transport_status=transport=mock-init-path\n' + ;; + *) + printf 'status=unexpected-action\n' + ;; +esac +"##, + ) + .unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&driver).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&driver, perms).unwrap(); + } + + let old_cmd = env::var_os("REDBEAR_IWLWIFI_CMD"); + unsafe { + env::set_var("REDBEAR_WIFICTL_PCI_ROOT", &pci); + env::set_var("REDBEAR_WIFICTL_FIRMWARE_ROOT", &firmware); + env::set_var("REDBEAR_IWLWIFI_CMD", &driver); + } + + let mut backend = IntelBackend::from_env(); + let transport_status = backend.transport_probe("wlan0").unwrap(); + assert_eq!(transport_status, "transport=mock-probe-only"); + + unsafe { + if let Some(old_cmd) = old_cmd { + env::set_var("REDBEAR_IWLWIFI_CMD", old_cmd); + } else { + env::remove_var("REDBEAR_IWLWIFI_CMD"); + } + } + } + + #[test] + fn intel_backend_connect_uses_driver_connect_action() { + let _guard = TEST_ENV_LOCK.lock().unwrap(); + let pci = temp_root("rbos-wifictl-pci-connect"); + let firmware = temp_root("rbos-wifictl-fw-connect"); + let slot = pci.join("0000--00--14.3"); + fs::create_dir_all(&slot).unwrap(); + let mut cfg = vec![0u8; 64]; + cfg[0x00] = 0x86; + cfg[0x01] = 0x80; + cfg[0x02] = 0x40; + cfg[0x03] = 0x77; + cfg[0x0A] = 0x80; + cfg[0x0B] = 0x02; + cfg[0x10] = 0x01; + cfg[0x2E] = 0x90; + cfg[0x2F] = 0x40; + cfg[0x3D] = 0x01; + fs::write(slot.join("config"), cfg).unwrap(); + fs::write(firmware.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(firmware.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let driver = temp_root("rbos-wifictl-driver-connect").join("redbear-iwlwifi-mock.sh"); + fs::write( + &driver, + r##"#!/usr/bin/env bash +set -euo pipefail +case "${1:-}" in + --prepare) + printf 'status=firmware-ready\n' + printf 'transport_status=transport=prepared\n' + ;; + --init-transport) + printf 'transport_status=transport=init\n' + ;; + --activate-nic) + printf 'activation=ok\n' + printf 'transport_status=transport=active\n' + ;; + --connect) + printf 'status=associated\n' + printf 'connect_result=mock-associated ssid=%s security=%s\n' "${3:-}" "${4:-}" + ;; + *) + printf 'status=unexpected-action\n' + ;; +esac +"##, + ) + .unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&driver).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&driver, perms).unwrap(); + } + + let old_cmd = env::var_os("REDBEAR_IWLWIFI_CMD"); + unsafe { + env::set_var("REDBEAR_WIFICTL_PCI_ROOT", &pci); + env::set_var("REDBEAR_WIFICTL_FIRMWARE_ROOT", &firmware); + env::set_var("REDBEAR_IWLWIFI_CMD", &driver); + } + + let mut backend = IntelBackend::from_env(); + assert_eq!(backend.prepare("wlan0").unwrap(), WifiStatus::FirmwareReady); + assert_eq!( + backend.init_transport("wlan0").unwrap(), + "transport_init=ok" + ); + assert_eq!(backend.activate("wlan0").unwrap(), "ok"); + + let state = InterfaceState { + ssid: "demo".to_string(), + security: "wpa2-psk".to_string(), + key: "secret".to_string(), + ..Default::default() + }; + assert_eq!( + backend.connect("wlan0", &state).unwrap(), + WifiStatus::Connected + ); + + unsafe { + if let Some(old_cmd) = old_cmd { + env::set_var("REDBEAR_IWLWIFI_CMD", old_cmd); + } else { + env::remove_var("REDBEAR_IWLWIFI_CMD"); + } + } + } +} diff --git a/local/recipes/system/redbear-wifictl/source/src/main.rs b/local/recipes/system/redbear-wifictl/source/src/main.rs new file mode 100644 index 00000000..88ac54e0 --- /dev/null +++ b/local/recipes/system/redbear-wifictl/source/src/main.rs @@ -0,0 +1,410 @@ +mod backend; +mod scheme; + +use std::env; +#[cfg(target_os = "redox")] +use std::os::fd::RawFd; +use std::path::Path; +use std::process; + +use backend::{Backend, IntelBackend, NoDeviceBackend, StubBackend}; +#[cfg(target_os = "redox")] +use log::info; +use log::LevelFilter; +#[cfg(target_os = "redox")] +use redox_scheme::{scheme::SchemeSync, SignalBehavior, Socket}; +#[cfg(target_os = "redox")] +use scheme::WifiCtlScheme; + +fn init_logging(level: LevelFilter) { + log::set_max_level(level); +} + +#[cfg(target_os = "redox")] +unsafe fn get_init_notify_fd() -> RawFd { + let fd: RawFd = env::var("INIT_NOTIFY") + .expect("redbear-wifictl: INIT_NOTIFY not set") + .parse() + .expect("redbear-wifictl: INIT_NOTIFY is not a valid fd"); + unsafe { + libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC); + } + fd +} + +#[cfg(target_os = "redox")] +fn notify_scheme_ready(notify_fd: RawFd, socket: &Socket, scheme: &mut WifiCtlScheme) { + let cap_id = scheme + .scheme_root() + .expect("redbear-wifictl: scheme_root failed"); + let cap_fd = socket + .create_this_scheme_fd(0, cap_id, 0, 0) + .expect("redbear-wifictl: create_this_scheme_fd failed"); + + syscall::call_wo( + notify_fd as usize, + &libredox::Fd::new(cap_fd).into_raw().to_ne_bytes(), + syscall::CallFlags::FD, + &[], + ) + .expect("redbear-wifictl: failed to notify init that scheme is ready"); +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum BackendMode { + Intel, + NoDevice, + Stub, +} + +fn iwlwifi_command_path() -> std::path::PathBuf { + env::var_os("REDBEAR_IWLWIFI_CMD") + .map(std::path::PathBuf::from) + .unwrap_or_else(|| std::path::PathBuf::from("/usr/lib/drivers/redbear-iwlwifi")) +} + +fn select_backend_mode( + explicit: Option<&str>, + intel_driver_present: bool, + intel_interfaces_present: bool, + redox_runtime: bool, +) -> BackendMode { + match explicit { + Some("intel") => BackendMode::Intel, + Some("stub") => BackendMode::Stub, + _ if redox_runtime && intel_driver_present && intel_interfaces_present => { + BackendMode::Intel + } + _ if redox_runtime && intel_driver_present => BackendMode::NoDevice, + _ => BackendMode::Stub, + } +} + +fn build_backend() -> Box { + let explicit = env::var("REDBEAR_WIFICTL_BACKEND").ok(); + let intel_driver_present = Path::new(&iwlwifi_command_path()).exists(); + let intel_interfaces_present = if cfg!(target_os = "redox") && intel_driver_present { + !IntelBackend::from_env().interfaces().is_empty() + } else { + false + }; + let mode = select_backend_mode( + explicit.as_deref(), + intel_driver_present, + intel_interfaces_present, + cfg!(target_os = "redox"), + ); + + match mode { + BackendMode::Intel => Box::new(IntelBackend::from_env()), + BackendMode::NoDevice => Box::new(NoDeviceBackend::new()), + BackendMode::Stub => Box::new(StubBackend::from_env()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_backend_selection_wins() { + assert_eq!( + select_backend_mode(Some("intel"), false, false, false), + BackendMode::Intel + ); + assert_eq!( + select_backend_mode(Some("stub"), true, true, true), + BackendMode::Stub + ); + } + + #[test] + fn redox_runtime_prefers_intel_when_driver_present() { + assert_eq!( + select_backend_mode(None, true, true, true), + BackendMode::Intel + ); + assert_eq!( + select_backend_mode(None, false, false, true), + BackendMode::Stub + ); + } + + #[test] + fn redox_runtime_uses_no_device_backend_without_detected_intel_interfaces() { + assert_eq!( + select_backend_mode(None, true, false, true), + BackendMode::NoDevice + ); + } + + #[test] + fn host_runtime_stays_stub_without_explicit_override() { + assert_eq!( + select_backend_mode(None, true, true, false), + BackendMode::Stub + ); + assert_eq!( + select_backend_mode(None, false, false, false), + BackendMode::Stub + ); + } +} + +fn main() { + let log_level = match env::var("REDBEAR_WIFICTL_LOG").as_deref() { + Ok("debug") => LevelFilter::Debug, + Ok("trace") => LevelFilter::Trace, + Ok("warn") => LevelFilter::Warn, + Ok("error") => LevelFilter::Error, + _ => LevelFilter::Info, + }; + init_logging(log_level); + + let mut args = env::args().skip(1); + match args.next().as_deref() { + Some("--probe") => { + let backend = build_backend(); + println!("interfaces={}", backend.interfaces().join(",")); + println!("capabilities={}", backend.capabilities().join(",")); + return; + } + Some("--prepare") => { + let iface = args.next().unwrap_or_else(|| "wlan0".to_string()); + let mut backend = build_backend(); + match backend.prepare(&iface) { + Ok(status) => { + println!("interface={}", iface); + println!("status={}", status.as_str()); + println!("firmware_status={}", backend.firmware_status(&iface)); + println!("transport_status={}", backend.transport_status(&iface)); + println!("transport_init_status=transport_init=not-run"); + return; + } + Err(err) => { + eprintln!("redbear-wifictl: prepare failed for {}: {}", iface, err); + process::exit(1); + } + } + } + Some("--status") => { + let iface = args.next().unwrap_or_else(|| "wlan0".to_string()); + let backend = build_backend(); + println!("interface={}", iface); + println!("status={}", backend.initial_status(&iface).as_str()); + println!("link_state={}", backend.initial_link_state(&iface)); + println!("firmware_status={}", backend.firmware_status(&iface)); + println!("transport_status={}", backend.transport_status(&iface)); + println!("transport_init_status=transport_init=unknown"); + println!("connect_result={}", backend.connect_result(&iface)); + return; + } + Some("--scan") => { + let iface = args.next().unwrap_or_else(|| "wlan0".to_string()); + let mut backend = build_backend(); + match backend.scan(&iface) { + Ok(results) => { + println!("interface={}", iface); + println!("status=scanning"); + println!("firmware_status={}", backend.firmware_status(&iface)); + println!("transport_status={}", backend.transport_status(&iface)); + println!("scan_results={}", results.join(",")); + return; + } + Err(err) => { + eprintln!("redbear-wifictl: scan failed for {}: {}", iface, err); + process::exit(1); + } + } + } + Some("--transport-probe") => { + let iface = args.next().unwrap_or_else(|| "wlan0".to_string()); + let mut backend = build_backend(); + match backend.transport_probe(&iface) { + Ok(status) => { + println!("interface={}", iface); + println!("transport_status={}", status); + return; + } + Err(err) => { + eprintln!( + "redbear-wifictl: transport probe failed for {}: {}", + iface, err + ); + process::exit(1); + } + } + } + Some("--init-transport") => { + let iface = args.next().unwrap_or_else(|| "wlan0".to_string()); + let mut backend = build_backend(); + match backend.init_transport(&iface) { + Ok(status) => { + println!("interface={}", iface); + println!("transport_init_status={}", status); + println!("transport_status={}", backend.transport_status(&iface)); + return; + } + Err(err) => { + eprintln!( + "redbear-wifictl: transport init failed for {}: {}", + iface, err + ); + process::exit(1); + } + } + } + Some("--activate-nic") => { + let iface = args.next().unwrap_or_else(|| "wlan0".to_string()); + let mut backend = build_backend(); + match backend.activate(&iface) { + Ok(status) => { + println!("interface={}", iface); + println!("activation_status={}", status); + println!("transport_status={}", backend.transport_status(&iface)); + return; + } + Err(err) => { + eprintln!( + "redbear-wifictl: activate-nic failed for {}: {}", + iface, err + ); + process::exit(1); + } + } + } + Some("--retry") => { + let iface = args.next().unwrap_or_else(|| "wlan0".to_string()); + let mut backend = build_backend(); + match backend.retry(&iface) { + Ok(status) => { + println!("interface={}", iface); + println!("status={}", status.as_str()); + println!("link_state=link=retrying"); + return; + } + Err(err) => { + eprintln!("redbear-wifictl: retry failed for {}: {}", iface, err); + process::exit(1); + } + } + } + Some("--connect") => { + let iface = args.next().unwrap_or_else(|| "wlan0".to_string()); + let ssid = args.next().unwrap_or_default(); + let security = args.next().unwrap_or_else(|| "open".to_string()); + let key = args.next().unwrap_or_default(); + let mut backend = build_backend(); + if let Err(err) = backend.prepare(&iface) { + eprintln!("redbear-wifictl: prepare failed for {}: {}", iface, err); + process::exit(1); + } + if let Err(err) = backend.init_transport(&iface) { + eprintln!( + "redbear-wifictl: transport init failed for {}: {}", + iface, err + ); + process::exit(1); + } + if let Err(err) = backend.activate(&iface) { + eprintln!( + "redbear-wifictl: activate-nic failed for {}: {}", + iface, err + ); + process::exit(1); + } + let state = backend::InterfaceState { + ssid, + security, + key, + ..Default::default() + }; + match backend.connect(&iface, &state) { + Ok(status) => { + println!("interface={}", iface); + println!("status={}", status.as_str()); + println!("firmware_status={}", backend.firmware_status(&iface)); + println!("transport_status={}", backend.transport_status(&iface)); + println!("connect_result={}", backend.connect_result(&iface)); + return; + } + Err(err) => { + eprintln!("redbear-wifictl: connect failed for {}: {}", iface, err); + process::exit(1); + } + } + } + Some("--disconnect") => { + let iface = args.next().unwrap_or_else(|| "wlan0".to_string()); + let mut backend = build_backend(); + if let Err(err) = backend.prepare(&iface) { + eprintln!("redbear-wifictl: prepare failed for {}: {}", iface, err); + process::exit(1); + } + if let Err(err) = backend.init_transport(&iface) { + eprintln!( + "redbear-wifictl: transport init failed for {}: {}", + iface, err + ); + process::exit(1); + } + if let Err(err) = backend.activate(&iface) { + eprintln!( + "redbear-wifictl: activate-nic failed for {}: {}", + iface, err + ); + process::exit(1); + } + match backend.disconnect(&iface) { + Ok(status) => { + println!("interface={}", iface); + println!("status={}", status.as_str()); + println!("firmware_status={}", backend.firmware_status(&iface)); + println!("transport_status={}", backend.transport_status(&iface)); + println!("disconnect_result={}", backend.disconnect_result(&iface)); + return; + } + Err(err) => { + eprintln!("redbear-wifictl: disconnect failed for {}: {}", iface, err); + process::exit(1); + } + } + } + _ => {} + } + + #[cfg(not(target_os = "redox"))] + { + eprintln!("redbear-wifictl: daemon mode is only supported on Redox; use --probe on host"); + process::exit(1); + } + + #[cfg(target_os = "redox")] + { + let notify_fd = unsafe { get_init_notify_fd() }; + let socket = Socket::create().expect("redbear-wifictl: failed to create scheme socket"); + let mut scheme = WifiCtlScheme::new(build_backend()); + let mut state = redox_scheme::scheme::SchemeState::new(); + + notify_scheme_ready(notify_fd, &socket, &mut scheme); + libredox::call::setrens(0, 0).expect("redbear-wifictl: failed to enter null namespace"); + info!("redbear-wifictl: registered scheme:wifictl"); + + while let Some(request) = socket + .next_request(SignalBehavior::Restart) + .expect("redbear-wifictl: failed to read scheme request") + { + match request.kind() { + redox_scheme::RequestKind::Call(request) => { + let response = request.handle_sync(&mut scheme, &mut state); + socket + .write_response(response, SignalBehavior::Restart) + .expect("redbear-wifictl: failed to write response"); + } + _ => {} + } + } + + process::exit(0); + } +} diff --git a/local/recipes/system/redbear-wifictl/source/src/scheme.rs b/local/recipes/system/redbear-wifictl/source/src/scheme.rs new file mode 100644 index 00000000..a77dc027 --- /dev/null +++ b/local/recipes/system/redbear-wifictl/source/src/scheme.rs @@ -0,0 +1,743 @@ +use std::collections::BTreeMap; + +use redox_scheme::scheme::SchemeSync; +use redox_scheme::{CallerCtx, OpenResult}; +use syscall::error::{Error, Result, EACCES, EBADF, EINVAL, ENOENT, EROFS}; +use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE}; +use syscall::schemev2::NewFdFlags; +use syscall::Stat; + +use crate::backend::{Backend, InterfaceState, WifiStatus}; + +const SCHEME_ROOT_ID: usize = 1; + +#[derive(Clone)] +enum HandleKind { + Root, + Ifaces, + Interface(String), + Capabilities, + Status(String), + LinkState(String), + FirmwareStatus(String), + TransportStatus(String), + TransportInitStatus(String), + ActivationStatus(String), + ConnectResult(String), + DisconnectResult(String), + ScanResults(String), + LastError(String), + Ssid(String), + Security(String), + Key(String), + Scan(String), + Prepare(String), + TransportProbe(String), + InitTransport(String), + ActivateNic(String), + Connect(String), + Disconnect(String), + Retry(String), +} + +pub struct WifiCtlScheme { + backend: Box, + next_id: usize, + handles: BTreeMap, + states: BTreeMap, +} + +impl WifiCtlScheme { + pub fn new(backend: Box) -> Self { + let mut states = BTreeMap::new(); + for iface in backend.interfaces() { + states.insert( + iface.clone(), + InterfaceState { + status: backend.initial_status(&iface).as_str().to_string(), + link_state: backend.initial_link_state(&iface), + firmware_status: backend.firmware_status(&iface), + transport_status: backend.transport_status(&iface), + transport_init_status: "transport_init=not-run".to_string(), + activation_status: "activation=not-run".to_string(), + connect_result: backend.connect_result(&iface), + disconnect_result: backend.disconnect_result(&iface), + scan_results: backend.default_scan_results(&iface), + ..Default::default() + }, + ); + } + + Self { + backend, + next_id: SCHEME_ROOT_ID + 1, + handles: BTreeMap::new(), + states, + } + } + + fn alloc_handle(&mut self, kind: HandleKind) -> usize { + let id = self.next_id; + self.next_id += 1; + self.handles.insert(id, kind); + id + } + + fn handle(&self, id: usize) -> Result<&HandleKind> { + self.handles.get(&id).ok_or(Error::new(EBADF)) + } + + fn state(&self, iface: &str) -> Result<&InterfaceState> { + self.states.get(iface).ok_or(Error::new(ENOENT)) + } + + fn state_mut(&mut self, iface: &str) -> Result<&mut InterfaceState> { + self.states.get_mut(iface).ok_or(Error::new(ENOENT)) + } + + fn read_handle(&self, kind: &HandleKind) -> Result { + Ok(match kind { + HandleKind::Root => "ifaces\ncapabilities\n".to_string(), + HandleKind::Ifaces => self.states.keys().cloned().collect::>().join("\n") + "\n", + HandleKind::Interface(_) => { + "status\nlink-state\nfirmware-status\ntransport-status\ntransport-init-status\nactivation-status\nconnect-result\ndisconnect-result\nscan-results\nlast-error\nssid\nsecurity\nkey\nscan\nprepare\ntransport-probe\ninit-transport\nactivate-nic\nconnect\ndisconnect\nretry\n" + .to_string() + } + HandleKind::Capabilities => self.backend.capabilities().join("\n") + "\n", + HandleKind::Status(iface) => { + let state = self.state(iface)?; + format!( + "status={}\nlink_state={}\nfirmware_status={}\ntransport_status={}\ntransport_init_status={}\nactivation_status={}\nconnect_result={}\ndisconnect_result={}\nssid={}\nsecurity={}\n", + state.status, + state.link_state, + state.firmware_status, + state.transport_status, + state.transport_init_status, + state.activation_status, + state.connect_result, + state.disconnect_result, + state.ssid, + state.security + ) + } + HandleKind::LinkState(iface) => format!("{}\n", self.state(iface)?.link_state), + HandleKind::FirmwareStatus(iface) => format!("{}\n", self.state(iface)?.firmware_status), + HandleKind::TransportStatus(iface) => { + format!("{}\n", self.state(iface)?.transport_status) + } + HandleKind::TransportInitStatus(iface) => { + format!("{}\n", self.state(iface)?.transport_init_status) + } + HandleKind::ActivationStatus(iface) => { + format!("{}\n", self.state(iface)?.activation_status) + } + HandleKind::ConnectResult(iface) => format!("{}\n", self.state(iface)?.connect_result), + HandleKind::DisconnectResult(iface) => { + format!("{}\n", self.state(iface)?.disconnect_result) + } + HandleKind::ScanResults(iface) => self.state(iface)?.scan_results.join("\n") + "\n", + HandleKind::LastError(iface) => format!("{}\n", self.state(iface)?.last_error), + HandleKind::Ssid(iface) => format!("{}\n", self.state(iface)?.ssid), + HandleKind::Security(iface) => format!("{}\n", self.state(iface)?.security), + HandleKind::Key(_iface) => "[redacted]\n".to_string(), + HandleKind::Scan(_) + | HandleKind::TransportProbe(_) + | HandleKind::InitTransport(_) + | HandleKind::ActivateNic(_) + | HandleKind::Retry(_) + | HandleKind::Prepare(_) + | HandleKind::Connect(_) + | HandleKind::Disconnect(_) => String::new(), + }) + } + + fn link_state_for_status(status: &WifiStatus) -> &'static str { + match status { + WifiStatus::Connected => "link=connected", + WifiStatus::Associating => "link=associating", + WifiStatus::Scanning => "link=scanning", + WifiStatus::FirmwareReady | WifiStatus::DeviceDetected => "link=down", + WifiStatus::Down => "link=down", + WifiStatus::Failed => "link=down", + } + } + + fn apply_connect_outcome( + &mut self, + iface: &str, + status: WifiStatus, + firmware_status: String, + transport_status: String, + connect_result: String, + disconnect_result: String, + ) -> Result<()> { + let state = self.state_mut(iface)?; + state.status = status.as_str().to_string(); + state.link_state = Self::link_state_for_status(&status).to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + state.connect_result = connect_result; + state.disconnect_result = disconnect_result; + Ok(()) + } +} + +impl SchemeSync for WifiCtlScheme { + fn scheme_root(&mut self) -> Result { + Ok(SCHEME_ROOT_ID) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + _flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let kind = if dirfd == SCHEME_ROOT_ID { + match path.trim_matches('/') { + "" => HandleKind::Root, + "ifaces" => HandleKind::Ifaces, + "capabilities" => HandleKind::Capabilities, + _ => return Err(Error::new(ENOENT)), + } + } else { + match self.handle(dirfd)? { + HandleKind::Ifaces => { + let iface = path.trim_matches('/'); + self.state(iface)?; + HandleKind::Interface(iface.to_string()) + } + HandleKind::Interface(iface) => match path.trim_matches('/') { + "status" => HandleKind::Status(iface.clone()), + "link-state" => HandleKind::LinkState(iface.clone()), + "firmware-status" => HandleKind::FirmwareStatus(iface.clone()), + "transport-status" => HandleKind::TransportStatus(iface.clone()), + "transport-init-status" => HandleKind::TransportInitStatus(iface.clone()), + "activation-status" => HandleKind::ActivationStatus(iface.clone()), + "connect-result" => HandleKind::ConnectResult(iface.clone()), + "disconnect-result" => HandleKind::DisconnectResult(iface.clone()), + "scan-results" => HandleKind::ScanResults(iface.clone()), + "last-error" => HandleKind::LastError(iface.clone()), + "ssid" => HandleKind::Ssid(iface.clone()), + "security" => HandleKind::Security(iface.clone()), + "key" => HandleKind::Key(iface.clone()), + "scan" => HandleKind::Scan(iface.clone()), + "prepare" => HandleKind::Prepare(iface.clone()), + "transport-probe" => HandleKind::TransportProbe(iface.clone()), + "init-transport" => HandleKind::InitTransport(iface.clone()), + "activate-nic" => HandleKind::ActivateNic(iface.clone()), + "connect" => HandleKind::Connect(iface.clone()), + "disconnect" => HandleKind::Disconnect(iface.clone()), + "retry" => HandleKind::Retry(iface.clone()), + _ => return Err(Error::new(ENOENT)), + }, + _ => return Err(Error::new(EACCES)), + } + }; + + Ok(OpenResult::ThisScheme { + number: self.alloc_handle(kind), + flags: NewFdFlags::empty(), + }) + } + + fn read( + &mut self, + id: usize, + buf: &mut [u8], + offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let data = self.read_handle(self.handle(id)?)?; + let bytes = data.as_bytes(); + let offset = usize::try_from(offset).map_err(|_| Error::new(EINVAL))?; + if offset >= bytes.len() { + return Ok(0); + } + let count = (bytes.len() - offset).min(buf.len()); + buf[..count].copy_from_slice(&bytes[offset..offset + count]); + Ok(count) + } + + fn write( + &mut self, + id: usize, + buf: &[u8], + _offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let value = std::str::from_utf8(buf) + .map_err(|_| Error::new(EINVAL))? + .trim() + .to_string(); + + match self.handle(id)?.clone() { + HandleKind::Ssid(iface) => self.state_mut(&iface)?.ssid = value, + HandleKind::Security(iface) => self.state_mut(&iface)?.security = value, + HandleKind::Key(iface) => self.state_mut(&iface)?.key = value, + HandleKind::Scan(iface) => { + let results = match self.backend.scan(&iface) { + Ok(results) => results, + Err(err) => { + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let state = self.state_mut(&iface)?; + state.last_error = err; + state.status = WifiStatus::Failed.as_str().to_string(); + state.link_state = "link=down".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + return Ok(buf.len()); + } + }; + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let state = self.state_mut(&iface)?; + state.status = WifiStatus::Scanning.as_str().to_string(); + state.link_state = "link=scanning".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + state.scan_results = results; + state.last_error.clear(); + } + HandleKind::Prepare(iface) => { + let status = match self.backend.prepare(&iface) { + Ok(status) => status, + Err(err) => { + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let state = self.state_mut(&iface)?; + state.last_error = err; + state.status = WifiStatus::Failed.as_str().to_string(); + state.link_state = "link=down".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + return Ok(buf.len()); + } + }; + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let state = self.state_mut(&iface)?; + state.status = status.as_str().to_string(); + state.link_state = "link=prepared".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + state.transport_init_status = "transport_init=not-run".to_string(); + } + HandleKind::TransportProbe(iface) => { + let transport_status = match self.backend.transport_probe(&iface) { + Ok(status) => status, + Err(err) => { + let firmware_status = self.backend.firmware_status(&iface); + let state = self.state_mut(&iface)?; + state.last_error = err; + state.status = WifiStatus::Failed.as_str().to_string(); + state.link_state = "link=down".to_string(); + state.firmware_status = firmware_status; + return Ok(buf.len()); + } + }; + let state = self.state_mut(&iface)?; + state.transport_status = transport_status; + } + HandleKind::InitTransport(iface) => { + let transport_init_status = match self.backend.init_transport(&iface) { + Ok(status) => status, + Err(err) => { + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let state = self.state_mut(&iface)?; + state.last_error = err; + state.status = WifiStatus::Failed.as_str().to_string(); + state.link_state = "link=down".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + state.transport_init_status = "transport_init=failed".to_string(); + return Ok(buf.len()); + } + }; + let state = self.state_mut(&iface)?; + state.transport_init_status = transport_init_status; + state.link_state = "link=transport-initialized".to_string(); + state.activation_status = "activation=not-run".to_string(); + } + HandleKind::ActivateNic(iface) => { + let activation_status = match self.backend.activate(&iface) { + Ok(status) => status, + Err(err) => { + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let state = self.state_mut(&iface)?; + state.last_error = err; + state.status = WifiStatus::Failed.as_str().to_string(); + state.link_state = "link=down".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + state.activation_status = "activation=failed".to_string(); + return Ok(buf.len()); + } + }; + let connect_result = self.backend.connect_result(&iface); + let disconnect_result = self.backend.disconnect_result(&iface); + let state = self.state_mut(&iface)?; + state.activation_status = activation_status; + state.link_state = "link=nic-active".to_string(); + state.connect_result = connect_result; + state.disconnect_result = disconnect_result; + } + HandleKind::Connect(iface) => { + let snapshot = self.state(&iface)?.clone(); + let new_status = match self.backend.connect(&iface, &snapshot) { + Ok(status) => status, + Err(err) => { + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let state = self.state_mut(&iface)?; + state.last_error = err; + state.status = WifiStatus::Failed.as_str().to_string(); + state.link_state = "link=down".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + state.transport_init_status = "transport_init=failed".to_string(); + state.activation_status = "activation=failed".to_string(); + return Ok(buf.len()); + } + }; + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let connect_result = self.backend.connect_result(&iface); + let disconnect_result = self.backend.disconnect_result(&iface); + self.apply_connect_outcome( + &iface, + new_status, + firmware_status, + transport_status, + connect_result, + disconnect_result, + )?; + } + HandleKind::Disconnect(iface) => { + let status = match self.backend.disconnect(&iface) { + Ok(status) => status, + Err(err) => { + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let state = self.state_mut(&iface)?; + state.last_error = err; + state.status = WifiStatus::Failed.as_str().to_string(); + state.link_state = "link=down".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + state.activation_status = "activation=failed".to_string(); + return Ok(buf.len()); + } + }; + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let disconnect_result = self.backend.disconnect_result(&iface); + let state = self.state_mut(&iface)?; + state.status = status.as_str().to_string(); + state.link_state = "link=down".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + state.disconnect_result = disconnect_result; + } + HandleKind::Retry(iface) => { + let status = match self.backend.retry(&iface) { + Ok(status) => status, + Err(err) => { + let firmware_status = self.backend.firmware_status(&iface); + let transport_status = self.backend.transport_status(&iface); + let state = self.state_mut(&iface)?; + state.last_error = err; + state.status = WifiStatus::Failed.as_str().to_string(); + state.link_state = "link=retry-failed".to_string(); + state.firmware_status = firmware_status; + state.transport_status = transport_status; + state.activation_status = "activation=failed".to_string(); + return Ok(buf.len()); + } + }; + let state = self.state_mut(&iface)?; + state.status = status.as_str().to_string(); + state.link_state = "link=retrying".to_string(); + } + _ => return Err(Error::new(EROFS)), + } + + Ok(buf.len()) + } + + fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> { + let kind = self.handle(id)?; + stat.st_mode = match kind { + HandleKind::Root | HandleKind::Ifaces | HandleKind::Interface(_) => MODE_DIR | 0o755, + HandleKind::Connect(_) + | HandleKind::Disconnect(_) + | HandleKind::Scan(_) + | HandleKind::TransportProbe(_) + | HandleKind::InitTransport(_) + | HandleKind::Retry(_) + | HandleKind::Prepare(_) + | HandleKind::Ssid(_) + | HandleKind::Security(_) + | HandleKind::Key(_) => MODE_FILE | 0o644, + _ => MODE_FILE | 0o444, + }; + Ok(()) + } + + fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> { + let _ = self.handle(id)?; + Ok(()) + } + + fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result { + let path = match self.handle(id)? { + HandleKind::Root => "wifictl:/".to_string(), + HandleKind::Ifaces => "wifictl:/ifaces".to_string(), + HandleKind::Interface(iface) => format!("wifictl:/ifaces/{iface}"), + HandleKind::Capabilities => "wifictl:/capabilities".to_string(), + HandleKind::Status(iface) => format!("wifictl:/ifaces/{iface}/status"), + HandleKind::LinkState(iface) => format!("wifictl:/ifaces/{iface}/link-state"), + HandleKind::FirmwareStatus(iface) => format!("wifictl:/ifaces/{iface}/firmware-status"), + HandleKind::TransportStatus(iface) => { + format!("wifictl:/ifaces/{iface}/transport-status") + } + HandleKind::TransportInitStatus(iface) => { + format!("wifictl:/ifaces/{iface}/transport-init-status") + } + HandleKind::ActivationStatus(iface) => { + format!("wifictl:/ifaces/{iface}/activation-status") + } + HandleKind::ConnectResult(iface) => format!("wifictl:/ifaces/{iface}/connect-result"), + HandleKind::DisconnectResult(iface) => { + format!("wifictl:/ifaces/{iface}/disconnect-result") + } + HandleKind::ScanResults(iface) => format!("wifictl:/ifaces/{iface}/scan-results"), + HandleKind::LastError(iface) => format!("wifictl:/ifaces/{iface}/last-error"), + HandleKind::Ssid(iface) => format!("wifictl:/ifaces/{iface}/ssid"), + HandleKind::Security(iface) => format!("wifictl:/ifaces/{iface}/security"), + HandleKind::Key(iface) => format!("wifictl:/ifaces/{iface}/key"), + HandleKind::Scan(iface) => format!("wifictl:/ifaces/{iface}/scan"), + HandleKind::Prepare(iface) => format!("wifictl:/ifaces/{iface}/prepare"), + HandleKind::TransportProbe(iface) => format!("wifictl:/ifaces/{iface}/transport-probe"), + HandleKind::InitTransport(iface) => format!("wifictl:/ifaces/{iface}/init-transport"), + HandleKind::ActivateNic(iface) => format!("wifictl:/ifaces/{iface}/activate-nic"), + HandleKind::Connect(iface) => format!("wifictl:/ifaces/{iface}/connect"), + HandleKind::Disconnect(iface) => format!("wifictl:/ifaces/{iface}/disconnect"), + HandleKind::Retry(iface) => format!("wifictl:/ifaces/{iface}/retry"), + }; + let bytes = path.as_bytes(); + let count = bytes.len().min(buf.len()); + buf[..count].copy_from_slice(&bytes[..count]); + Ok(count) + } + + fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result { + let _ = self.handle(id)?; + Ok(EventFlags::empty()) + } + + fn on_close(&mut self, id: usize) { + if id != SCHEME_ROOT_ID { + self.handles.remove(&id); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::{IntelBackend, StubBackend, TEST_ENV_LOCK}; + use std::env; + use std::fs; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_root(prefix: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = env::temp_dir().join(format!("{prefix}-{stamp}")); + fs::create_dir_all(&path).unwrap(); + path + } + + #[test] + fn status_updates_after_connect_and_disconnect() { + let mut scheme = WifiCtlScheme::new(Box::new(StubBackend::from_env())); + let iface = "wlan0".to_string(); + + { + let state = scheme.state_mut(&iface).unwrap(); + state.ssid = "demo-ssid".to_string(); + state.security = "wpa2-psk".to_string(); + state.key = "secret".to_string(); + } + + let snapshot = scheme.state(&iface).unwrap().clone(); + let status = scheme.backend.connect(&iface, &snapshot).unwrap(); + scheme + .apply_connect_outcome( + &iface, + status, + scheme.backend.firmware_status(&iface), + scheme.backend.transport_status(&iface), + scheme.backend.connect_result(&iface), + scheme.backend.disconnect_result(&iface), + ) + .unwrap(); + assert_eq!(scheme.state(&iface).unwrap().status, "connected"); + assert_eq!(scheme.state(&iface).unwrap().link_state, "link=connected"); + + let status = scheme.backend.disconnect(&iface).unwrap(); + scheme.state_mut(&iface).unwrap().status = status.as_str().to_string(); + assert_eq!(scheme.state(&iface).unwrap().status, "device-detected"); + } + + #[test] + fn apply_connect_outcome_preserves_pending_link_state() { + let mut scheme = WifiCtlScheme::new(Box::new(StubBackend::from_env())); + let iface = "wlan0".to_string(); + + scheme + .apply_connect_outcome( + &iface, + WifiStatus::Associating, + "firmware=present".to_string(), + "transport=active".to_string(), + "connect_result=host-bounded-pending ssid=demo security=wpa2-psk".to_string(), + "disconnect_result=not-run".to_string(), + ) + .unwrap(); + + let state = scheme.state(&iface).unwrap(); + assert_eq!(state.status, "associating"); + assert_eq!(state.link_state, "link=associating"); + assert!(state.connect_result.contains("host-bounded-pending")); + } + + #[test] + fn stub_prepare_marks_firmware_ready() { + let mut scheme = WifiCtlScheme::new(Box::new(StubBackend::from_env())); + let iface = "wlan0".to_string(); + + let status = scheme.backend.prepare(&iface).unwrap(); + let firmware_status = scheme.backend.firmware_status(&iface); + let state = scheme.state_mut(&iface).unwrap(); + state.status = status.as_str().to_string(); + state.firmware_status = firmware_status; + + assert_eq!(scheme.state(&iface).unwrap().status, "firmware-ready"); + assert_eq!( + scheme.state(&iface).unwrap().firmware_status, + "firmware=stub" + ); + assert_eq!( + scheme.state(&iface).unwrap().transport_status, + "transport=stub" + ); + assert_eq!( + scheme.state(&iface).unwrap().transport_init_status, + "transport_init=not-run" + ); + } + + #[test] + fn stub_scan_updates_scan_results() { + let mut scheme = WifiCtlScheme::new(Box::new(StubBackend::from_env())); + let iface = "wlan0".to_string(); + + let results = scheme.backend.scan(&iface).unwrap(); + let state = scheme.state_mut(&iface).unwrap(); + state.status = WifiStatus::Scanning.as_str().to_string(); + state.scan_results = results; + + assert_eq!(scheme.state(&iface).unwrap().status, "scanning"); + assert_eq!( + scheme.state(&iface).unwrap().scan_results, + vec!["demo-ssid".to_string(), "demo-open".to_string()] + ); + } + + #[test] + fn intel_prepare_failure_records_last_error() { + let _guard = TEST_ENV_LOCK.lock().unwrap(); + let pci = temp_root("rbos-wifictl-pci-missing"); + let firmware = temp_root("rbos-wifictl-fw-missing"); + let slot = pci.join("0000--00--14.3"); + fs::create_dir_all(&slot).unwrap(); + let mut cfg = vec![0u8; 64]; + cfg[0x00] = 0x86; + cfg[0x01] = 0x80; + cfg[0x02] = 0x40; + cfg[0x03] = 0x77; + cfg[0x0A] = 0x80; + cfg[0x0B] = 0x02; + cfg[0x04] = 0x06; + cfg[0x10] = 0x01; + cfg[0x2E] = 0x90; + cfg[0x2F] = 0x40; + cfg[0x3D] = 0x01; + fs::write(slot.join("config"), cfg).unwrap(); + + unsafe { + env::set_var("REDBEAR_WIFICTL_PCI_ROOT", &pci); + env::set_var("REDBEAR_WIFICTL_FIRMWARE_ROOT", &firmware); + env::remove_var("REDBEAR_IWLWIFI_CMD"); + } + + let mut scheme = WifiCtlScheme::new(Box::new(IntelBackend::from_env())); + let iface = "wlan0".to_string(); + + let err = scheme.backend.prepare(&iface).unwrap_err(); + let firmware_status = scheme.backend.firmware_status(&iface); + let state = scheme.state_mut(&iface).unwrap(); + state.last_error = err.clone(); + state.status = WifiStatus::Failed.as_str().to_string(); + state.firmware_status = firmware_status; + + assert!(scheme + .state(&iface) + .unwrap() + .last_error + .contains("missing firmware")); + assert_eq!(scheme.state(&iface).unwrap().status, "failed"); + assert!(scheme + .state(&iface) + .unwrap() + .firmware_status + .contains("firmware=missing")); + } + + #[test] + fn stub_transport_probe_updates_transport_status() { + let mut scheme = WifiCtlScheme::new(Box::new(StubBackend::from_env())); + let iface = "wlan0".to_string(); + let transport_status = scheme.backend.transport_probe(&iface).unwrap(); + scheme.state_mut(&iface).unwrap().transport_status = transport_status; + assert!(scheme + .state(&iface) + .unwrap() + .transport_status + .contains("mmio_probe=host-skipped")); + } + + #[test] + fn stub_init_transport_records_state() { + let mut scheme = WifiCtlScheme::new(Box::new(StubBackend::from_env())); + let iface = "wlan0".to_string(); + let status = scheme.backend.init_transport(&iface).unwrap(); + scheme.state_mut(&iface).unwrap().transport_init_status = status; + assert_eq!( + scheme.state(&iface).unwrap().transport_init_status, + "transport_init=stub" + ); + } +} diff --git a/local/recipes/system/redbear-wifictl/source/tests/cli_transport.rs b/local/recipes/system/redbear-wifictl/source/tests/cli_transport.rs new file mode 100644 index 00000000..fe580a5d --- /dev/null +++ b/local/recipes/system/redbear-wifictl/source/tests/cli_transport.rs @@ -0,0 +1,184 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn temp_root(prefix: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("{prefix}-{stamp}")); + fs::create_dir_all(&path).unwrap(); + path +} + +fn write_intel_candidate(pci_root: &PathBuf) { + let slot = pci_root.join("0000--00--14.3"); + fs::create_dir_all(&slot).unwrap(); + let mut cfg = vec![0u8; 64]; + cfg[0x00] = 0x86; + cfg[0x01] = 0x80; + cfg[0x02] = 0x40; + cfg[0x03] = 0x77; + cfg[0x0A] = 0x80; + cfg[0x0B] = 0x02; + cfg[0x10] = 0x01; + cfg[0x2E] = 0x90; + cfg[0x2F] = 0x40; + cfg[0x3D] = 0x01; + fs::write(slot.join("config"), cfg).unwrap(); +} + +fn write_mock_driver(path: &PathBuf) { + fs::write( + path, + r##"#!/usr/bin/env bash +set -euo pipefail +case "${1:-}" in + --transport-probe) + printf 'transport_status=transport=cli-probe-path\n' + ;; + --init-transport) + printf 'transport_init_status=transport_init=cli-init-path\n' + printf 'transport_status=transport=cli-init-path\n' + ;; + --prepare) + printf 'status=firmware-ready\n' + printf 'transport_status=transport=prepared\n' + ;; + --activate-nic) + printf 'activation=ok\n' + printf 'transport_status=transport=active\n' + ;; + --connect) + printf 'status=associated\n' + printf 'connect_result=cli-associated ssid=%s security=%s\n' "${3:-}" "${4:-}" + ;; + --disconnect) + printf 'status=device-detected\n' + printf 'disconnect_result=cli-disconnected\n' + ;; + *) + printf 'status=unexpected-action\n' + ;; +esac +"##, + ) + .unwrap(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(path, perms).unwrap(); + } +} + +fn run_wifictl(args: &[&str], pci_root: &PathBuf, fw_root: &PathBuf, driver: &PathBuf) -> String { + let output = Command::new(env!("CARGO_BIN_EXE_redbear-wifictl")) + .args(args) + .env("REDBEAR_WIFICTL_BACKEND", "intel") + .env("REDBEAR_WIFICTL_PCI_ROOT", pci_root) + .env("REDBEAR_WIFICTL_FIRMWARE_ROOT", fw_root) + .env("REDBEAR_IWLWIFI_CMD", driver) + .output() + .unwrap(); + + assert!( + output.status.success(), + "command {:?} failed: {}", + args, + String::from_utf8_lossy(&output.stderr) + ); + + String::from_utf8(output.stdout).unwrap() +} + +#[test] +fn cli_transport_probe_uses_probe_path() { + let pci = temp_root("rbos-wifictl-cli-pci"); + let fw = temp_root("rbos-wifictl-cli-fw"); + write_intel_candidate(&pci); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let driver = temp_root("rbos-wifictl-cli-driver").join("redbear-iwlwifi-mock.sh"); + write_mock_driver(&driver); + + let probe = run_wifictl(&["--transport-probe", "wlan0"], &pci, &fw, &driver); + assert!(probe.contains("transport_status=transport=cli-probe-path")); + assert!(!probe.contains("cli-init-path")); +} + +#[test] +fn cli_connect_reports_driver_status_honestly() { + let pci = temp_root("rbos-wifictl-cli-pci-connect"); + let fw = temp_root("rbos-wifictl-cli-fw-connect"); + write_intel_candidate(&pci); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0-92.ucode"), []).unwrap(); + fs::write(fw.join("iwlwifi-bz-b0-gf-a0.pnvm"), []).unwrap(); + + let driver = temp_root("rbos-wifictl-cli-driver-connect").join("redbear-iwlwifi-mock.sh"); + write_mock_driver(&driver); + + let connect = run_wifictl( + &["--connect", "wlan0", "demo", "wpa2-psk", "secret"], + &pci, + &fw, + &driver, + ); + assert!(connect.contains("status=connected")); + assert!(connect.contains("transport_status=transport=active")); + + let pending_driver = + temp_root("rbos-wifictl-cli-driver-pending").join("redbear-iwlwifi-pending.sh"); + fs::write( + &pending_driver, + r##"#!/usr/bin/env bash +set -euo pipefail +case "${1:-}" in + --prepare) + printf 'status=firmware-ready\n' + ;; + --activate-nic) + printf 'activation=ok\n' + ;; + --connect) + printf 'status=associating\n' + printf 'connect_result=host-bounded-pending ssid=%s security=%s\n' "${3:-}" "${4:-}" + ;; + --disconnect) + printf 'status=device-detected\n' + printf 'disconnect_result=cli-disconnected\n' + ;; + *) + printf 'status=device-detected\n' + ;; +esac +"##, + ) + .unwrap(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&pending_driver).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&pending_driver, perms).unwrap(); + } + + let pending = run_wifictl( + &["--connect", "wlan0", "demo", "wpa2-psk", "secret"], + &pci, + &fw, + &pending_driver, + ); + assert!(pending.contains("status=associating")); + assert!(pending.contains("connect_result=host-bounded-pending")); + + let disconnect = run_wifictl(&["--disconnect", "wlan0"], &pci, &fw, &driver); + assert!(disconnect.contains("status=device-detected")); + assert!(disconnect.contains("disconnect_result=cli-disconnected")); +} diff --git a/recipes/drivers/redbear-iwlwifi b/recipes/drivers/redbear-iwlwifi new file mode 120000 index 00000000..406adbf1 --- /dev/null +++ b/recipes/drivers/redbear-iwlwifi @@ -0,0 +1 @@ +../../local/recipes/drivers/redbear-iwlwifi \ No newline at end of file diff --git a/recipes/system/redbear-wifictl b/recipes/system/redbear-wifictl new file mode 120000 index 00000000..63cd8b9c --- /dev/null +++ b/recipes/system/redbear-wifictl @@ -0,0 +1 @@ +../../local/recipes/system/redbear-wifictl \ No newline at end of file