Skip to content

iTWT (Individual Target Wake Time) support - #5960

Open
main-- wants to merge 5 commits into
esp-rs:mainfrom
main--:feat/itwt-support
Open

iTWT (Individual Target Wake Time) support#5960
main-- wants to merge 5 commits into
esp-rs:mainfrom
main--:feat/itwt-support

Conversation

@main--

@main-- main-- commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Submission Checklist 📝

  • I have updated existing examples or added new ones (if applicable).
  • I have used cargo xtask fmt-packages command to ensure that all changed code is formatted correctly.
  • I have added changelog entries and/or migration guide notes in the sections below, or I will ask a maintainer to add the skip-changelog or manual-changelog label as appropriate.
  • My changes are in accordance to the esp-rs developer guidelines

Extra:

Pull Request Details 📖

Description

Add individual Target Wake Time (iTWT) support for Wi-Fi 6 (802.11ax) chips (ESP32-C5, C6, C61), enabling significant power savings for periodic traffic patterns. After negotiating iTWT with the AP, the entire model will go to sleep and only wake up during the negotiated time windows.

Depends on esp-rs/esp-wifi-sys#505.

Testing

Tested on ESP32-C6.

Measured power consumption in an example that sends a 160 byte UDP packet every 20ms (simulating a VoIP use case):

  • Baseline: 63 mA
  • With Power Save Maximum: 63 mA (no impact because send interval prevents the modem from going to sleep)
  • With iTWT (2ms wakeup every 20ms): 32 mA (modem sleeps 90% of the time, can be optimized further)

Changelog

esp-radio

  • Added: wifi::twt module with iTWT configuration types and constants
  • Added: WifiController methods for iTWT setup, teardown, suspend, probing, and configuration (Wi-Fi 6 chips only)
  • Added: Structured event data for all TWT-related WiFi events
  • Added: TwtFull, TwtSetupTimeout, TwtSetupTxFail, TwtSetupRejected, and TwtSetupFailed variants added to WifiError
  • Changed: wifi_clock_disable OS adapter callback now keeps clocks on during modem-sleep, matching ESP-IDF behavior

esp-phy

  • No changelog necessary.

While Wi-Fi is active, ESP-IDF blocks wifi_clock_disable calls from the blob: https://github.com/espressif/esp-idf/blob/release/v5.5/components/esp_hw_support/modem_clock.c#L77-L85

This is necessary because otherwise hardware-provided timers such as TWT do not fire.
@bjoernQ

bjoernQ commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Depends on esp-rs/esp-wifi-sys#505.

The additional bindings PR got merged so this should become buildable by changing the git revision of the esp-wifi-sys dependencies

@main--
main-- force-pushed the feat/itwt-support branch 4 times, most recently from dde6d48 to eb5d2d3 Compare July 24, 2026 09:00
main-- added 3 commits July 24, 2026 11:19
Add individual Target Wake Time (iTWT) support for Wi-Fi 6 (802.11ax)
chips (ESP32-C5, C6, C61), enabling significant power savings for
periodic traffic patterns.

Examples:
- embassy_twt: comprehensively demonstrates usage of iTWT APIs
- embassy_twt_udp: voice-over-WiFi (160B UDP on 20ms TWT wakeup)
@main--
main-- force-pushed the feat/itwt-support branch from eb5d2d3 to f1dbfa1 Compare July 24, 2026 09:19
Comment thread esp-radio/src/wifi/mod.rs Outdated
@bjoernQ

bjoernQ commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

This is an exciting addition - however the udp example doesn't seem to work for me - maybe "I'm holding it wrong"

I see

Requesting iTWT setup...
iTWT setup OK: ITwtSetupConfig { setup_cmd: Accept, trigger: true, flow_type: Announced, flow_id: Flow1, wake_interval: Duration(20000 µs), wake_duration: Duration(2048 µs), timeout: Duration(0 µs) }
#1: sent 160B voice frame (type=Individual, flow=Flow1)

And nothing more - nothing is received. But the setup looks good, doesn't it?
(The router is a Fritz!Box 5090, tested on ESP32-C6)

@main--

main-- commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Oh no! Here is what I get with a Fritz!Box 7682 and my ESP32-C6:

Requesting iTWT setup...
iTWT setup OK: ITwtSetupConfig { setup_cmd: Accept, trigger: true, flow_type: Announced, flow_id: Flow1, wake_interval: Duration(20000 µs), wake_duration: Duration(2048 µs), timeout: Duration(0 µs) }
#1: sent 160B voice frame (type=Individual, flow=Flow1)
#51: sent 160B voice frame (type=Individual, flow=Flow1)
#101: sent 160B voice frame (type=Individual, flow=Flow1)
#151: sent 160B voice frame (type=Individual, flow=Flow1)
#201: sent 160B voice frame (type=Individual, flow=Flow1)

and so on

