Skip to content

Commit 4fb1a52

Browse files
committed
Add Wi-Fi power save configuration with WifiPowerSave enum and builder method
1 parent 479270a commit 4fb1a52

5 files changed

Lines changed: 100 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- `rustyfarian-esp-idf-mqtt`: non-blocking `MqttHandle::try_publish`, `try_publish_retained`, and `try_publish_with` with `TryPublishError` for time-critical loops
13+
- `wifi-pure`: `WifiPowerSave` enum (`None`, `MinModem`, `MaxModem`) and `WiFiConfig::with_power_save()` builder method
14+
- `rustyfarian-esp-idf-wifi`: applies configured power save mode via `esp_wifi_set_ps()` after Wi-Fi start
1315

1416
## [0.1.0] - 2026-03-16
1517

crates/rustyfarian-esp-idf-wifi/src/lib.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ use rgb::RGB8;
6666
// Re-export all pure types from wifi-pure
6767
pub use wifi_pure::{
6868
validate_password, validate_ssid, wifi_disconnect_reason_name, ConnectMode, WiFiConfig,
69-
WifiDriver, DEFAULT_TIMEOUT_SECS, PASSWORD_MAX_LEN, POLL_INTERVAL_MS, SSID_MAX_LEN,
69+
WifiDriver, WifiPowerSave, DEFAULT_TIMEOUT_SECS, PASSWORD_MAX_LEN, POLL_INTERVAL_MS,
70+
SSID_MAX_LEN,
7071
};
7172

7273
// Re-export StatusLed and SimpleLed from led_effects for convenience
@@ -156,7 +157,15 @@ impl WiFiManager {
156157
}))?;
157158

158159
wifi.start()?;
159-
log::info!("WiFi started");
160+
161+
let ps_mode = match config.power_save {
162+
WifiPowerSave::None => esp_idf_svc::sys::wifi_ps_type_t_WIFI_PS_NONE,
163+
WifiPowerSave::MinModem => esp_idf_svc::sys::wifi_ps_type_t_WIFI_PS_MIN_MODEM,
164+
WifiPowerSave::MaxModem => esp_idf_svc::sys::wifi_ps_type_t_WIFI_PS_MAX_MODEM,
165+
};
166+
esp_idf_svc::sys::esp!(unsafe { esp_idf_svc::sys::esp_wifi_set_ps(ps_mode) })
167+
.context("failed to set WiFi power save mode")?;
168+
log::info!("WiFi started, power save: {:?}", config.power_save);
160169

161170
// In non-blocking mode, subscribe to disconnect events so failures such as
162171
// WIFI_REASON_NO_AP_FOUND are visible at WARN level without enabling debug logs.

crates/wifi-pure/src/lib.rs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,27 @@ impl Default for ConnectMode {
8282
}
8383
}
8484

85+
// ─── WifiPowerSave ──────────────────────────────────────────────────────────
86+
87+
/// Wi-Fi power save mode passed to `esp_wifi_set_ps()` after starting Wi-Fi.
88+
///
89+
/// # ESP-NOW caveat
90+
///
91+
/// `esp-idf-svc` `EspNow::take()` internally forces `WIFI_PS_NONE`,
92+
/// overriding whatever mode was set here.
93+
/// This setting is most useful for battery-powered devices that use
94+
/// Wi-Fi *without* ESP-NOW.
95+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
96+
pub enum WifiPowerSave {
97+
/// Radio always on. Best latency, highest power draw.
98+
#[default]
99+
None,
100+
/// Minimum modem sleep — radio sleeps between DTIM beacon intervals.
101+
MinModem,
102+
/// Maximum modem sleep — radio sleeps as long as possible.
103+
MaxModem,
104+
}
105+
85106
// ─── WiFiConfig ─────────────────────────────────────────────────────────────
86107

87108
/// Wi-Fi connection configuration.
@@ -90,14 +111,16 @@ impl Default for ConnectMode {
90111
///
91112
/// ```ignore
92113
/// let config = WiFiConfig::new("MyNetwork", "password123")
93-
/// .with_timeout(60) // optional: override the 30 s default
94-
/// .connect_nonblocking(); // optional: return immediately from new()
114+
/// .with_timeout(60) // optional: override the 30 s default
115+
/// .connect_nonblocking() // optional: return immediately from new()
116+
/// .with_power_save(WifiPowerSave::MinModem); // optional: modem sleep for battery savings
95117
/// ```
96118
#[derive(Debug, Clone)]
97119
pub struct WiFiConfig<'a> {
98120
pub ssid: &'a str,
99121
pub password: &'a str,
100122
pub connect_mode: ConnectMode,
123+
pub power_save: WifiPowerSave,
101124
}
102125

