cadmus_core/device/wifi/kobo/
mod.rs

1//! WiFi management for Kobo devices.
2//!
3//! This module provides native WiFi lifecycle management, replacing the previous
4//! shell script-based implementation at `scripts/wifi-enable.sh` and
5//! `scripts/wifi-disable.sh`.
6//!
7//! # Architecture
8//!
9//! The implementation follows the same patterns as KOReader, with some
10//! key differences documented below.
11//!
12//! ## Power Toggle Strategies
13//!
14//! Different Kobo devices use different mechanisms to power the WiFi chip:
15//!
16//! - **`Module`**: Insmod `sdio_wifi_pwr.ko` - most older devices
17//! - **`NtxIo`**: ioctl on `/dev/ntx_io` with command 208 - devices with `moal` module
18//! - **`Wmt`**: WMT character device - devices with `wlan_drv_gen4m` module
19//!
20//! ## Module Path Resolution
21//!
22//! The module path depends on the WiFi module type:
23//! - Default: `/drivers/{PLATFORM}/wifi/`
24//! - `wlan_drv_gen4m`: `/drivers/{PLATFORM}/mt66xx/`
25//!
26//! ## Why No DHCP Client?
27//!
28//! This implementation does NOT run a DHCP client.
29//!
30//! The rationale is:
31//!
32//! 1. **Nickel's `dhcpcd` persists**: When Cadmus starts, it does NOT kill Nickel's
33//!    `dhcpcd -d -z wlan0` daemon. This daemon continuously manages the DHCP lease
34//!    and writes lease files to `/var/db/` on eMMC, which persists across reboots.
35//! 2. **Lease stability**: When reconnecting, `dhcpcd` reads the matching lease file
36//!    and requests the same IP (DHCP Option 50), avoiding the DHCP server handing
37//!    out a different address.
38//! 3. **Avoid `udhcpc` pitfalls**: The original shell script used `udhcpc -q` which
39//!    exits after obtaining a lease with no persistence mechanism, causing a new IP
40//!    on every toggle.
41//! 4. **wpa_supplicant handles 802.11 connectivity**: Network configuration is
42//!    managed by the platform's `dhcpcd`.
43//!
44//! ## Features Copied from KOReader
45//!
46//! The following improvements were adopted from KOReader's implementation:
47//!
48//! 1. **No DHCP client**: KOReader omits udhcpc when possible.
49//! 2. **Module loading**: Uses `insmod_asneeded()` helper that checks
50//!    `/proc/modules` before loading to avoid duplicate module loads
51//! 3. **250ms delay after insmod**: Matches KOReader's timing for module stabilization
52//! 4. **File descriptor cleanup**: Not implemented (KOReader closes non-standard fds
53//!    before WiFi operations to avoid issues with USBMS)
54//!
55//! ## Country Code Handling
56//!
57//! Regulatory domain is read from `/mnt/onboard/.kobo/Kobo/Kobo eReader.conf`
58//! under the `WifiRegulatoryDomain` key. The code parameter is passed to the
59//! kernel module as:
60//! - `8821cs`: `rtw_country_code=XX`
61//! - `moal`: `reg_alpha2=XX`
62//!
63//! ## Moal-Specific Module Parameters
64//!
65//! The `moal` module (NXP/Marvell 88W8987) additionally requires:
66//! - `mod_para=nxp/wifi_mod_para_sd8987.conf`
67//! - Loading `mlan.ko` dependency before the main module
68
69mod types;
70
71#[cfg(target_os = "linux")]
72use procfs;
73
74use crate::device::wifi::error::WifiError;
75use crate::device::wifi::kobo::types::{PowerToggle, WifiModule, WifiModuleConfig};
76use crate::device::wifi::manager::WifiManager;
77use nix::ioctl_write_int_bad;
78use std::fs;
79use std::os::fd::AsRawFd;
80use std::path::Path;
81use std::process::Command;
82use std::sync::Mutex;
83use tracing::{debug, error, info, warn};
84
85const DRIVERS_DIR: &str = "/drivers";
86const NTX_IO_PATH: &str = "/dev/ntx_io";
87const WMT_WIFI_PATH: &str = "/dev/wmtWifi";
88const CONFIG_PATH: &str = "/mnt/onboard/.kobo/Kobo/Kobo eReader.conf";
89const WPA_SUPPLICANT_CONF: &str = "/etc/wpa_supplicant/wpa_supplicant.conf";
90const WPA_SUPPLICANT_SOCKET: &str = "/var/run/wpa_supplicant";
91const WIFI_POST_UP_SCRIPT: &str = "scripts/wifi-post-up.sh";
92const WIFI_POST_DOWN_SCRIPT: &str = "scripts/wifi-post-down.sh";
93const WIFI_PRE_UP_SCRIPT: &str = "scripts/wifi-pre-up.sh";
94const WIFI_PRE_DOWN_SCRIPT: &str = "scripts/wifi-pre-down.sh";
95
96const NTX_IO_WIFI_CTRL: u8 = 208;
97ioctl_write_int_bad!(set_ntx_io_wifi_ctrl, NTX_IO_WIFI_CTRL as libc::c_int);
98
99#[cfg(target_os = "linux")]
100#[cfg_attr(feature = "otel", tracing::instrument(skip_all, ret(level=tracing::Level::TRACE)))]
101fn is_module_loaded(module_name: &str) -> bool {
102    procfs::modules()
103        .map(|modules| modules.iter().any(|(name, _)| name == module_name))
104        .unwrap_or(false)
105}
106
107#[cfg(not(target_os = "linux"))]
108#[cfg_attr(feature = "otel", tracing::instrument(skip_all, ret(level=tracing::Level::TRACE)))]
109fn is_module_loaded(_module_name: &str) -> bool {
110    unreachable!("is_module_loaded is only implemented on Linux")
111}
112
113#[cfg(target_os = "linux")]
114fn is_interface_up(interface: &str) -> bool {
115    let operstate_path = format!("/sys/class/net/{}/operstate", interface);
116    if let Ok(state) = fs::read_to_string(&operstate_path) {
117        let state = state.trim();
118        return state == "up" || state == "unknown";
119    }
120    false
121}
122
123#[cfg(not(target_os = "linux"))]
124fn is_interface_up(_interface: &str) -> bool {
125    unreachable!("is_interface_up is only implemented on Linux")
126}
127
128pub struct KoboWifiManager {
129    config: WifiModuleConfig,
130    lock: Mutex<()>,
131}
132
133impl KoboWifiManager {
134    pub fn new(config: WifiModuleConfig) -> Self {
135        Self {
136            config,
137            lock: Mutex::new(()),
138        }
139    }
140
141    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
142    fn run_script(&self, script: &str) {
143        if Path::new(script).exists() {
144            let output = Command::new(script).output();
145            if let Ok(output) = output {
146                if !output.status.success() {
147                    warn!(
148                        script,
149                        stderr = %String::from_utf8_lossy(&output.stderr),
150                        "WiFi script failed"
151                    );
152                } else {
153                    debug!(script, "WiFi script succeeded");
154                }
155            }
156        }
157    }
158
159    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(path = %path), ret(level=tracing::Level::TRACE)))]
160    fn insmod(&self, path: &str) -> Result<(), WifiError> {
161        let output = Command::new("insmod").arg(path).output().map_err(|e| {
162            error!(error = %e, path, "Failed to execute insmod");
163            WifiError::KernelModule(format!("insmod execution failed: {}", e))
164        })?;
165
166        if !output.status.success() {
167            let stderr = String::from_utf8_lossy(&output.stderr);
168            error!(path, stderr = %stderr, "insmod failed");
169            return Err(WifiError::KernelModule(format!(
170                "Failed to load module {}: {}",
171                path, stderr
172            )));
173        }
174
175        debug!(path, "Module loaded successfully");
176        Ok(())
177    }
178
179    /// Loads a kernel module if it is not already loaded.
180    ///
181    /// This function is idempotent: if the module is already loaded, this returns `Ok(())`
182    /// without attempting to reload it.
183    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(path = %path, module_name = %module_name), ret(level=tracing::Level::TRACE)))]
184    fn insmod_asneeded(&self, path: &str, module_name: &str) -> Result<(), WifiError> {
185        if !is_module_loaded(module_name) {
186            match self.insmod(path) {
187                Ok(()) => {
188                    std::thread::sleep(std::time::Duration::from_millis(250));
189                }
190                Err(WifiError::KernelModule(ref msg)) if msg.contains("File exists") => {
191                    debug!(
192                        module_name,
193                        "Module already loaded (insmod returned File exists)"
194                    );
195                }
196                Err(e) => return Err(e),
197            }
198        } else {
199            debug!(module_name, "Module already loaded");
200        }
201        Ok(())
202    }
203
204    /// Loads a kernel module with parameters if it is not already loaded.
205    ///
206    /// This function is idempotent: if the module is already loaded, this returns `Ok(())`
207    /// without attempting to reload it.
208    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(path = %path, module_name = %module_name), ret(level=tracing::Level::TRACE)))]
209    fn insmod_asneeded_with_params(
210        &self,
211        path: &str,
212        module_name: &str,
213        params: &[&str],
214    ) -> Result<(), WifiError> {
215        if !is_module_loaded(module_name) {
216            let output = Command::new("insmod")
217                .arg(path)
218                .args(params)
219                .output()
220                .map_err(|e| {
221                    error!(error = %e, path, "Failed to execute insmod");
222                    WifiError::KernelModule(format!("insmod execution failed: {}", e))
223                })?;
224
225            if !output.status.success() {
226                let stderr = String::from_utf8_lossy(&output.stderr);
227                if stderr.contains("File exists") {
228                    debug!(
229                        module_name,
230                        "Module already loaded (insmod returned File exists)"
231                    );
232                } else {
233                    error!(path, stderr = %stderr, "insmod failed");
234                    return Err(WifiError::KernelModule(format!(
235                        "Failed to load module {}: {}",
236                        path, stderr
237                    )));
238                }
239            } else {
240                std::thread::sleep(std::time::Duration::from_millis(250));
241            }
242        } else {
243            debug!(module_name, "Module already loaded");
244        }
245        Ok(())
246    }
247
248    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(module_name = %module_name), ret(level=tracing::Level::TRACE)))]
249    fn rmmod(&self, module_name: &str) -> Result<(), WifiError> {
250        if !is_module_loaded(module_name) {
251            debug!(module_name, "Module not loaded, skipping rmmod");
252            return Ok(());
253        }
254
255        let output = Command::new("rmmod")
256            .arg(module_name)
257            .output()
258            .map_err(|e| {
259                error!(error = %e, module_name, "Failed to execute rmmod");
260                WifiError::KernelModule(format!("rmmod execution failed: {}", e))
261            })?;
262
263        if !output.status.success() {
264            let stderr = String::from_utf8_lossy(&output.stderr);
265            error!(module_name, stderr = %stderr, "rmmod failed");
266            return Err(WifiError::KernelModule(format!(
267                "Failed to unload {}: {}",
268                module_name, stderr
269            )));
270        }
271
272        debug!(module_name, "Module unloaded successfully");
273        Ok(())
274    }
275
276    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
277    fn power_up_ntx_io(&self) -> Result<(), WifiError> {
278        use std::os::unix::fs::OpenOptionsExt;
279
280        let fd = std::fs::OpenOptions::new()
281            .write(true)
282            .custom_flags(libc::O_NONBLOCK)
283            .open(NTX_IO_PATH)
284            .map_err(|e| {
285                error!(error = %e, "Failed to open ntx_io");
286                WifiError::Ioctl(format!("Failed to open {}: {}", NTX_IO_PATH, e))
287            })?;
288
289        self.ioctl_wifi_ctrl(fd.as_raw_fd(), 1)?;
290
291        info!("WiFi powered up via ntx_io");
292        Ok(())
293    }
294
295    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
296    fn power_down_ntx_io(&self) -> Result<(), WifiError> {
297        use std::os::unix::fs::OpenOptionsExt;
298
299        let fd = std::fs::OpenOptions::new()
300            .write(true)
301            .custom_flags(libc::O_NONBLOCK)
302            .open(NTX_IO_PATH)
303            .map_err(|e| {
304                error!(error = %e, "Failed to open ntx_io");
305                WifiError::Ioctl(format!("Failed to open {}: {}", NTX_IO_PATH, e))
306            })?;
307
308        self.ioctl_wifi_ctrl(fd.as_raw_fd(), 0)?;
309
310        info!("WiFi powered down via ntx_io");
311        Ok(())
312    }
313
314    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(enable = enable), ret(level=tracing::Level::TRACE)))]
315    fn ioctl_wifi_ctrl(&self, fd: std::os::fd::RawFd, enable: u8) -> Result<(), WifiError> {
316        let ret = unsafe { set_ntx_io_wifi_ctrl(fd, enable as libc::c_int) }.map_err(|e| {
317            WifiError::Ioctl(format!(
318                "ioctl CM_WIFI_CTRL with arg {} failed: {}",
319                enable, e
320            ))
321        })?;
322
323        if ret < 0 {
324            return Err(WifiError::Ioctl(format!(
325                "ioctl CM_WIFI_CTRL with arg {} failed",
326                enable
327            )));
328        }
329
330        debug!(enable, "ioctl CM_WIFI_CTRL succeeded");
331        Ok(())
332    }
333
334    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
335    fn power_up_wmt(&self) -> Result<(), WifiError> {
336        let module_path = &self.config.module_path;
337
338        self.insmod_asneeded(&format!("{}/wmt_drv.ko", module_path), "wmt_drv")?;
339        self.insmod_asneeded(
340            &format!("{}/wmt_chrdev_wifi.ko", module_path),
341            "wmt_chrdev_wifi",
342        )?;
343        self.insmod_asneeded(&format!("{}/wmt_cdev_bt.ko", module_path), "wmt_cdev_bt")?;
344
345        let wifi_module_path = format!("{}/{}.ko", module_path, self.config.module);
346        if Path::new(&wifi_module_path).exists() {
347            self.insmod_asneeded(&wifi_module_path, self.config.module.as_ref())?;
348        }
349
350        fs::write("/proc/driver/wmt_dbg", "0xDB9DB9").ok();
351        fs::write("/proc/driver/wmt_dbg", "7 9 0").ok();
352        std::thread::sleep(std::time::Duration::from_secs(1));
353        fs::write("/proc/driver/wmt_dbg", "0xDB9DB9").ok();
354        fs::write("/proc/driver/wmt_dbg", "7 9 1").ok();
355
356        fs::write(WMT_WIFI_PATH, "1").map_err(|e| {
357            error!(error = %e, "Failed to write to wmtWifi");
358            WifiError::Ioctl(format!("Failed to write to {}: {}", WMT_WIFI_PATH, e))
359        })?;
360
361        info!("WiFi powered up via WMT");
362        Ok(())
363    }
364
365    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
366    fn power_down_wmt(&self) -> Result<(), WifiError> {
367        if !Path::new(WMT_WIFI_PATH).exists() {
368            debug!("wmtWifi not present, skipping power down");
369            return Ok(());
370        }
371        match fs::write(WMT_WIFI_PATH, "0") {
372            Ok(()) => {
373                info!("WiFi powered down via WMT");
374                Ok(())
375            }
376            Err(e) => {
377                debug!(error = %e, "wmtWifi power down failed, assuming already off");
378                Ok(())
379            }
380        }
381    }
382
383    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
384    fn power_up_module(&self) -> Result<(), WifiError> {
385        let module_path = &self.config.module_path;
386        self.insmod_asneeded(
387            &format!("{}/sdio_wifi_pwr.ko", module_path),
388            "sdio_wifi_pwr",
389        )?;
390        info!("WiFi powered up via module");
391        Ok(())
392    }
393
394    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
395    fn power_down_module(&self) -> Result<(), WifiError> {
396        self.rmmod("sdio_wifi_pwr")?;
397        info!("WiFi powered down via module");
398        Ok(())
399    }
400
401    #[cfg_attr(feature = "otel", tracing::instrument(skip_all, ret(level=tracing::Level::TRACE)))]
402    fn read_country_code(&self) -> Option<String> {
403        let content = fs::read_to_string(CONFIG_PATH).ok()?;
404        for line in content.lines() {
405            if line.starts_with("WifiRegulatoryDomain=") {
406                return Some(line.split('=').nth(1)?.to_string());
407            }
408        }
409        None
410    }
411
412    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
413    fn build_module_params(&self) -> Vec<String> {
414        let mut params = Vec::new();
415
416        if let Some(country_code) = self.read_country_code() {
417            match &self.config.module {
418                WifiModule::Eight821cs => {
419                    params.push(format!("rtw_country_code={}", country_code));
420                }
421                WifiModule::Moal => {
422                    params.push(format!("reg_alpha2={}", country_code));
423                }
424                _ => {}
425            }
426        }
427
428        if self.config.module == WifiModule::Moal {
429            params.push("mod_para=nxp/wifi_mod_para_sd8987.conf".to_string());
430        }
431
432        params
433    }
434
435    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
436    fn load_wifi_module(&self) -> Result<(), WifiError> {
437        let module_params = self.build_module_params();
438        let platform = std::env::var("PLATFORM").unwrap_or_default();
439
440        if self.config.module == WifiModule::Moal {
441            let mlan_path = if Path::new(&format!("{}/{}/mlan.ko", DRIVERS_DIR, platform)).exists()
442            {
443                format!("{}/{}/mlan.ko", DRIVERS_DIR, platform)
444            } else {
445                format!("{}/mlan.ko", self.config.module_path)
446            };
447
448            if Path::new(&mlan_path).exists() && !is_module_loaded("mlan") {
449                self.insmod(&mlan_path)?;
450            }
451        }
452
453        let wifi_module_path = if Path::new(&format!(
454            "{}/{}/{}.ko",
455            DRIVERS_DIR, platform, self.config.module
456        ))
457        .exists()
458        {
459            format!("{}/{}/{}.ko", DRIVERS_DIR, platform, self.config.module)
460        } else {
461            format!("{}/{}.ko", self.config.module_path, self.config.module)
462        };
463
464        if !Path::new(&wifi_module_path).exists() {
465            return Err(WifiError::KernelModule(format!(
466                "WiFi module not found: {}",
467                wifi_module_path
468            )));
469        }
470
471        let params: Vec<&str> = module_params.iter().map(|s| s.as_str()).collect();
472        self.insmod_asneeded_with_params(&wifi_module_path, self.config.module.as_ref(), &params)?;
473
474        debug!(
475            module = %self.config.module,
476            "WiFi module loaded"
477        );
478        Ok(())
479    }
480
481    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(interface = %self.config.interface), ret(level=tracing::Level::TRACE)))]
482    fn wait_for_interface(&self) -> Result<(), WifiError> {
483        let interface_path = format!("/sys/class/net/{}", self.config.interface);
484        let max_attempts = 20;
485
486        for attempt in 0..max_attempts {
487            if Path::new(&interface_path).exists() {
488                debug!(
489                    interface = %self.config.interface,
490                    attempt,
491                    "Network interface appeared"
492                );
493                return Ok(());
494            }
495            std::thread::sleep(std::time::Duration::from_millis(200));
496        }
497
498        Err(WifiError::Interface(format!(
499            "Network interface {} did not appear after {} attempts",
500            self.config.interface, max_attempts
501        )))
502    }
503
504    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
505    fn start_wpa_supplicant(&self) -> Result<(), WifiError> {
506        use std::process::Command;
507
508        if self.is_wpa_supplicant_running() {
509            debug!("wpa_supplicant already running");
510            return Ok(());
511        }
512
513        let output = Command::new("wpa_supplicant")
514            .arg("-D")
515            .arg(self.config.wpa_supplicant_driver)
516            .arg("-s")
517            .arg("-i")
518            .arg(&self.config.interface)
519            .arg("-c")
520            .arg(WPA_SUPPLICANT_CONF)
521            .arg("-C")
522            .arg(WPA_SUPPLICANT_SOCKET)
523            .arg("-B")
524            .env("LD_LIBRARY_PATH", "")
525            .output()
526            .map_err(|e| {
527                error!(error = %e, "Failed to execute wpa_supplicant");
528                WifiError::Interface(format!("Failed to start wpa_supplicant: {}", e))
529            })?;
530
531        if !output.status.success() {
532            let stderr = String::from_utf8_lossy(&output.stderr);
533            warn!(
534                stderr = %stderr,
535                "wpa_supplicant may have failed, continuing"
536            );
537        } else {
538            debug!("wpa_supplicant started");
539        }
540
541        Ok(())
542    }
543
544    #[cfg_attr(feature = "otel", tracing::instrument(skip_all, ret(level=tracing::Level::TRACE)))]
545    fn is_wpa_supplicant_running(&self) -> bool {
546        std::process::Command::new("pkill")
547            .args(["-0", "wpa_supplicant"])
548            .output()
549            .map(|o| o.status.success())
550            .unwrap_or(false)
551    }
552
553    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
554    fn stop_wpa_supplicant(&self) -> Result<(), WifiError> {
555        let output = std::process::Command::new("wpa_cli")
556            .arg("-i")
557            .arg(&self.config.interface)
558            .arg("terminate")
559            .output()
560            .map_err(|e| {
561                error!(error = %e, "Failed to execute wpa_cli");
562                WifiError::Interface(format!("Failed to stop wpa_supplicant: {}", e))
563            })?;
564
565        if !output.status.success() {
566            let stderr = String::from_utf8_lossy(&output.stderr);
567            debug!(stderr = %stderr, "wpa_cli terminate may have failed");
568        } else {
569            debug!("wpa_supplicant stopped");
570        }
571
572        Ok(())
573    }
574
575    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(interface = %self.config.interface), ret(level=tracing::Level::TRACE)))]
576    fn ifconfig_up(&self) -> Result<(), WifiError> {
577        let output = std::process::Command::new("ifconfig")
578            .arg(&self.config.interface)
579            .arg("up")
580            .output()
581            .map_err(|e| {
582                error!(error = %e, "Failed to execute ifconfig");
583                WifiError::Interface(format!("Failed to bring up interface: {}", e))
584            })?;
585
586        if !output.status.success() {
587            let stderr = String::from_utf8_lossy(&output.stderr);
588            error!(stderr = %stderr, "ifconfig up failed");
589            return Err(WifiError::Interface(format!(
590                "Failed to bring up interface: {}",
591                stderr
592            )));
593        }
594
595        debug!(interface = %self.config.interface, "Interface brought up");
596        Ok(())
597    }
598
599    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(interface = %self.config.interface), ret(level=tracing::Level::TRACE)))]
600    fn ifconfig_down(&self) -> Result<(), WifiError> {
601        let output = std::process::Command::new("ifconfig")
602            .arg(&self.config.interface)
603            .arg("down")
604            .output()
605            .map_err(|e| {
606                error!(error = %e, "Failed to execute ifconfig");
607                WifiError::Interface(format!("Failed to bring down interface: {}", e))
608            })?;
609
610        if !output.status.success() {
611            let stderr = String::from_utf8_lossy(&output.stderr);
612            debug!(stderr = %stderr, "ifconfig down may have failed");
613        } else {
614            debug!(interface = %self.config.interface, "Interface brought down");
615        }
616
617        Ok(())
618    }
619
620    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(interface = %self.config.interface), ret(level=tracing::Level::TRACE)))]
621    fn wlarm_le_up(&self) -> Result<(), WifiError> {
622        if self.config.module != WifiModule::Dhd {
623            return Ok(());
624        }
625
626        if let Err(e) = std::process::Command::new("wlarm_le")
627            .arg("-i")
628            .arg(&self.config.interface)
629            .arg("up")
630            .output()
631        {
632            warn!(error = %e, "Failed to execute wlarm_le up");
633            return Ok(());
634        }
635
636        debug!(interface = %self.config.interface, "wlarm_le up succeeded");
637        Ok(())
638    }
639
640    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), fields(interface = %self.config.interface), ret(level=tracing::Level::TRACE)))]
641    fn wlarm_le_down(&self) -> Result<(), WifiError> {
642        if self.config.module != WifiModule::Dhd {
643            return Ok(());
644        }
645
646        if let Err(e) = std::process::Command::new("wlarm_le")
647            .arg("-i")
648            .arg(&self.config.interface)
649            .arg("down")
650            .output()
651        {
652            warn!(error = %e, "Failed to execute wlarm_le down");
653            return Ok(());
654        }
655
656        debug!(interface = %self.config.interface, "wlarm_le down succeeded");
657        Ok(())
658    }
659
660    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
661    fn power_up(&self) -> Result<(), WifiError> {
662        match self.config.power_toggle {
663            PowerToggle::Wmt => self.power_up_wmt()?,
664            PowerToggle::NtxIo => self.power_up_ntx_io()?,
665            PowerToggle::Module => self.power_up_module()?,
666        }
667        Ok(())
668    }
669
670    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
671    fn power_down(&self) -> Result<(), WifiError> {
672        match self.config.power_toggle {
673            PowerToggle::Wmt => self.power_down_wmt()?,
674            PowerToggle::NtxIo => self.power_down_ntx_io()?,
675            PowerToggle::Module => self.power_down_module()?,
676        }
677        Ok(())
678    }
679}
680
681impl WifiManager for KoboWifiManager {
682    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
683    fn enable(&self) -> Result<(), WifiError> {
684        let _lock = self
685            .lock
686            .lock()
687            .map_err(|e| WifiError::Lock(format!("Failed to acquire lock: {}", e)))?;
688
689        if is_module_loaded(self.config.module.as_ref()) && is_interface_up(&self.config.interface)
690        {
691            info!("WiFi already enabled, skipping");
692            return Ok(());
693        }
694
695        info!(
696            module = %self.config.module,
697            interface = %self.config.interface,
698            "Enabling WiFi"
699        );
700
701        self.run_script(WIFI_PRE_UP_SCRIPT);
702
703        self.power_up()?;
704        self.load_wifi_module()?;
705        self.wait_for_interface()?;
706        self.ifconfig_up()?;
707        self.wlarm_le_up()?;
708        self.start_wpa_supplicant()?;
709
710        self.run_script(WIFI_POST_UP_SCRIPT);
711
712        info!("WiFi enabled successfully");
713        Ok(())
714    }
715
716    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
717    fn disable(&self) -> Result<(), WifiError> {
718        let _lock = self
719            .lock
720            .lock()
721            .map_err(|e| WifiError::Lock(format!("Failed to acquire lock: {}", e)))?;
722
723        if !is_module_loaded(self.config.module.as_ref()) {
724            info!("WiFi already disabled, skipping");
725            return Ok(());
726        }
727
728        info!(
729            module = %self.config.module,
730            "Disabling WiFi"
731        );
732
733        self.run_script(WIFI_PRE_DOWN_SCRIPT);
734
735        self.stop_wpa_supplicant()?;
736        self.wlarm_le_down()?;
737        self.ifconfig_down()?;
738
739        std::thread::sleep(std::time::Duration::from_millis(200));
740
741        if self.config.power_toggle != PowerToggle::Wmt {
742            self.rmmod(self.config.module.as_ref())?;
743            if self.config.module == WifiModule::Moal {
744                self.rmmod("mlan")?;
745            }
746        }
747
748        self.power_down()?;
749
750        self.run_script(WIFI_POST_DOWN_SCRIPT);
751
752        info!("WiFi disabled successfully");
753        Ok(())
754    }
755}
756
757/// Creates a WiFi manager for the current platform.
758///
759/// Reads `WIFI_MODULE`, `PLATFORM`, and `INTERFACE` environment variables
760/// to determine the appropriate configuration.
761///
762/// # Errors
763///
764/// Returns [`WifiError`] if required environment variables are not set.
765///
766/// # Example
767///
768/// ```ignore
769/// use cadmus_core::device::wifi::create_wifi_manager;
770///
771/// # fn example() -> Result<(), cadmus_core::device::wifi::WifiError> {
772/// let wifi_manager = create_wifi_manager()?;
773/// # Ok(())
774/// # }
775/// ```
776#[cfg_attr(feature = "otel", tracing::instrument)]
777pub fn create_wifi_manager() -> Result<Box<dyn WifiManager>, WifiError> {
778    let config = WifiModuleConfig::from_env().ok_or_else(|| {
779        WifiError::DeviceInfo("Missing WIFI_MODULE, PLATFORM, or INTERFACE env".to_string())
780    })?;
781
782    Ok(Box::new(KoboWifiManager::new(config)))
783}