I can think of a few possibilities:

  • I have already reported some iTWT-related bugs to AVM, so perhaps the 5090 has some issue preventing this from working
    • seems unlikely, and according to espressif even if the AP fails to send triggers in trigger mode after some timeout the chip should wake up anyways
  • I also found a bug in the wifi blob (Individual Target Wake Time (iTWT) downlink broken (IDFGH-17670) espressif/esp-idf#18623), but it should not prevent this example from running because it only concerns receiving
    • unless maybe you got unlucky and got deauthenticated immediately after starting due to this bug (normally it takes 15-30mins for me)
    • or maybe there is some other bug that you're running into
  • you can try other config options, e.g. .with_trigger(false) and see if that maybe helps

Since you are also using a C6 it shouldn't be a hardware difference, but I originally saw the same problem and it was fixed by 734971d.

And move twt-specific stuff outof the general wifi module
@bjoernQ

bjoernQ commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Did you configure anything special on your Fritz!Box 7682 ? Are you using WPA2+WPA3 (I enabled WPA2 only - not sure if that might make a difference)

(Ah - there was a typo - mine is a 5590 not 5090)

@bjoernQ

bjoernQ commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

When changing wifi_clock_disable to an empty stub I receive at least 4 packets before it stops doing anything 🤷‍♂️

@main--

main-- commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

I am using WPA2+WPA3, and all the settings on default as far as I can see. But in theory, the fact that you are getting a positive setup response should already be a definitive proof that your Fritz!Box can do iTWT and accepts your parameters. Also in my tests esp-hal doesn't even do WPA3 so that's another reason why it shouldn't matter.

If you're hanging on the call to wait_for_next_twt_wakeup, the only reasons I can think of are:

  • some bug in esp-hal is causing the wakeup event to be lost
  • some bug in the wifi blob is causing it to lose the wakeup event
  • some bug in the hardware is causing it not to wake up in the first place

I'm very confused because none of those sound plausible to me.

When changing wifi_clock_disable to an empty stub I receive at least 4 packets before it stops doing anything 🤷‍♂️

By receive you mean receiving the UDP packets on another host, or observing the wakeup events in the logs?

Maybe if you listen in monitor mode you can see what's really going on (maybe deauthentication, maybe AP is tearing down the TWT agreement, etc)

@bjoernQ

bjoernQ commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Yes for now we can't support WPA3 and I agree that accepting the iTWT setup should tell it should work 🤷‍♂️

By receive you mean receiving the UDP packets on another host, or observing the wakeup events in the logs?

Yes - seeing the packets getting received on the other host

@main--

main-- commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Given that I'm not able to reproduce the issue, I have no idea how to proceed here. Do you have the same issue with esp-idf?

@bjoernQ

bjoernQ commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Maybe the esp-idf example also doesn't work correctly - I'm not sure how it should look like if it's working, can you paste your output of that example here?

I asked a colleague to test the PR but also without luck - I'm still trying to check / fix my setup but the esp-idf example output might be helpful for me

@main--

main-- commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Right, the IDF example does not post events or send anything out of the box. Here are my changes that makes it similar to the udp example here:

Diff
diff --git a/examples/wifi/itwt/main/itwt_main.c b/examples/wifi/itwt/main/itwt_main.c
index ae4914067cb..ee417d87eb8 100644
--- a/examples/wifi/itwt/main/itwt_main.c
+++ b/examples/wifi/itwt/main/itwt_main.c
@@ -18,6 +18,9 @@
    start esp32c6 and when it connected to AP it will setup itwt.
 */
 #include <netdb.h>
+#include <inttypes.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
 #include "freertos/FreeRTOS.h"
 #include "freertos/event_groups.h"
 #include "esp_wifi.h"
@@ -37,6 +40,10 @@
  *******************************************************/
 static const char *TAG = "itwt";
 
+/* UDP target for TWT wakeup packets */
+#define TWT_WAKEUP_UDP_TARGET_IP   "255.255.255.255"
+#define TWT_WAKEUP_UDP_TARGET_PORT 12345
+
 /*******************************************************
  *                Structures
  *******************************************************/
@@ -72,6 +79,10 @@ const int CONNECTED_BIT = BIT0;
 const int DISCONNECTED_BIT = BIT1;
 EventGroupHandle_t wifi_event_group;
 
+static int twt_udp_sock = -1;
+static struct sockaddr_in twt_udp_dest;
+static uint32_t twt_wakeup_counter = 0;
+
 /*******************************************************
  *                Function Declarations
  *******************************************************/
@@ -112,12 +123,44 @@ static const char *itwt_probe_status_to_str(wifi_itwt_probe_status_t status)
     }
 }
 
