iTWT (Individual Target Wake Time) support - #5960
Conversation
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.
The additional bindings PR got merged so this should become buildable by changing the git revision of the esp-wifi-sys dependencies |
dde6d48 to
eb5d2d3
Compare
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)
eb5d2d3 to
f1dbfa1
Compare
|
This is an exciting addition - however the udp example doesn't seem to work for me - maybe "I'm holding it wrong" I see And nothing more - nothing is received. But the setup looks good, doesn't it? |
|
Oh no! Here is what I get with a Fritz!Box 7682 and my ESP32-C6: and so on I can think of a few possibilities:
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
|
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) |
|
When changing |
|
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
I'm very confused because none of those sound plausible to me.
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) |
|
Yes for now we can't support WPA3 and I agree that accepting the iTWT setup should tell it should work 🤷♂️
Yes - seeing the packets getting received on the other host |
|
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? |
|
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 |
|
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: Diffdiff --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)); |
|
Ok this at least indicates something bad I guess |
|
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? |
|
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 |
|
Aha - so maybe the update did something - now I also get an error for the Rust example: |
|
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 |
|
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. 🤷 |
| .subscriber() | ||
| .expect("Unable to subscribe to events - consider increasing the internal event channel subscriber count"); | ||
|
|
||
| let twt_id = NEXT_TWT_ID.fetch_add(1, Ordering::Relaxed); |
There was a problem hiding this comment.
This can overflow and start handing out already in-use IDs.
There was a problem hiding this comment.
To the best of my knowledge (unfortunately there is no clear documentation on this, but it does not exist on air so it's definitely local to the wifi blob) this ID is only used to match a setup request with the corresponding response event when it comes back from the wifi blob. Then this ID is meaningless and the flow will only be identified by its FlowId.
For a spec-compliant AP I don't think we can send 65536 setup requests before receiving a reply. If I have an AP which does not send any setup replies and I pick a high enough timeout then this might overflow fast enough to in-use IDs (although before this happens the Wifi Blob may even OOM from having too many in-flight setup requests).
And don't forget that there can only be 8 flows in total, so an application will never attempt to allocate sixty-thousand of them in an instant unless it already is in some serious unrecoverable error state.
|
We shouldn't let this PR rot away, even if we can't test it ourselves. Although I'm slightly worried about useability - is this really something we expect the user to manage? If it is, it is, but in that case the documentation could be a bit more detailed, a bit more guide-like. |
| /// ``` | ||
| #[derive(BuilderLite, Clone, Copy, PartialEq, Eq, Hash)] | ||
| #[instability::unstable] | ||
| pub struct ITwtSetupConfig { |
There was a problem hiding this comment.
Config objects should not have public methods, they should be exclusively constructible via BuilderLite, so that we don't stabilise their field layout. Even if the type is currently unstable, this must change eventually.
There was a problem hiding this comment.
Also I find the name "setup config" weirdly redundant. Can this just be called ITwtConfig?
There was a problem hiding this comment.
This was an attempt to balance between two conflicting interests:
- either we could expose exponent+mantissa and interval+unit as-is (like IDF does) and have the user handle the maths
- or we could only expose a much nicer "porcelain" interface (see
with_wake_intervalbelow) that simply takes a Duration and converts it
Now the problem with the second approach is that for a given Duration value there will often be multiple possible raw representations, and one of the Fritz!Box bugs I ran into resulted in a case where the user might have to specify raw values in order to work around the AP bug for certain Duration values. This is why the struct does expose an entirely builder-based interface (and this is what one is supposed to reach for) but gives the option to use struct syntax if one really must specify raw values instead.
Having said that, there is always the obvious alternative of naming the other builders something like with_raw_wake_interval_exponent so if you think that's a better solution then I'm happy to change it.
Regarding the name, I just used the same name as in IDF but of course we can change it.
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| #[cfg_attr(feature = "defmt", derive(defmt::Format))] | ||
| #[instability::unstable] | ||
| pub enum ITwtTeardownStatus { |
There was a problem hiding this comment.
I would prefer (just to end up with idiomatic Rust) using error enums and Result.
| if negotiated_config.twt_id != twt_id { | ||
| continue; | ||
| } | ||
| if status == 1 { |
There was a problem hiding this comment.
That 1 should be a named constant since non-zero usually means failure.
Agreed - it somehow fell off my radar. Given iTWT is apparently something which is not as widely supported as I assumed, maybe it would be good to make the example show a more "defensive" way to deal with that situation (i.e. printing a warning message and work without iTWT) - at least that might help users in dealing with the situation. I also wonder if we could get away with only one example - I see how |
|
My phone is supposedly a WiFi 6 access point, but the embassy_twt example just crashes miserably, which is a blocker for this PR: |
|
If you need some testing on this PR lmk. I should be able to do some packet capturing, and hopefully diagnosing the issue. |
Given that no one in our team can get this working, we'd be most grateful 🙏 |
I like the idea. The problem is, I don't have a lot of hardware to test with so I can only test the failure scenarios that I can personally reproduce. For example, I have yet to see any actual negotiation or rejection response - all I ever get from my router is Accept. 🤷
It is likely that your phone does not support iTWT, which at least explains the I'm also happy to take a look at pcap files, just make sure to set he_sniffer_params on Intel NICs. |
cf0a394 to
105c7eb
Compare
|
I have to admit, that I haven't been able to do much in the way of testing, mainly because my Vodafone Wi-Fi 6 router is apparently not capable of iTWT and rejects all request. |

Submission Checklist 📝
cargo xtask fmt-packagescommand to ensure that all changed code is formatted correctly.skip-changelogormanual-changeloglabel as appropriate.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):
Changelog
esp-radio
wifi::twtmodule with iTWT configuration types and constantsWifiControllermethods for iTWT setup, teardown, suspend, probing, and configuration (Wi-Fi 6 chips only)TwtFull,TwtSetupTimeout,TwtSetupTxFail,TwtSetupRejected, andTwtSetupFailedvariants added toWifiErrorwifi_clock_disableOS adapter callback now keeps clocks on during modem-sleep, matching ESP-IDF behavioresp-phy