103126
impl<'a> WiFiConfig<'a> {
@@ -109,6 +132,7 @@ impl<'a> WiFiConfig<'a> {
109132
ssid,
110133
password,
111134
connect_mode: ConnectMode::default(),
135+
power_save: WifiPowerSave::default(),
112136
}
113137
}
114138

@@ -134,6 +158,19 @@ impl<'a> WiFiConfig<'a> {
134158
self.connect_mode = ConnectMode::NonBlocking;
135159
self
136160
}
161+
162+
/// Sets the Wi-Fi power save mode applied after `wifi.start()`.
163+
///
164+
/// Defaults to [`WifiPowerSave::None`] (radio always on).
165+
///
166+
/// # ESP-NOW caveat
167+
///
168+
/// `esp-idf-svc` `EspNow::take()` internally forces `WIFI_PS_NONE`,
169+
/// overriding whatever mode is set here.
170+
pub fn with_power_save(mut self, mode: WifiPowerSave) -> Self {
171+
self.power_save = mode;
172+
self
173+
}
137174
}
138175

139176
// ─── Disconnect reason mapping ──────────────────────────────────────────────
@@ -332,6 +369,43 @@ mod tests {
332369
assert!(!driver.is_connected().unwrap());
333370
}
334371

372+
// ── WifiPowerSave tests ──────────────────────────────────────────────
373+
374+
#[test]
375+
fn power_save_default_is_none() {
376+
assert_eq!(WifiPowerSave::default(), WifiPowerSave::None);
377+
}
378+
379+
#[test]
380+
fn wifi_config_default_power_save_is_none() {
381+
let config = test_config();
382+
assert_eq!(config.power_save, WifiPowerSave::None);
383+
}
384+
385+
#[test]
386+
fn wifi_config_power_save_min_modem() {
387+
let config = test_config().with_power_save(WifiPowerSave::MinModem);
388+
assert_eq!(config.power_save, WifiPowerSave::MinModem);
389+
}
390+
391+
#[test]
392+
fn wifi_config_power_save_max_modem() {
393+
let config = test_config().with_power_save(WifiPowerSave::MaxModem);
394+
assert_eq!(config.power_save, WifiPowerSave::MaxModem);
395+
}
396+
397+
#[test]
398+
fn wifi_config_chained_builders() {
399+
let config = test_config()
400+
.with_timeout(60)
401+
.with_power_save(WifiPowerSave::MinModem)
402+
.connect_nonblocking();
403+
assert!(matches!(config.connect_mode, ConnectMode::NonBlocking));
404+
assert_eq!(config.power_save, WifiPowerSave::MinModem);
405+
}
406+
407+
// ── MockWifiDriver tests ────────────────────────────────────────────
408+
335409
#[test]
336410
fn mock_driver_fail_connect() {
337411
let mut driver = mock::MockWifiDriver::new();

crates/wifi-pure/src/mock.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
//! assert_eq!(driver.connect_count, 1);
2323
//! ```
2424
25-
use crate::WifiDriver;
25+
use crate::{WifiDriver, WifiPowerSave};
2626

2727
/// Error type for [`MockWifiDriver`].
2828
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -48,6 +48,7 @@ pub struct MockWifiDriver {
4848
pub netif_up: bool,
4949
pub connect_count: u32,
5050
pub fail_connect: bool,
51+
pub power_save: WifiPowerSave,
5152
}
5253

5354
impl MockWifiDriver {
@@ -60,6 +61,7 @@ impl MockWifiDriver {
6061
netif_up: false,
6162
connect_count: 0,
6263
fail_connect: false,
64+
power_save: WifiPowerSave::default(),
6365
}
6466
}
6567
}

docs/ROADMAP.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ timeline
2525
: Phase 5 — TTN v3 EU868 OTAA validation (blocked on hardware)
2626
: LoRa post-adoption backlog — builder pattern, CRC-32, hardware driver, state machine
2727
28-
Long term : Full EspHalLoraRadio hardware driver (after TTN validation)
28+
Long term : Evaluate ESP-IDF v5.5.2 coex fix for ESP-NOW send failures
29+
: Full EspHalLoraRadio hardware driver (after TTN validation)
2930
: rustyfarian-esp-hal-mqtt — minimq-based bare-metal MQTT (after esp-hal WiFi)
3031
```
3132

@@ -136,6 +137,12 @@ All steps use TTN v3 EU868.
136137

137138
</details>
138139

140+
### Research
141+
142+
| # | Item |
143+
|--:|:---------------------------------------------------------------------------------------------------------------------------|
144+
| 1 | Evaluate ESP-IDF v5.5.2 — contains fix for "ESP-NOW send failure when coexistence is enabled"; track for next ESP-IDF bump |
145+
139146
### LoRa post-adoption backlog
140147

141148
<details>

0 commit comments

Comments
 (0)