+static void twt_wakeup_handler(void *arg, esp_event_base_t event_base,
+                               int32_t event_id, void *event_data)
+{
+return;
+    if (twt_udp_sock < 0) {
+        return;
+    }
+    twt_wakeup_counter++;
+    int ret = sendto(twt_udp_sock, &twt_wakeup_counter, sizeof(twt_wakeup_counter), 0,
+                     (struct sockaddr *)&twt_udp_dest, sizeof(twt_udp_dest));
+    if (ret < 0) {
+        ESP_LOGD(TAG, "<TWT_WAKEUP> UDP send failed: errno %d", errno);
+    } else {
+        ESP_LOGI(TAG, "<TWT_WAKEUP> sent UDP pkt #%"PRIu32, twt_wakeup_counter);
+    }
+}
+
 static void got_ip_handler(void *arg, esp_event_base_t event_base,
                            int32_t event_id, void *event_data)
 {
     xEventGroupClearBits(wifi_event_group, DISCONNECTED_BIT);
     xEventGroupSetBits(wifi_event_group, CONNECTED_BIT);
 
+    /* Create UDP socket for TWT wakeup packets */
+    if (twt_udp_sock < 0) {
+        twt_udp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+        if (twt_udp_sock < 0) {
+            ESP_LOGE(TAG, "Failed to create UDP socket: errno %d", errno);
+        } else {
+            memset(&twt_udp_dest, 0, sizeof(twt_udp_dest));
+            twt_udp_dest.sin_family = AF_INET;
+            twt_udp_dest.sin_port = htons(TWT_WAKEUP_UDP_TARGET_PORT);
+            inet_aton(TWT_WAKEUP_UDP_TARGET_IP, &twt_udp_dest.sin_addr);
+            ESP_LOGI(TAG, "TWT wakeup UDP socket created, target %s:%d",
+                     TWT_WAKEUP_UDP_TARGET_IP, TWT_WAKEUP_UDP_TARGET_PORT);
+        }
+    }
+
     /* setup a trigger-based announce individual TWT agreement. */
     wifi_phy_mode_t phymode;
     wifi_config_t sta_cfg = { 0, };
@@ -260,6 +303,11 @@ static void wifi_itwt(void)
                     &itwt_probe_handler,
                     NULL,
                     NULL));
+    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
+                    WIFI_EVENT_TWT_WAKEUP,
+                    &twt_wakeup_handler,
+                    NULL,
+                    NULL));
 
     wifi_config_t wifi_config = {
         .sta = {
@@ -271,7 +319,7 @@ static void wifi_itwt(void)
     ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
 
     wifi_twt_config_t wifi_twt_config = {
-        .post_wakeup_event = false,
+        .post_wakeup_event = true,
         .twt_enable_keep_alive = keep_alive_enabled,
     };
     ESP_ERROR_CHECK(esp_wifi_sta_twt_config(&wifi_twt_config));

@bjoernQ

bjoernQ commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Ok this at least indicates something bad I guess

I (5610) esp_netif_handlers: sta ip: 192.168.178.86, mask: 255.255.255.0, gw: 192.168.178.1
I (5611) itwt: TWT wakeup UDP socket created, target 255.255.255.255:12345
E (10654) itwt: <WIFI_EVENT_ITWT_SETUP>twt_id:0, timeout of receiving twt setup response frame`

@main--

main-- commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

It's confusing that IDF gives you a timeout during setup when you were getting a success response before. Are you running the same wifi blob in both cases?

@bjoernQ

bjoernQ commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Yes - it's run from the same commit I took the blobs from

Maybe I should retry the Rust example since my router did an auto-update since then - but I don't expect much to have changed

@bjoernQ

bjoernQ commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Aha - so maybe the update did something - now I also get an error for the Rust example:

Wifi connected: ConnectedInfo { ssid: "FRITZ!Box 5590 YH", bssid: [x, x, x, x, x, x], channel: 6, authmode: Wpa2Personal, aid: 6 }
Got IP: 192.168.178.86/24
Requesting iTWT setup...
iTWT setup FAILED: Failed(ITwtSetupFailedInfo { config: ITwtSetupConfig { setup_cmd: Request, trigger: true, flow_type: Announced, flow_id: Flow0, wake_interval: Duration(20000 µs), wake_duration: Duration(2048 µs), timeout: Duration(0 µs) }, status: 12312, reason: 0 })

@bjoernQ

bjoernQ commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Interestingly - at least with new firmware - I see that with "WPA2" only enabled, by default PMF is disabled, enabling it doesn't help.

"WPA2+WPA3" enabled PMF automatically (and there is no option to disable it anymore)

I also tried a few other config changes which made kind of sense but none of them made a difference.

At least ESP-IDF and the Rust implementation show the same behavior

@main--

main-- commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

This is strange, your Fritz!Box claims to support WiFi 6, so the iTWT setup request should definitely not go unanswered. But if you're getting the same issue with IDF then it looks like at least it's not caused by any problem with the code in this PR. 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants