From 60e5573f159057dfb2a35c611ba8f082620dc690 Mon Sep 17 00:00:00 2001 From: Nikolay Chirkov Date: Mon, 29 Jun 2026 13:25:22 +0300 Subject: [PATCH 01/32] Add ESP32 prepared send integration scaffold --- .gitmodules | 3 + aether-client-cpp | 1 + main/CMakeLists.txt | 11 +- main/controller.cpp | 63 +++-- main/prepared_send/prepared_send.cpp | 379 +++++++++++++++++++++++++++ main/prepared_send/prepared_send.h | 59 +++++ 6 files changed, 497 insertions(+), 19 deletions(-) create mode 160000 aether-client-cpp create mode 100644 main/prepared_send/prepared_send.cpp create mode 100644 main/prepared_send/prepared_send.h diff --git a/.gitmodules b/.gitmodules index e69de29..f0b438b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "aether-client-cpp"] + path = aether-client-cpp + url = https://github.com/aethernetio/aether-client-cpp.git diff --git a/aether-client-cpp b/aether-client-cpp new file mode 160000 index 0000000..00a5ba6 --- /dev/null +++ b/aether-client-cpp @@ -0,0 +1 @@ +Subproject commit 00a5ba648994298f60053cda47387eee299db1ad diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 299d92e..6443bd6 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -17,6 +17,7 @@ cmake_minimum_required(VERSION 3.16.0) list(APPEND src_list "main.cpp" "controller.cpp" + "prepared_send/prepared_send.cpp" ) list(APPEND bme68x_srcs @@ -47,7 +48,10 @@ if(NOT CM_PLATFORM) include(../cmake/CPM.cmake) - CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#main") + CPMAddPackage( + NAME aether-client-cpp + SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../aether-client-cpp" + ) add_executable(${PROJECT_NAME} ${src_list} ${sleeping_src} ${sensors_src}) set(TARGET_NAME ${PROJECT_NAME}) @@ -77,7 +81,10 @@ else() set(TARGET_NAME "${COMPONENT_LIB}") include(../cmake/CPM.cmake) - CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#main") + CPMAddPackage( + NAME aether-client-cpp + SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../aether-client-cpp" + ) target_link_libraries(${TARGET_NAME} PRIVATE aether) diff --git a/main/controller.cpp b/main/controller.cpp index 3d0ab38..1e92265 100644 --- a/main/controller.cpp +++ b/main/controller.cpp @@ -20,6 +20,7 @@ #include "aether/all.h" #include "sensors/sensors.h" #include "sleeping/sleeping.h" +#include "prepared_send/prepared_send.h" /** * Standard uid for test application. @@ -62,10 +63,47 @@ void GoToSleep(ae::Uap::Timer uap_timer); static ae::RcPtr aether_app; static ae::RcPtr message_stream; +#ifndef AETHER_PREPARED_HOT_SLEEP_SECONDS +# define AETHER_PREPARED_HOT_SLEEP_SECONDS 600 +#endif + +static constexpr auto kPreparedHotSleepSeconds = + std::chrono::seconds{AETHER_PREPARED_HOT_SLEEP_SECONDS}; + +static constexpr std::size_t kPreparedNonceReserve = +#ifdef AETHER_PREPARED_NONCE_RESERVE + AETHER_PREPARED_NONCE_RESERVE; +#else + 32; +#endif + + void setup() { std::cout << ae::Format("Setup {:%Y-%m-%d %H:%M:%S}") << ae::Now() << std::endl; +#if defined(ESP_PLATFORM) + // Prepared-send hot path: try to send current temperature without creating + // full AetherApp. Any error falls back to the normal full boot below. + { + std::int16_t hot_temperature = {}; + ReadSensors(&hot_temperature, nullptr, nullptr, nullptr, nullptr); + + auto hot_status = + temp_sensor::prepared_send::TryHotWakePreparedSend(hot_temperature); + + std::cout << ae::Format(" >>> Prepared hot path status: {}\n", + temp_sensor::prepared_send::ToString(hot_status)); + + if (hot_status == temp_sensor::prepared_send::HotSendStatus::kSent) { + auto sleep_until = std::chrono::system_clock::now() + + kPreparedHotSleepSeconds; + DeepSleep(sleep_until, sleep_until, 3000); + return; + } + } +#endif + aether_app = ae::AetherApp::Construct( ae::AetherAppContext{} #if AE_DISTILLATION @@ -170,25 +208,16 @@ void SendValue(std::int16_t temperature) { return; } - struct Header { - std::uint8_t const root_code = 0x3; - std::uint8_t const size = sizeof(std::uint8_t) + sizeof(std::int16_t); - std::uint8_t const dev_code = 0x10; - AE_REFLECT_MEMBERS(root_code, size, dev_code) - }; - static constexpr auto header = Header{}; - - auto message = ae::DataBuffer{}; - message.reserve(sizeof(header) + 2); - { - auto writer = ae::VectorWriter<>{message}; - auto stream = ae::omstream{writer}; - // write message header and temperature value - // temperature in range -100.0 to 100.0 x100 (-10000 to 10000) - stream << header << temperature; - } + auto message = temp_sensor::prepared_send::MakeTemperaturePayload(temperature); message_stream->Write(std::move(message)).status_event().Subscribe([](auto) { + // Export/refresh prepared block after the full send path has a valid stream. + // If export fails, keep normal behavior and just sleep. + if (aether_app && message_stream) { + temp_sensor::prepared_send::ExportPreparedSendBlock( + *aether_app, *message_stream, kPreparedNonceReserve); + } + // with any result ready to sleep aether_app->aether()->uap->SleepReady(); }); diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp new file mode 100644 index 0000000..c88cd64 --- /dev/null +++ b/main/prepared_send/prepared_send.cpp @@ -0,0 +1,379 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Experimental ESP32 prepared-send integration. + * + * This file intentionally keeps the hot path isolated from controller.cpp. + * If hot path fails for any reason, controller.cpp falls back to normal + * full-Aether boot. + */ + +#include "prepared_send/prepared_send.h" + +#include +#include +#include + +#include "aether/all.h" +#include "aether/mstream.h" +#include "aether/mstream_buffers.h" + +#if defined(ESP_PLATFORM) +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#endif + +namespace temp_sensor::prepared_send { +namespace { + +static constexpr char const* kTag = "prepared-send"; + +static constexpr std::uint32_t kMagic = 0x50534456; // "PSDV" +static constexpr std::uint32_t kVersion = 1; +static constexpr std::size_t kMaxPreparedBlockBytes = 4096; + +#ifndef AETHER_PREPARED_NONCE_RESERVE +# define AETHER_PREPARED_NONCE_RESERVE 32 +#endif + +#ifndef AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS +# define AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS 15000 +#endif + +struct RetainedPreparedBlock { + std::uint32_t magic; + std::uint32_t version; + std::uint32_t size; + std::uint32_t checksum; + std::array bytes; +}; + +#if defined(ESP_PLATFORM) +// RTC_NOINIT_ATTR: do not zero on wake from deep sleep. +// It may contain garbage on first boot, so magic/version/checksum validate it. +RTC_NOINIT_ATTR RetainedPreparedBlock g_retained_prepared_block; +#else +RetainedPreparedBlock g_retained_prepared_block; +#endif + +std::uint32_t Checksum(std::uint8_t const* data, std::size_t size) { + std::uint32_t h = 2166136261u; + for (std::size_t i = 0; i < size; ++i) { + h ^= data[i]; + h *= 16777619u; + } + return h; +} + +bool RetainedLooksValid() { + auto const& r = g_retained_prepared_block; + if (r.magic != kMagic || r.version != kVersion) { + return false; + } + if (r.size == 0 || r.size > r.bytes.size()) { + return false; + } + return Checksum(r.bytes.data(), r.size) == r.checksum; +} + +void SaveRawBlock(ae::DataBuffer const& bytes) { + auto& r = g_retained_prepared_block; + std::memset(&r, 0, sizeof(r)); + + r.magic = kMagic; + r.version = kVersion; + r.size = static_cast(bytes.size()); + std::memcpy(r.bytes.data(), bytes.data(), bytes.size()); + r.checksum = Checksum(r.bytes.data(), r.size); +} + +bool LoadRawBlock(ae::DataBuffer& bytes) { + if (!RetainedLooksValid()) { + return false; + } + + auto const& r = g_retained_prepared_block; + bytes.assign(r.bytes.begin(), r.bytes.begin() + r.size); + return true; +} + +template +bool SerializeToRetained(T const& value) { + ae::DataBuffer bytes; + bytes.reserve(512); + + { + auto writer = ae::VectorWriter<>{bytes}; + auto os = ae::omstream{writer}; + os << value; + } + + if (bytes.empty() || bytes.size() > kMaxPreparedBlockBytes) { + std::cerr << "[prepared-send] prepared block serialized size invalid: " + << bytes.size() << "\n"; + return false; + } + + SaveRawBlock(bytes); + return true; +} + +template +bool DeserializeFromRetained(T& value) { + ae::DataBuffer bytes; + if (!LoadRawBlock(bytes)) { + return false; + } + + auto reader = ae::VectorReader<>{bytes}; + auto is = ae::imstream{reader}; + is >> value; + + return ae::data_was_read(is); +} + +#if defined(ESP_PLATFORM) + +static EventGroupHandle_t g_wifi_event_group = nullptr; +static constexpr EventBits_t kWifiConnectedBit = BIT0; +static constexpr EventBits_t kWifiFailBit = BIT1; + +void WifiEventHandler(void*, esp_event_base_t event_base, + std::int32_t event_id, void*) { + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && + event_id == WIFI_EVENT_STA_DISCONNECTED) { + xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + xEventGroupSetBits(g_wifi_event_group, kWifiConnectedBit); + } +} + +bool EnsureWifiConnectedForHotPath() { +#ifndef WIFI_SSID + ESP_LOGE(kTag, "WIFI_SSID is not defined"); + return false; +#endif +#ifndef WIFI_PASSWORD + ESP_LOGE(kTag, "WIFI_PASSWORD is not defined"); + return false; +#endif + + // It is OK if these were already initialized by a previous attempt. + auto err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || + err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + if (err != ESP_OK && err != ESP_ERR_NVS_NO_FREE_PAGES) { + ESP_LOGE(kTag, "nvs_flash_init failed: %s", esp_err_to_name(err)); + return false; + } + + if (esp_netif_init() != ESP_OK) { + ESP_LOGW(kTag, "esp_netif_init returned non-OK; continuing"); + } + if (esp_event_loop_create_default() != ESP_OK) { + ESP_LOGW(kTag, "event loop already exists or failed; continuing"); + } + + g_wifi_event_group = xEventGroupCreate(); + if (g_wifi_event_group == nullptr) { + ESP_LOGE(kTag, "failed to create Wi-Fi event group"); + return false; + } + + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + if (esp_wifi_init(&cfg) != ESP_OK) { + ESP_LOGE(kTag, "esp_wifi_init failed"); + return false; + } + + esp_event_handler_instance_t any_id; + esp_event_handler_instance_t got_ip; + esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + &WifiEventHandler, nullptr, &any_id); + esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, + &WifiEventHandler, nullptr, &got_ip); + + wifi_config_t wifi_config{}; + std::strncpy(reinterpret_cast(wifi_config.sta.ssid), WIFI_SSID, + sizeof(wifi_config.sta.ssid)); + std::strncpy(reinterpret_cast(wifi_config.sta.password), WIFI_PASSWORD, + sizeof(wifi_config.sta.password)); + + if (esp_wifi_set_mode(WIFI_MODE_STA) != ESP_OK || + esp_wifi_set_config(WIFI_IF_STA, &wifi_config) != ESP_OK || + esp_wifi_start() != ESP_OK) { + ESP_LOGE(kTag, "failed to start Wi-Fi STA"); + return false; + } + + EventBits_t bits = xEventGroupWaitBits( + g_wifi_event_group, kWifiConnectedBit | kWifiFailBit, pdFALSE, pdFALSE, + pdMS_TO_TICKS(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS)); + + if ((bits & kWifiConnectedBit) == 0) { + ESP_LOGE(kTag, "Wi-Fi hot path connect timeout/fail"); + esp_wifi_stop(); + return false; + } + + return true; +} + +#else + +bool EnsureWifiConnectedForHotPath() { + return true; +} + +#endif + +} // namespace + +char const* ToString(HotSendStatus status) { + switch (status) { + case HotSendStatus::kSent: + return "sent"; + case HotSendStatus::kNoPreparedBlock: + return "no-prepared-block"; + case HotSendStatus::kNonceExhausted: + return "nonce-exhausted"; + case HotSendStatus::kEncodeFailed: + return "encode-failed"; + case HotSendStatus::kPersistFailed: + return "persist-failed"; + case HotSendStatus::kWifiFailed: + return "wifi-failed"; + case HotSendStatus::kSendFailed: + return "send-failed"; + case HotSendStatus::kUnsupported: + return "unsupported"; + } + return "unknown"; +} + +ae::DataBuffer MakeTemperaturePayload(std::int16_t temperature) { + struct Header { + std::uint8_t const root_code = 0x3; + std::uint8_t const size = sizeof(std::uint8_t) + sizeof(std::int16_t); + std::uint8_t const dev_code = 0x10; + AE_REFLECT_MEMBERS(root_code, size, dev_code) + }; + + static constexpr auto header = Header{}; + + auto message = ae::DataBuffer{}; + message.reserve(sizeof(header) + sizeof(temperature)); + { + auto writer = ae::VectorWriter<>{message}; + auto stream = ae::omstream{writer}; + stream << header << temperature; + } + return message; +} + +bool HasPreparedSendBlock() { return RetainedLooksValid(); } + +void ClearPreparedSendBlock() { + std::memset(&g_retained_prepared_block, 0, sizeof(g_retained_prepared_block)); +} + +bool ExportPreparedSendBlock(ae::AetherApp& app, + ae::P2pStream& stream, + std::size_t reserve_nonce_count) { + auto prepared_block = + ae::prepared_packet::PrepareSendMessage(stream, reserve_nonce_count); + + if (!prepared_block) { + std::cerr << "[prepared-send] PrepareSendMessage failed\n"; + return false; + } + + // Critical ordering: + // PrepareSendMessage has already reserved/burned a nonce range in the full + // Aether state. Persist full Aether state only after reserve. + app.aether().Save(); + + if (!SerializeToRetained(*prepared_block)) { + std::cerr << "[prepared-send] failed to retain prepared block\n"; + return false; + } + + std::cout << "[prepared-send] exported prepared block, reserved " + << reserve_nonce_count << " nonces\n"; + return true; +} + +HotSendStatus TryHotWakePreparedSend(std::int16_t temperature) { + ae::prepared_packet::PreparedSendMessageBlock block; + if (!DeserializeFromRetained(block)) { + return HotSendStatus::kNoPreparedBlock; + } + + if (!EnsureWifiConnectedForHotPath()) { + return HotSendStatus::kWifiFailed; + } + + auto payload = MakeTemperaturePayload(temperature); + + ae::DataBuffer packet; + auto encode_result = ae::prepared_packet::EncodePacket(block, payload, packet); + + if (!encode_result) { + ClearPreparedSendBlock(); + return HotSendStatus::kEncodeFailed; + } + + // Persist immediately after EncodePacket, before UDP send/sleep, because + // EncodePacket consumes nonce state. + if (!SerializeToRetained(block)) { + return HotSendStatus::kPersistFailed; + } + +#if defined(ESP_PLATFORM) + auto const& endpoint = block.endpoint; + + sockaddr_in dest{}; + dest.sin_family = AF_INET; + dest.sin_port = htons(endpoint.port); + + if (inet_pton(AF_INET, endpoint.address.c_str(), &dest.sin_addr) != 1) { + std::cerr << "[prepared-send] invalid endpoint address\n"; + return HotSendStatus::kSendFailed; + } + + int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (sock < 0) { + return HotSendStatus::kSendFailed; + } + + auto sent = sendto(sock, packet.data(), packet.size(), 0, + reinterpret_cast(&dest), sizeof(dest)); + close(sock); + + if (sent != static_cast(packet.size())) { + return HotSendStatus::kSendFailed; + } + + std::cout << "[prepared-send] hot path UDP sent " << sent << " bytes\n"; + return HotSendStatus::kSent; +#else + return HotSendStatus::kUnsupported; +#endif +} + +} // namespace temp_sensor::prepared_send diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h new file mode 100644 index 0000000..c40e3d6 --- /dev/null +++ b/main/prepared_send/prepared_send.h @@ -0,0 +1,59 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Experimental prepared-send hot path for battery thermometer. + * + * Scope: + * - full boot prepares/exports PreparedSendMessageBlock for the service stream; + * - following ESP32 wakeups try to send one UDP prepared packet without + * constructing full AetherApp; + * - any error falls back to normal full Aether boot. + */ + +#ifndef TEMP_SENSOR_PREPARED_SEND_H_ +#define TEMP_SENSOR_PREPARED_SEND_H_ + +#include +#include + +#include "aether/all.h" + +namespace temp_sensor::prepared_send { + +enum class HotSendStatus { + kSent, + kNoPreparedBlock, + kNonceExhausted, + kEncodeFailed, + kPersistFailed, + kWifiFailed, + kSendFailed, + kUnsupported, +}; + +char const* ToString(HotSendStatus status); + +// Build the same binary temperature payload as SendValue(). +ae::DataBuffer MakeTemperaturePayload(std::int16_t temperature); + +// Try the MCU hot path. +// Returns kSent only if: +// - retained prepared block exists; +// - Wi-Fi was connected; +// - prepared packet was encoded; +// - mutated block was persisted after nonce consumption; +// - UDP datagram was sent. +HotSendStatus TryHotWakePreparedSend(std::int16_t temperature); + +// Export a new prepared block from the already initialized full Aether stream. +// Must be called only after full client/stream are usable. +bool ExportPreparedSendBlock(ae::AetherApp& app, + ae::P2pStream& stream, + std::size_t reserve_nonce_count); + +void ClearPreparedSendBlock(); +bool HasPreparedSendBlock(); + +} // namespace temp_sensor::prepared_send + +#endif // TEMP_SENSOR_PREPARED_SEND_H_ From c9a3cf8d63617a976bde1e4506dad0cdeee0516b Mon Sep 17 00:00:00 2001 From: Nikolay Chirkov Date: Tue, 30 Jun 2026 16:17:24 +0300 Subject: [PATCH 02/32] Fetch aether-client-cpp via CPM on prepared-packet-v0 branch. Remove the git submodule so the prepared-send integration tracks the branch with prepared packet support. Co-authored-by: Cursor --- .gitmodules | 3 --- README.md | 5 +---- aether-client-cpp | 1 - main/CMakeLists.txt | 10 ++-------- ulp/CMakeLists.txt | 2 +- 5 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 .gitmodules delete mode 160000 aether-client-cpp diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f0b438b..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "aether-client-cpp"] - path = aether-client-cpp - url = https://github.com/aethernetio/aether-client-cpp.git diff --git a/README.md b/README.md index 1f0e51b..cb61e6f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ ## Build Instructions ### Init dependencies -It uses git submodules to manage dependencies. -``` -git submodule update --init --remote ./aether-client-cpp -``` +`aether-client-cpp` is fetched automatically by [CPM.cmake](https://github.com/cpm-cmake/CPM.cmake) during the CMake configure step (branch `prepared-packet-v0`). ### For desktop For desktop a regular cmake project is used. diff --git a/aether-client-cpp b/aether-client-cpp deleted file mode 160000 index 00a5ba6..0000000 --- a/aether-client-cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 00a5ba648994298f60053cda47387eee299db1ad diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 6443bd6..e027ddf 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -48,10 +48,7 @@ if(NOT CM_PLATFORM) include(../cmake/CPM.cmake) - CPMAddPackage( - NAME aether-client-cpp - SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../aether-client-cpp" - ) + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#prepared-packet-v0") add_executable(${PROJECT_NAME} ${src_list} ${sleeping_src} ${sensors_src}) set(TARGET_NAME ${PROJECT_NAME}) @@ -81,10 +78,7 @@ else() set(TARGET_NAME "${COMPONENT_LIB}") include(../cmake/CPM.cmake) - CPMAddPackage( - NAME aether-client-cpp - SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../aether-client-cpp" - ) + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#prepared-packet-v0") target_link_libraries(${TARGET_NAME} PRIVATE aether) diff --git a/ulp/CMakeLists.txt b/ulp/CMakeLists.txt index 1e63d3e..7522442 100644 --- a/ulp/CMakeLists.txt +++ b/ulp/CMakeLists.txt @@ -52,7 +52,7 @@ ulp_add_build_binary_targets(${ULP_APP_NAME}) target_include_directories(${ULP_APP_NAME} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/../main") include(../cmake/CPM.cmake) -CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#main") +CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#prepared-packet-v0") # not link but only setup include directory for aether/config_consts.h target_include_directories(${ULP_APP_NAME} PUBLIC "${aether-client-cpp_SOURCE_DIR}") From 4ac8b9c3434178f2ae2c079109a26e7ef7121c0b Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Wed, 1 Jul 2026 17:31:35 +0300 Subject: [PATCH 03/32] Cursor work. --- main/CMakeLists.txt | 45 ++++++++------- main/boards/aether_esp32_c6.h | 4 +- main/boards/nano_esp32_c6.h | 1 - main/controller.cpp | 3 + main/prepared_send/prepared_send.cpp | 86 +++++++++++++++++++--------- main/sensors/utils.h | 10 +++- main/user_config.h | 2 +- 7 files changed, 99 insertions(+), 52 deletions(-) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 6443bd6..781e6c0 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -48,10 +48,7 @@ if(NOT CM_PLATFORM) include(../cmake/CPM.cmake) - CPMAddPackage( - NAME aether-client-cpp - SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../aether-client-cpp" - ) + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#main") add_executable(${PROJECT_NAME} ${src_list} ${sleeping_src} ${sensors_src}) set(TARGET_NAME ${PROJECT_NAME}) @@ -69,27 +66,22 @@ else() INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_CURRENT_SOURCE_DIR}/.." REQUIRES soc ulp PRIV_REQUIRES - esp_driver_tsens - esp_driver_gpio - esp_http_server - esp_event - esp_timer - esp_netif - esp_hw_support - driver) + idf::driver + idf::esp_driver_i2c + idf::esp_driver_tsens + idf::esp_driver_gpio + idf::esp_http_server + idf::esp_event + idf::esp_timer + idf::esp_netif + idf::esp_hw_support) set(TARGET_NAME "${COMPONENT_LIB}") - include(../cmake/CPM.cmake) - CPMAddPackage( - NAME aether-client-cpp - SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../aether-client-cpp" - ) + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#prepared-packet-v0") target_link_libraries(${TARGET_NAME} PRIVATE aether) - ulp_add_project("ulp_main" "${CMAKE_CURRENT_LIST_DIR}/../ulp") - if (NOT "${WIFI_SSID}" STREQUAL "") target_compile_definitions(${TARGET_NAME} PRIVATE "WIFI_SSID=\"${WIFI_SSID}\"") endif() @@ -100,6 +92,19 @@ else() target_compile_definitions(${TARGET_NAME} PRIVATE "BOARD=${BOARD}") endif() + # Define the path to the board configuration file in the build folder + set(ULP_BOARD_FILE "${CMAKE_BINARY_DIR}/ulp_board_config.cmake") + + # Write the value of the BOARD variable to this file + if(NOT "${BOARD}" STREQUAL "") + file(WRITE "${ULP_BOARD_FILE}" "set(BOARD \"${BOARD}\")\n") + else() + file(WRITE "${ULP_BOARD_FILE}" "set(BOARD \"\")\n") + endif() + + # We call the project addition as usual + ulp_add_project("ulp_main" "${CMAKE_CURRENT_LIST_DIR}/../ulp") + else() #Other platforms message(FATAL_ERROR "Platform ${CM_PLATFORM} is not supported") @@ -108,4 +113,4 @@ endif() if (NOT "${SERVICE_UID}" STREQUAL "") target_compile_definitions(${TARGET_NAME} PRIVATE "SERVICE_UID=\"${SERVICE_UID}\"") -endif() +endif() \ No newline at end of file diff --git a/main/boards/aether_esp32_c6.h b/main/boards/aether_esp32_c6.h index 5e813c5..62ffd0a 100644 --- a/main/boards/aether_esp32_c6.h +++ b/main/boards/aether_esp32_c6.h @@ -20,8 +20,6 @@ # error "Illegal CPU! It must be an ESP32C6." #endif -#include "hal/i2c_types.h" - #ifndef BOARD_HAS_ULP # define BOARD_HAS_ULP 0 #endif @@ -36,7 +34,7 @@ #endif // --- Sensors --- #define BOARD_HAS_SHTC3 0 -#define BOARD_HAS_SHT45 1 +#define BOARD_HAS_SHT45 0 #define BOARD_HAS_STCC4 1 #define BOARD_HAS_BME688 0 // --- Hardware Settings --- diff --git a/main/boards/nano_esp32_c6.h b/main/boards/nano_esp32_c6.h index d0ae5a5..cb9b0d2 100644 --- a/main/boards/nano_esp32_c6.h +++ b/main/boards/nano_esp32_c6.h @@ -20,7 +20,6 @@ # error "Illegal CPU! It must be an ESP32C6." #endif -#include "hal/i2c_types.h" #include "soc/gpio_num.h" #ifndef BOARD_HAS_ULP diff --git a/main/controller.cpp b/main/controller.cpp index 1e92265..acfd43c 100644 --- a/main/controller.cpp +++ b/main/controller.cpp @@ -188,6 +188,9 @@ void loop() { } } +void ReadSensors(int16_t* temperature, uint32_t* humidity, uint32_t* pressure, + uint32_t* co2, uint32_t* gas_resistance) {} + // implemented in sensors/ void UpdateSensors() { std::int16_t temperature = {}; diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index c88cd64..1fd6139 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -13,10 +13,13 @@ #include #include #include +#include +#include #include "aether/all.h" #include "aether/mstream.h" #include "aether/mstream_buffers.h" +#include "aether/prepared_packet/packet_encoder.h" #if defined(ESP_PLATFORM) # include @@ -36,10 +39,6 @@ namespace { static constexpr char const* kTag = "prepared-send"; -static constexpr std::uint32_t kMagic = 0x50534456; // "PSDV" -static constexpr std::uint32_t kVersion = 1; -static constexpr std::size_t kMaxPreparedBlockBytes = 4096; - #ifndef AETHER_PREPARED_NONCE_RESERVE # define AETHER_PREPARED_NONCE_RESERVE 32 #endif @@ -48,20 +47,16 @@ static constexpr std::size_t kMaxPreparedBlockBytes = 4096; # define AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS 15000 #endif -struct RetainedPreparedBlock { - std::uint32_t magic; - std::uint32_t version; - std::uint32_t size; - std::uint32_t checksum; - std::array bytes; -}; - #if defined(ESP_PLATFORM) // RTC_NOINIT_ATTR: do not zero on wake from deep sleep. // It may contain garbage on first boot, so magic/version/checksum validate it. -RTC_NOINIT_ATTR RetainedPreparedBlock g_retained_prepared_block; +RTC_NOINIT_ATTR ae::prepared_packet::RetainedPreparedBlock g_retained_prepared_block; + +// ESP32-C6 exposes 8 KiB RTC slow memory; the linker asserts the segment fits. +static_assert(sizeof(ae::prepared_packet::RetainedPreparedBlock) <= 8 * 1024, + "RetainedPreparedBlock must fit in ESP32 RTC slow memory"); #else -RetainedPreparedBlock g_retained_prepared_block; +ae::prepared_packet::RetainedPreparedBlock g_retained_prepared_block; #endif std::uint32_t Checksum(std::uint8_t const* data, std::size_t size) { @@ -75,7 +70,7 @@ std::uint32_t Checksum(std::uint8_t const* data, std::size_t size) { bool RetainedLooksValid() { auto const& r = g_retained_prepared_block; - if (r.magic != kMagic || r.version != kVersion) { + if (r.magic != ae::prepared_packet::kMagic || r.version != ae::prepared_packet::kVersion) { return false; } if (r.size == 0 || r.size > r.bytes.size()) { @@ -88,8 +83,8 @@ void SaveRawBlock(ae::DataBuffer const& bytes) { auto& r = g_retained_prepared_block; std::memset(&r, 0, sizeof(r)); - r.magic = kMagic; - r.version = kVersion; + r.magic = ae::prepared_packet::kMagic; + r.version = ae::prepared_packet::kVersion; r.size = static_cast(bytes.size()); std::memcpy(r.bytes.data(), bytes.data(), bytes.size()); r.checksum = Checksum(r.bytes.data(), r.size); @@ -116,7 +111,7 @@ bool SerializeToRetained(T const& value) { os << value; } - if (bytes.empty() || bytes.size() > kMaxPreparedBlockBytes) { + if (bytes.empty() || bytes.size() > ae::prepared_packet::kMaxPreparedBlockBytes) { std::cerr << "[prepared-send] prepared block serialized size invalid: " << bytes.size() << "\n"; return false; @@ -142,6 +137,31 @@ bool DeserializeFromRetained(T& value) { #if defined(ESP_PLATFORM) +bool FillUdpDestination(ae::prepared_packet::PreparedUdpEndpoint const& endpoint, + sockaddr* dest_addr, socklen_t* dest_len) { + if (endpoint.version == ae::prepared_packet::PreparedIpVersion::kIpV4) { + auto* dest = reinterpret_cast(dest_addr); + std::memset(dest, 0, sizeof(*dest)); + dest->sin_family = AF_INET; + dest->sin_port = htons(endpoint.port); + std::memcpy(&dest->sin_addr.s_addr, endpoint.ip.data(), 4); + *dest_len = sizeof(*dest); + return true; + } + + if (endpoint.version == ae::prepared_packet::PreparedIpVersion::kIpV6) { + auto* dest = reinterpret_cast(dest_addr); + std::memset(dest, 0, sizeof(*dest)); + dest->sin6_family = AF_INET6; + dest->sin6_port = htons(endpoint.port); + std::memcpy(dest->sin6_addr.s6_addr, endpoint.ip.data(), 16); + *dest_len = sizeof(*dest); + return true; + } + + return false; +} + static EventGroupHandle_t g_wifi_event_group = nullptr; static constexpr EventBits_t kWifiConnectedBit = BIT0; static constexpr EventBits_t kWifiFailBit = BIT1; @@ -292,11 +312,22 @@ void ClearPreparedSendBlock() { std::memset(&g_retained_prepared_block, 0, sizeof(g_retained_prepared_block)); } +std::optional PrepareSendMessage( + ae::P2pStream& stream, std::size_t reserve_nonce_count) { + if (reserve_nonce_count == 0 || + reserve_nonce_count > std::numeric_limits::max()) { + return std::nullopt; + } + + return stream.ExportPreparedSendMessageBlock( + static_cast(reserve_nonce_count)); +} + bool ExportPreparedSendBlock(ae::AetherApp& app, ae::P2pStream& stream, std::size_t reserve_nonce_count) { auto prepared_block = - ae::prepared_packet::PrepareSendMessage(stream, reserve_nonce_count); + PrepareSendMessage(stream, reserve_nonce_count); if (!prepared_block) { std::cerr << "[prepared-send] PrepareSendMessage failed\n"; @@ -347,22 +378,25 @@ HotSendStatus TryHotWakePreparedSend(std::int16_t temperature) { #if defined(ESP_PLATFORM) auto const& endpoint = block.endpoint; - sockaddr_in dest{}; - dest.sin_family = AF_INET; - dest.sin_port = htons(endpoint.port); - - if (inet_pton(AF_INET, endpoint.address.c_str(), &dest.sin_addr) != 1) { + sockaddr_storage dest_storage{}; + socklen_t dest_len = 0; + if (!FillUdpDestination(endpoint, reinterpret_cast(&dest_storage), + &dest_len)) { std::cerr << "[prepared-send] invalid endpoint address\n"; return HotSendStatus::kSendFailed; } - int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + int sock = socket( + endpoint.version == ae::prepared_packet::PreparedIpVersion::kIpV6 + ? AF_INET6 + : AF_INET, + SOCK_DGRAM, IPPROTO_IP); if (sock < 0) { return HotSendStatus::kSendFailed; } auto sent = sendto(sock, packet.data(), packet.size(), 0, - reinterpret_cast(&dest), sizeof(dest)); + reinterpret_cast(&dest_storage), dest_len); close(sock); if (sent != static_cast(packet.size())) { diff --git a/main/sensors/utils.h b/main/sensors/utils.h index 4757403..febc790 100644 --- a/main/sensors/utils.h +++ b/main/sensors/utils.h @@ -17,8 +17,16 @@ #if defined ESP_PLATFORM # include -# include # include +# if ULP_COMP == 1 +# include +# define I2C_BUS_HANDLE int +# define I2C_HANDLE_PORT i2c_port_t +# elif ULP_COMP == 0 +# include +# define I2C_BUS_HANDLE i2c_master_bus_handle_t* +# define I2C_HANDLE_PORT i2c_master_dev_handle_t +# endif # ifdef __cplusplus extern "C" { diff --git a/main/user_config.h b/main/user_config.h index 8579c3e..41591b2 100644 --- a/main/user_config.h +++ b/main/user_config.h @@ -54,7 +54,7 @@ # define BOARD_M5STACK_ATOM_LITE 4 # ifndef BOARD -# define BOARD BOARD_NANO_ESP32_C6 +# define BOARD BOARD_AETHER_ESP32_C6 # endif # if BOARD == BOARD_AETHER_ESP32_C6 From 9623f3373a4882cbd94243f0bbc2fa61f2f4a87f Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Thu, 2 Jul 2026 14:50:35 +0300 Subject: [PATCH 04/32] Updating PreparedSend functionality. --- main/controller.cpp | 9 +++++---- main/prepared_send/prepared_send.cpp | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/main/controller.cpp b/main/controller.cpp index acfd43c..4e2757b 100644 --- a/main/controller.cpp +++ b/main/controller.cpp @@ -61,7 +61,7 @@ void SendValue(std::int16_t temperature); void GoToSleep(ae::Uap::Timer uap_timer); static ae::RcPtr aether_app; -static ae::RcPtr message_stream; +static std::unique_ptr message_stream; #ifndef AETHER_PREPARED_HOT_SLEEP_SECONDS # define AETHER_PREPARED_HOT_SLEEP_SECONDS 600 @@ -159,8 +159,9 @@ void setup() { c->uid(), kServiceUid); // open message stream to aether service client - message_stream = - c->message_stream_manager().CreateStream(kServiceUid); + message_stream = std::make_unique( + *aether_app, c, kServiceUid, + c->message_stream_manager().CreatePort(kServiceUid)); message_stream->out_data_event().Subscribe(MessageReceived); // measure temperature and send updated value @@ -183,7 +184,7 @@ void loop() { aether_app->WaitUntil(new_time); } else { // cleanup resources - message_stream.Reset(); + message_stream.reset(); aether_app.Reset(); } } diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index 1fd6139..4f5378c 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -137,8 +137,8 @@ bool DeserializeFromRetained(T& value) { #if defined(ESP_PLATFORM) -bool FillUdpDestination(ae::prepared_packet::PreparedUdpEndpoint const& endpoint, - sockaddr* dest_addr, socklen_t* dest_len) { +bool FillUdpDestination(ae::prepared_packet::PreparedEndpoint const& endpoint, + sockaddr* dest_addr, socklen_t* dest_len) { if (endpoint.version == ae::prepared_packet::PreparedIpVersion::kIpV4) { auto* dest = reinterpret_cast(dest_addr); std::memset(dest, 0, sizeof(*dest)); From 2bd82145a2d0bed571a56d71a1fd2fdeaa5e3e3a Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Fri, 3 Jul 2026 10:55:45 +0300 Subject: [PATCH 05/32] Test. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cb61e6f..1940401 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,5 @@ ESP is using wifi connection for internet. Edit platformio.ini to match your board and set ssid and password. ```sh pio run -e -t upload -``` \ No newline at end of file +``` +Test \ No newline at end of file From 3680297f3c07a2bc659cd7198c97692853088927 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Fri, 3 Jul 2026 11:01:07 +0300 Subject: [PATCH 06/32] Removing Test. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 1940401..cb61e6f 100644 --- a/README.md +++ b/README.md @@ -31,5 +31,4 @@ ESP is using wifi connection for internet. Edit platformio.ini to match your board and set ssid and password. ```sh pio run -e -t upload -``` -Test \ No newline at end of file +``` \ No newline at end of file From 951d56f7c5b6c14acf2df98a4d1429277a246f87 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Fri, 3 Jul 2026 21:09:56 +0300 Subject: [PATCH 07/32] Changed temperature format from std:uint16_t to std::string and added 1000 mS delay. --- main/controller.cpp | 27 +++++++++++++++++---------- main/prepared_send/prepared_send.cpp | 6 +++--- main/prepared_send/prepared_send.h | 4 ++-- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/main/controller.cpp b/main/controller.cpp index 4e2757b..23d0d42 100644 --- a/main/controller.cpp +++ b/main/controller.cpp @@ -37,7 +37,7 @@ static constexpr auto kServiceUid = #ifdef SERVICE_UID ae::Uid::FromString(SERVICE_UID); #else - ae::Uid::FromString("e839f1a9-e0ec-4ff6-b85c-d49efaabf24f"); + ae::Uid::FromString("c6f15489-09cb-4c59-a8aa-eadd7abe6ae3"); #endif #ifdef ESP_PLATFORM @@ -56,7 +56,7 @@ void UpdateSensors(); // Message from aether service received void MessageReceived(ae::DataBuffer const& buffer); // Send the message value to the aether service -void SendValue(std::int16_t temperature); +void SendValue(std::string temperature); // Go to sleep method void GoToSleep(ae::Uap::Timer uap_timer); @@ -64,7 +64,7 @@ static ae::RcPtr aether_app; static std::unique_ptr message_stream; #ifndef AETHER_PREPARED_HOT_SLEEP_SECONDS -# define AETHER_PREPARED_HOT_SLEEP_SECONDS 600 +# define AETHER_PREPARED_HOT_SLEEP_SECONDS 30 #endif static constexpr auto kPreparedHotSleepSeconds = @@ -77,6 +77,10 @@ static constexpr std::size_t kPreparedNonceReserve = 32; #endif +void ReadHotSensors(std::string* temperature, uint32_t* humidity, uint32_t* pressure, + uint32_t* co2, uint32_t* gas_resistance) { + *temperature = "Hot 25.67"; + } void setup() { std::cout << ae::Format("Setup {:%Y-%m-%d %H:%M:%S}") << ae::Now() @@ -86,8 +90,8 @@ void setup() { // Prepared-send hot path: try to send current temperature without creating // full AetherApp. Any error falls back to the normal full boot below. { - std::int16_t hot_temperature = {}; - ReadSensors(&hot_temperature, nullptr, nullptr, nullptr, nullptr); + std::string hot_temperature = {}; + ReadHotSensors(&hot_temperature, nullptr, nullptr, nullptr, nullptr); auto hot_status = temp_sensor::prepared_send::TryHotWakePreparedSend(hot_temperature); @@ -189,12 +193,14 @@ void loop() { } } -void ReadSensors(int16_t* temperature, uint32_t* humidity, uint32_t* pressure, - uint32_t* co2, uint32_t* gas_resistance) {} +void ReadSensors(std::string* temperature, uint32_t* humidity, uint32_t* pressure, + uint32_t* co2, uint32_t* gas_resistance) { + *temperature = "25.67"; + } // implemented in sensors/ void UpdateSensors() { - std::int16_t temperature = {}; + std::string temperature = {}; ReadSensors(&temperature, nullptr, nullptr, nullptr, nullptr); std::cout << ae::Format(" >>> Temperature: [{}]\n", temperature); // TODO: add check if wakeup cause is ulp then send value @@ -206,7 +212,7 @@ void MessageReceived(ae::DataBuffer const& buffer) { std::cout << ae::Format(" >>> Received message from service: [{}]\n", buffer); } -void SendValue(std::int16_t temperature) { +void SendValue(std::string temperature) { // The stream is not initialized yet if (!message_stream) { return; @@ -233,7 +239,7 @@ void GoToSleep(ae::Uap::Timer uap_timer) { if (!aether_app) { return; } - + // get the interval with the specified offset // offset is required to account the Save operation auto interval = uap_timer.interval(std::chrono::seconds{10}); @@ -245,6 +251,7 @@ void GoToSleep(ae::Uap::Timer uap_timer) { " >>> Sleep from {:%Y-%m-%d %H:%M:%S} until {:%Y-%m-%d %H:%M:%S}...\n", ae::Now(), sleep_until); // TODO: add separate sleep duration + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); DeepSleep(interval.until(), interval.until(), 3000); // wait till time or 30 deegrees } diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index 4f5378c..744c54d 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -40,7 +40,7 @@ namespace { static constexpr char const* kTag = "prepared-send"; #ifndef AETHER_PREPARED_NONCE_RESERVE -# define AETHER_PREPARED_NONCE_RESERVE 32 +# define AETHER_PREPARED_NONCE_RESERVE 30 #endif #ifndef AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS @@ -286,7 +286,7 @@ char const* ToString(HotSendStatus status) { return "unknown"; } -ae::DataBuffer MakeTemperaturePayload(std::int16_t temperature) { +ae::DataBuffer MakeTemperaturePayload(std::string temperature) { struct Header { std::uint8_t const root_code = 0x3; std::uint8_t const size = sizeof(std::uint8_t) + sizeof(std::int16_t); @@ -349,7 +349,7 @@ bool ExportPreparedSendBlock(ae::AetherApp& app, return true; } -HotSendStatus TryHotWakePreparedSend(std::int16_t temperature) { +HotSendStatus TryHotWakePreparedSend(std::string temperature) { ae::prepared_packet::PreparedSendMessageBlock block; if (!DeserializeFromRetained(block)) { return HotSendStatus::kNoPreparedBlock; diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h index c40e3d6..b7990f7 100644 --- a/main/prepared_send/prepared_send.h +++ b/main/prepared_send/prepared_send.h @@ -34,7 +34,7 @@ enum class HotSendStatus { char const* ToString(HotSendStatus status); // Build the same binary temperature payload as SendValue(). -ae::DataBuffer MakeTemperaturePayload(std::int16_t temperature); +ae::DataBuffer MakeTemperaturePayload(std::string temperature); // Try the MCU hot path. // Returns kSent only if: @@ -43,7 +43,7 @@ ae::DataBuffer MakeTemperaturePayload(std::int16_t temperature); // - prepared packet was encoded; // - mutated block was persisted after nonce consumption; // - UDP datagram was sent. -HotSendStatus TryHotWakePreparedSend(std::int16_t temperature); +HotSendStatus TryHotWakePreparedSend(std::string temperature); // Export a new prepared block from the already initialized full Aether stream. // Must be called only after full client/stream are usable. From 1bcd5052472ceccaa5a403021405565ceb24cb30 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Thu, 9 Jul 2026 17:57:03 +0300 Subject: [PATCH 08/32] Fixing WiFi connection. --- main/controller.cpp | 11 +- main/prepared_send/prepared_send.cpp | 210 +++++++++++++++++++++++---- 2 files changed, 191 insertions(+), 30 deletions(-) diff --git a/main/controller.cpp b/main/controller.cpp index 23d0d42..61f06ac 100644 --- a/main/controller.cpp +++ b/main/controller.cpp @@ -37,7 +37,7 @@ static constexpr auto kServiceUid = #ifdef SERVICE_UID ae::Uid::FromString(SERVICE_UID); #else - ae::Uid::FromString("c6f15489-09cb-4c59-a8aa-eadd7abe6ae3"); + ae::Uid::FromString("ce61234d-de02-46e0-804d-ae7cb0900f3d"); #endif #ifdef ESP_PLATFORM @@ -47,7 +47,7 @@ static const auto kWifiCreds = ae::WifiCreds{ }; static const auto kWifiInit = ae::WiFiInit{ std::vector{{kWifiCreds, {}}}, - ae::WiFiPowerSaveParam{}, + {}, }; #endif @@ -79,7 +79,7 @@ static constexpr std::size_t kPreparedNonceReserve = void ReadHotSensors(std::string* temperature, uint32_t* humidity, uint32_t* pressure, uint32_t* co2, uint32_t* gas_resistance) { - *temperature = "Hot 25.67"; + *temperature = "Prepared 25.67"; } void setup() { @@ -195,7 +195,7 @@ void loop() { void ReadSensors(std::string* temperature, uint32_t* humidity, uint32_t* pressure, uint32_t* co2, uint32_t* gas_resistance) { - *temperature = "25.67"; + *temperature = "Full 25.67"; } // implemented in sensors/ @@ -219,6 +219,8 @@ void SendValue(std::string temperature) { } auto message = temp_sensor::prepared_send::MakeTemperaturePayload(temperature); + std::cout << ae::Format(" [CALL-CHAIN] SendValue payload_size={}\n", + message.size()); message_stream->Write(std::move(message)).status_event().Subscribe([](auto) { // Export/refresh prepared block after the full send path has a valid stream. @@ -251,7 +253,6 @@ void GoToSleep(ae::Uap::Timer uap_timer) { " >>> Sleep from {:%Y-%m-%d %H:%M:%S} until {:%Y-%m-%d %H:%M:%S}...\n", ae::Now(), sleep_until); // TODO: add separate sleep duration - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); DeepSleep(interval.until(), interval.until(), 3000); // wait till time or 30 deegrees } diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index 744c54d..94abdeb 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -11,10 +11,12 @@ #include "prepared_send/prepared_send.h" #include +#include #include #include #include #include +#include #include "aether/all.h" #include "aether/mstream.h" @@ -27,6 +29,7 @@ # include # include # include +# include # include # include # include @@ -47,6 +50,10 @@ static constexpr char const* kTag = "prepared-send"; # define AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS 15000 #endif +#ifndef AETHER_PREPARED_HOT_WIFI_MAX_RETRY +# define AETHER_PREPARED_HOT_WIFI_MAX_RETRY 10 +#endif + #if defined(ESP_PLATFORM) // RTC_NOINIT_ATTR: do not zero on wake from deep sleep. // It may contain garbage on first boot, so magic/version/checksum validate it. @@ -163,21 +170,122 @@ bool FillUdpDestination(ae::prepared_packet::PreparedEndpoint const& endpoint, } static EventGroupHandle_t g_wifi_event_group = nullptr; +static esp_netif_t* g_wifi_netif = nullptr; +static esp_event_handler_instance_t g_wifi_any_id_handler = nullptr; +static esp_event_handler_instance_t g_wifi_got_ip_handler = nullptr; +static bool g_wifi_initialized = false; +static bool g_wifi_started = false; +static bool g_default_event_loop_created = false; +static int g_wifi_retry_count = 0; static constexpr EventBits_t kWifiConnectedBit = BIT0; static constexpr EventBits_t kWifiFailBit = BIT1; void WifiEventHandler(void*, esp_event_base_t event_base, - std::int32_t event_id, void*) { + std::int32_t event_id, void* event_data) { + if (g_wifi_event_group == nullptr) { + return; + } + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { - esp_wifi_connect(); + g_wifi_retry_count = 0; + auto err = esp_wifi_connect(); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Wi-Fi hot path connect start failed: %s", + esp_err_to_name(err)); + xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); + } } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { - xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); + auto const* event = + static_cast(event_data); + auto const reason = event != nullptr ? static_cast(event->reason) : -1; + + if (g_wifi_retry_count < AETHER_PREPARED_HOT_WIFI_MAX_RETRY) { + ++g_wifi_retry_count; + ESP_LOGW(kTag, + "Wi-Fi hot path disconnected reason=%d; retry %d/%d", + reason, g_wifi_retry_count, + static_cast(AETHER_PREPARED_HOT_WIFI_MAX_RETRY)); + + auto err = esp_wifi_connect(); + if (err != ESP_OK) { + ESP_LOGE(kTag, "Wi-Fi hot path reconnect failed: %s", + esp_err_to_name(err)); + xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); + } + } else { + ESP_LOGE(kTag, "Wi-Fi hot path retry limit reached; reason=%d", reason); + xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); + } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ESP_LOGI(kTag, "Wi-Fi hot path connected after %d retries", + g_wifi_retry_count); xEventGroupSetBits(g_wifi_event_group, kWifiConnectedBit); } } +void CleanupHotPathWifi() { + if (g_wifi_any_id_handler != nullptr) { + auto err = esp_event_handler_instance_unregister( + WIFI_EVENT, ESP_EVENT_ANY_ID, g_wifi_any_id_handler); + if (err != ESP_OK) { + ESP_LOGW(kTag, "failed to unregister WIFI handler: %s", + esp_err_to_name(err)); + } + g_wifi_any_id_handler = nullptr; + } + + if (g_wifi_got_ip_handler != nullptr) { + auto err = esp_event_handler_instance_unregister( + IP_EVENT, IP_EVENT_STA_GOT_IP, g_wifi_got_ip_handler); + if (err != ESP_OK) { + ESP_LOGW(kTag, "failed to unregister IP handler: %s", + esp_err_to_name(err)); + } + g_wifi_got_ip_handler = nullptr; + } + + if (g_wifi_started) { + auto err = esp_wifi_stop(); + if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_STARTED && + err != ESP_ERR_WIFI_NOT_INIT) { + ESP_LOGW(kTag, "esp_wifi_stop failed during cleanup: %s", + esp_err_to_name(err)); + } + g_wifi_started = false; + } + + if (g_wifi_initialized) { + auto err = esp_wifi_deinit(); + if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_INIT) { + ESP_LOGW(kTag, "esp_wifi_deinit failed during cleanup: %s", + esp_err_to_name(err)); + } + g_wifi_initialized = false; + } + + if (g_wifi_netif != nullptr) { + esp_netif_destroy_default_wifi(g_wifi_netif); + g_wifi_netif = nullptr; + } + + if (g_wifi_event_group != nullptr) { + vEventGroupDelete(g_wifi_event_group); + g_wifi_event_group = nullptr; + } + + g_wifi_retry_count = 0; + + if (g_default_event_loop_created) { + auto err = esp_event_loop_delete_default(); + if (err != ESP_OK) { + ESP_LOGW(kTag, "esp_event_loop_delete_default failed: %s", + esp_err_to_name(err)); + } + g_default_event_loop_created = false; + } +} + bool EnsureWifiConnectedForHotPath() { #ifndef WIFI_SSID ESP_LOGE(kTag, "WIFI_SSID is not defined"); @@ -200,33 +308,63 @@ bool EnsureWifiConnectedForHotPath() { return false; } + CleanupHotPathWifi(); + if (esp_netif_init() != ESP_OK) { ESP_LOGW(kTag, "esp_netif_init returned non-OK; continuing"); } - if (esp_event_loop_create_default() != ESP_OK) { - ESP_LOGW(kTag, "event loop already exists or failed; continuing"); + err = esp_event_loop_create_default(); + if (err == ESP_OK) { + g_default_event_loop_created = true; + } else if (err == ESP_ERR_INVALID_STATE) { + ESP_LOGW(kTag, "event loop already exists; continuing"); + } else { + ESP_LOGE(kTag, "esp_event_loop_create_default failed: %s", + esp_err_to_name(err)); + return false; } g_wifi_event_group = xEventGroupCreate(); if (g_wifi_event_group == nullptr) { ESP_LOGE(kTag, "failed to create Wi-Fi event group"); + CleanupHotPathWifi(); return false; } - esp_netif_create_default_wifi_sta(); + g_wifi_netif = esp_netif_create_default_wifi_sta(); + if (g_wifi_netif == nullptr) { + ESP_LOGE(kTag, "failed to create default Wi-Fi STA netif"); + CleanupHotPathWifi(); + return false; + } wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - if (esp_wifi_init(&cfg) != ESP_OK) { - ESP_LOGE(kTag, "esp_wifi_init failed"); + err = esp_wifi_init(&cfg); + if (err != ESP_OK) { + ESP_LOGE(kTag, "esp_wifi_init failed: %s", esp_err_to_name(err)); + CleanupHotPathWifi(); + return false; + } + g_wifi_initialized = true; + + err = esp_event_handler_instance_register( + WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiEventHandler, nullptr, + &g_wifi_any_id_handler); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to register WIFI handler: %s", + esp_err_to_name(err)); + CleanupHotPathWifi(); return false; } - esp_event_handler_instance_t any_id; - esp_event_handler_instance_t got_ip; - esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, - &WifiEventHandler, nullptr, &any_id); - esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, - &WifiEventHandler, nullptr, &got_ip); + err = esp_event_handler_instance_register( + IP_EVENT, IP_EVENT_STA_GOT_IP, &WifiEventHandler, nullptr, + &g_wifi_got_ip_handler); + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to register IP handler: %s", esp_err_to_name(err)); + CleanupHotPathWifi(); + return false; + } wifi_config_t wifi_config{}; std::strncpy(reinterpret_cast(wifi_config.sta.ssid), WIFI_SSID, @@ -234,20 +372,35 @@ bool EnsureWifiConnectedForHotPath() { std::strncpy(reinterpret_cast(wifi_config.sta.password), WIFI_PASSWORD, sizeof(wifi_config.sta.password)); - if (esp_wifi_set_mode(WIFI_MODE_STA) != ESP_OK || - esp_wifi_set_config(WIFI_IF_STA, &wifi_config) != ESP_OK || - esp_wifi_start() != ESP_OK) { - ESP_LOGE(kTag, "failed to start Wi-Fi STA"); + err = esp_wifi_set_mode(WIFI_MODE_STA); + if (err != ESP_OK) { + ESP_LOGE(kTag, "esp_wifi_set_mode failed: %s", esp_err_to_name(err)); + CleanupHotPathWifi(); return false; } + err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); + if (err != ESP_OK) { + ESP_LOGE(kTag, "esp_wifi_set_config failed: %s", esp_err_to_name(err)); + CleanupHotPathWifi(); + return false; + } + + err = esp_wifi_start(); + if (err != ESP_OK) { + ESP_LOGE(kTag, "esp_wifi_start failed: %s", esp_err_to_name(err)); + CleanupHotPathWifi(); + return false; + } + g_wifi_started = true; + EventBits_t bits = xEventGroupWaitBits( g_wifi_event_group, kWifiConnectedBit | kWifiFailBit, pdFALSE, pdFALSE, pdMS_TO_TICKS(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS)); if ((bits & kWifiConnectedBit) == 0) { ESP_LOGE(kTag, "Wi-Fi hot path connect timeout/fail"); - esp_wifi_stop(); + CleanupHotPathWifi(); return false; } @@ -260,6 +413,8 @@ bool EnsureWifiConnectedForHotPath() { return true; } +void CleanupHotPathWifi() {} + #endif } // namespace @@ -359,6 +514,11 @@ HotSendStatus TryHotWakePreparedSend(std::string temperature) { return HotSendStatus::kWifiFailed; } + auto fail_after_wifi = [](HotSendStatus status) { + CleanupHotPathWifi(); + return status; + }; + auto payload = MakeTemperaturePayload(temperature); ae::DataBuffer packet; @@ -366,13 +526,13 @@ HotSendStatus TryHotWakePreparedSend(std::string temperature) { if (!encode_result) { ClearPreparedSendBlock(); - return HotSendStatus::kEncodeFailed; + return fail_after_wifi(HotSendStatus::kEncodeFailed); } // Persist immediately after EncodePacket, before UDP send/sleep, because // EncodePacket consumes nonce state. if (!SerializeToRetained(block)) { - return HotSendStatus::kPersistFailed; + return fail_after_wifi(HotSendStatus::kPersistFailed); } #if defined(ESP_PLATFORM) @@ -383,7 +543,7 @@ HotSendStatus TryHotWakePreparedSend(std::string temperature) { if (!FillUdpDestination(endpoint, reinterpret_cast(&dest_storage), &dest_len)) { std::cerr << "[prepared-send] invalid endpoint address\n"; - return HotSendStatus::kSendFailed; + return fail_after_wifi(HotSendStatus::kSendFailed); } int sock = socket( @@ -392,7 +552,7 @@ HotSendStatus TryHotWakePreparedSend(std::string temperature) { : AF_INET, SOCK_DGRAM, IPPROTO_IP); if (sock < 0) { - return HotSendStatus::kSendFailed; + return fail_after_wifi(HotSendStatus::kSendFailed); } auto sent = sendto(sock, packet.data(), packet.size(), 0, @@ -400,9 +560,9 @@ HotSendStatus TryHotWakePreparedSend(std::string temperature) { close(sock); if (sent != static_cast(packet.size())) { - return HotSendStatus::kSendFailed; + return fail_after_wifi(HotSendStatus::kSendFailed); } - + std::this_thread::sleep_for(std::chrono::milliseconds(450)); std::cout << "[prepared-send] hot path UDP sent " << sent << " bytes\n"; return HotSendStatus::kSent; #else From 3a82d18e8b5fe2818c7392bf8d6ca06e46a36a31 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Fri, 10 Jul 2026 12:56:51 +0300 Subject: [PATCH 09/32] Removing UAP. --- main/controller.cpp | 176 ++++++++++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 62 deletions(-) diff --git a/main/controller.cpp b/main/controller.cpp index 61f06ac..ba72f46 100644 --- a/main/controller.cpp +++ b/main/controller.cpp @@ -22,6 +22,8 @@ #include "sleeping/sleeping.h" #include "prepared_send/prepared_send.h" +using namespace std::chrono_literals; + /** * Standard uid for test application. * This is intended to use only for testing purposes due to its limitations. @@ -37,7 +39,7 @@ static constexpr auto kServiceUid = #ifdef SERVICE_UID ae::Uid::FromString(SERVICE_UID); #else - ae::Uid::FromString("ce61234d-de02-46e0-804d-ae7cb0900f3d"); + ae::Uid::FromString("f763febf-b555-487c-bf0f-c3813a1398d2"); #endif #ifdef ESP_PLATFORM @@ -51,16 +53,42 @@ static const auto kWifiInit = ae::WiFiInit{ }; #endif +struct WorkMode { + using type = std::uint8_t; + static constexpr type kTx = 0x1, kRx = 0x2, kTxRx = 0x3; + + static std::string_view ToText(type v) { + switch (v) { + case WorkMode::kTx: + return "TX"; + case WorkMode::kRx: + return "RX"; + case WorkMode::kTxRx: + return "TX+RX"; + default: + return "NONE"; + } + } +}; + +static constexpr ae::Duration kTxInterval = 30s; +static RTC_STORAGE_ATTR ae::TimePoint next_tx_time = {}; + +// Client selection handler +void ClientSelected(ae::Result res); // Update temperature sensor void UpdateSensors(); // Message from aether service received void MessageReceived(ae::DataBuffer const& buffer); // Send the message value to the aether service void SendValue(std::string temperature); +// Make all required work and ready to sleep +void SleepReady(); // Go to sleep method -void GoToSleep(ae::Uap::Timer uap_timer); +void GoToSleep(ae::TimePoint time_point); static ae::RcPtr aether_app; +static ae::Client::ptr client; static std::unique_ptr message_stream; #ifndef AETHER_PREPARED_HOT_SLEEP_SECONDS @@ -122,60 +150,13 @@ void setup() { kWifiInit); }) # endif - .UapFactory([](ae::AetherAppContext const& context) { - auto uap = context.aether()->uap; - if (uap.is_valid()) { - return uap; - } - // configure uap - // 60secs for send/receive - // then 2 times by 30 seconds for send only - return ae::Uap::ptr::Create( - ae::CreateWith{context.domain()}.with_id(ae::GlobalId::kUap), - context.aether(), - std::initializer_list{ - ae::Interval{.type = ae::IntervalType::kSendReceive, - .duration = std::chrono::seconds{60}, - .window = std::chrono::seconds{10}}, - ae::Interval{.type = ae::IntervalType::kSendOnly, - .duration = std::chrono::seconds{30}}, - ae::Interval{.type = ae::IntervalType::kSendOnly, - .duration = std::chrono::seconds{30}}}); - }) #endif ); - // setup sleep on uap event - aether_app->aether()->uap->sleep_event().Subscribe(GoToSleep); - // select controller's client - auto& select_client = - aether_app->aether()->SelectClient(kParentUid, "Controller"); - - select_client.result_event().Subscribe( - [&](ae::Result&& res) { - if (res) { - ae::Client::ptr client = std::move(res).value(); - client.WithLoaded([](auto const& c) { - std::cout << Format( - "\n\n>>>>>>>\n>>>>>>> Client Loaded UID:{} \n>>>>>>> Visit " - "https://aethernet.io/smarthub.html?uuid={} \n<<<<<\n\n", - c->uid(), kServiceUid); - - // open message stream to aether service client - message_stream = std::make_unique( - *aether_app, c, kServiceUid, - c->message_stream_manager().CreatePort(kServiceUid)); - message_stream->out_data_event().Subscribe(MessageReceived); - - // measure temperature and send updated value - UpdateSensors(); - }); - } else { - std::cerr << " !!! Client selection error"; - aether_app->Exit(1); - } - }); + aether_app->aether()->SelectClient(kParentUid, "Controller") + .result_event() + .Subscribe(&ClientSelected); } void loop() { @@ -193,6 +174,60 @@ void loop() { } } +void ClientSelected(ae::Result res) { + if (!res) { + std::cerr << " !!! Client selection error\n"; + aether_app->Exit(1); + return; + } + + client = std::move(res).value(); + auto r = client.WithLoaded([](ae::Ptr const& c) { + std::cout << ae::Format( + "\n\n>>>>>>>\n>>>>>>> Client Loaded UID:{} \n>>>>>>> Visit " + "https://aethernet.io/smarthub.html?uuid={} \n<<<<<\n\n", + c->uid(), kServiceUid); + + // Config connectivity policy, open 5s RX window every 60s. + c->connectivity_policy() + ->ConfigureRxTimings(ae::RequestPolicy::All{}) + .ForAllPriorities(ae::RxTimingConf::Every(60s).WithWindow(5s)); + + // check current work_mode + // it's always TX if we woke up + auto work_mode = WorkMode::kTx; + auto current_time = ae::Now(); + static constexpr ae::Duration threshold = 5s; + auto cp_status = c->connectivity_policy()->GetStatus(); + // if it's next_service_time it's also RX + if ((current_time + threshold) >= cp_status.next_service_time) { + work_mode = work_mode | WorkMode::kRx; + } + std::cout << ae::Format(">>>> Run in {} work mode\n", + WorkMode::ToText(work_mode)); + + if ((work_mode & WorkMode::kRx) != 0) { + // open message stream for receive and send + message_stream = std::make_unique( + *aether_app, c, kServiceUid, + c->message_stream_manager().CreatePort(kServiceUid)); + message_stream->out_data_event().Subscribe(MessageReceived); + } else { + // open message stream for send only + message_stream = std::make_unique( + *aether_app, c, kServiceUid, ae::P2pPortHandle{}); + } + + // measure temperature and send updated value + UpdateSensors(); + }); + + if (!r) { + std::cerr << " !!! Client wasn't loaded"; + aether_app->Exit(2); + } +} + void ReadSensors(std::string* temperature, uint32_t* humidity, uint32_t* pressure, uint32_t* co2, uint32_t* gas_resistance) { *temperature = "Full 25.67"; @@ -231,28 +266,45 @@ void SendValue(std::string temperature) { } // with any result ready to sleep - aether_app->aether()->uap->SleepReady(); + SleepReady(); }); + + next_tx_time = ae::Now() + kTxInterval; } -void GoToSleep(ae::Uap::Timer uap_timer) { +void SleepReady() { + auto go_to_sleep = [](auto next_rx_time) noexcept { + auto next_time = std::min(next_tx_time, next_rx_time); + std::cout << ae::Format( + ">> Go to sleep no wait, next_tx_time {}, next_rx_time {}\n", + next_tx_time, next_rx_time); + GoToSleep(next_time); + }; + + auto status = client->connectivity_policy()->GetStatus(); + if (status.can_suspend) { + go_to_sleep(status.next_service_time); + } else { + std::cout << ">>> Wait for can suspend\n"; + client->connectivity_policy()->suspend_allowed_event().Subscribe([&]() { + go_to_sleep(client->connectivity_policy()->GetStatus().next_service_time); + }); + } +} + +void GoToSleep(ae::TimePoint time_point) { std::cout << " >>> Going to sleep...\n"; if (!aether_app) { return; } - - // get the interval with the specified offset - // offset is required to account the Save operation - auto interval = uap_timer.interval(std::chrono::seconds{10}); // save current aether state aether_app->aether().Save(); + // Go to sleep - auto sleep_until = interval.until(); std::cout << ae::Format( " >>> Sleep from {:%Y-%m-%d %H:%M:%S} until {:%Y-%m-%d %H:%M:%S}...\n", - ae::Now(), sleep_until); + ae::Now(), time_point); // TODO: add separate sleep duration - DeepSleep(interval.until(), interval.until(), - 3000); // wait till time or 30 deegrees + DeepSleep(time_point, time_point, 3000); // wait till time or 30 deegrees } From 31617dc8b31d1c5ac85591eb1ae16b47cfc97874 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Mon, 20 Jul 2026 18:05:07 +0300 Subject: [PATCH 10/32] Add optimizations. --- main/prepared_send/prepared_send.cpp | 30 +++++++++++ sdkconfig.defaults | 75 ++++++++++++++++++++++++++++ system/partitions.csv | 4 +- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index 94abdeb..b5185f7 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -41,6 +41,8 @@ namespace temp_sensor::prepared_send { namespace { static constexpr char const* kTag = "prepared-send"; +static RTC_DATA_ATTR esp_netif_ip_info_t rtc_ip_info = {}; +static RTC_DATA_ATTR bool adress_is_valid{false}; #ifndef AETHER_PREPARED_NONCE_RESERVE # define AETHER_PREPARED_NONCE_RESERVE 30 @@ -218,6 +220,18 @@ void WifiEventHandler(void*, esp_event_base_t event_base, xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + if (!adress_is_valid) { + rtc_ip_info.ip = event->ip_info.ip; + rtc_ip_info.netmask = event->ip_info.netmask; + rtc_ip_info.gw = event->ip_info.gw; + /*wifi_ap_record_t ap_info; + if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { + rtc_net_cfg.channel = ap_info.primary; + memcpy(rtc_net_cfg.bssid, ap_info.bssid, 6); + }*/ + adress_is_valid = true; + } ESP_LOGI(kTag, "Wi-Fi hot path connected after %d retries", g_wifi_retry_count); xEventGroupSetBits(g_wifi_event_group, kWifiConnectedBit); @@ -338,6 +352,19 @@ bool EnsureWifiConnectedForHotPath() { return false; } + if (adress_is_valid) { + std::cout << "Restoring netif config\n"; + esp_netif_dhcpc_stop(g_wifi_netif); + esp_netif_ip_info_t ip_info = { + .ip = {.addr = rtc_ip_info.ip.addr}, + .netmask = {.addr = rtc_ip_info.netmask.addr}, + .gw = {.addr = rtc_ip_info.gw.addr} + }; + esp_netif_set_ip_info(g_wifi_netif, &ip_info); + } else { + std::cout << "Restoring netif config filed\n"; + } + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); err = esp_wifi_init(&cfg); if (err != ESP_OK) { @@ -386,6 +413,8 @@ bool EnsureWifiConnectedForHotPath() { return false; } + esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N); + err = esp_wifi_start(); if (err != ESP_OK) { ESP_LOGE(kTag, "esp_wifi_start failed: %s", esp_err_to_name(err)); @@ -506,6 +535,7 @@ bool ExportPreparedSendBlock(ae::AetherApp& app, HotSendStatus TryHotWakePreparedSend(std::string temperature) { ae::prepared_packet::PreparedSendMessageBlock block; + //sleep(10); if (!DeserializeFromRetained(block)) { return HotSendStatus::kNoPreparedBlock; } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 86072ad..b5fa9ce 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -32,6 +32,7 @@ CONFIG_PARTITION_TABLE_MD5=y CONFIG_ESP_MAIN_TASK_STACK_SIZE=10112 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048 CONFIG_HTTPD_MAX_URI_LEN=2048 @@ -58,3 +59,77 @@ CONFIG_LOG_DEFAULT_LEVEL=3 CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=n CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y + +#==============================Optimization========================================= + +# 1. Compiler optimization (Critical for execution speed) +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y + +# 2. Bootloader acceleration +# Skip image validation when exiting Deep Sleep (saves ~100-150ms) +CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y +CONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON=y +CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y + +# 3. Disable logging (UART is the main bottleneck at startup) +CONFIG_LOG_DEFAULT_LEVEL_NONE=y +CONFIG_LOG_MAXIMUM_LEVEL_0=y + +# 4. Radio and Wi-Fi (No-NVS mode) +# Move PHY calibration to RTC RAM (eliminates Flash accesses) +# CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE_IN_RTC=y +# Completely disable Wi-Fi stack dependency on NVS +# CONFIG_ESP_WIFI_NVS_ENABLED=n + +# 5. ESP32-C6 specific settings for fast startup +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y +CONFIG_ESP32C6_REV_MIN_0=y + +CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n +CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=n +# Use partial calibration (takes data from NVS, doesn't re-measure) +CONFIG_ESP_PHY_RF_CAL_PARTIAL=y +# Disable verbose PHY logs +CONFIG_ESP_PHY_LOG_VERBOSE=n +CONFIG_RTC_CLK_CAL_THRESHOLD=0 +CONFIG_ESP_PHY_ENABLE_USB_CONTROLLER=n + +# Force power-down of Flash in deep sleep +CONFIG_ESP_SLEEP_POWER_DOWN_FLASH=y + +# Power down VDDSDIO (memory controller) during sleep +CONFIG_ESP_SLEEP_PD_DOMAIN_VDDSDIO=y + +# Use internal RC oscillator (most power-efficient sleep clocking option) +CONFIG_RTC_CLK_SRC_INT_RC=y + +# Disable clocking of unused RTC-domain peripherals +CONFIG_RTC_CLK_CAL_THRESHOLD=0 + +# Redirect console to UART0 or disable entirely (None) +# This frees built-in USB-JTAG resources +CONFIG_ESP_CONSOLE_UART_DEFAULT=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y + +# Disable USB-Serial-JTAG clocking in sleep mode +CONFIG_ESP_PHY_ENABLE_USB_CONTROLLER=n + +# Minimize power stabilization time (Brownout) +CONFIG_ESP_BROWNOUT_DET=n +CONFIG_ESP_BROWNOUT_DET_LVL_SEL_7=y + + +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y +# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_40=y + +CONFIG_ESPTOOLPY_FLASHMODE_DIO=y +## CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +CONFIG_PM_ENABLE=y +# CONFIG_PM_DFS_INIT_FREQ_MAX_40=y + +CONFIG_FREERTOS_USE_TICKLESS_IDLE=y +CONFIG_FREERTOS_IDLE_TASK_REPLICATION=n diff --git a/system/partitions.csv b/system/partitions.csv index 1ebf306..bd65597 100644 --- a/system/partitions.csv +++ b/system/partitions.csv @@ -2,5 +2,5 @@ # Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild nvs, data, nvs, , 0x6000, phy_init, data, phy, , 0x1000, -factory, app, factory, , 2M, -storage, data, spiffs, , 1900K \ No newline at end of file +factory, app, factory, , 3M, +storage, data, spiffs, , 900K \ No newline at end of file From a095e4e5e3d112439085d0cf3fd08adee3c165e9 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Tue, 21 Jul 2026 19:06:59 +0300 Subject: [PATCH 11/32] Some Wi-Fi optimization has been added. --- main/prepared_send/prepared_send.cpp | 52 +++++++++++++++++++++++----- main/prepared_send/prepared_send.h | 4 +++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index b5185f7..336a433 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -28,8 +28,10 @@ # include # include # include +# include # include # include +# include # include # include # include @@ -39,10 +41,11 @@ namespace temp_sensor::prepared_send { namespace { - static constexpr char const* kTag = "prepared-send"; static RTC_DATA_ATTR esp_netif_ip_info_t rtc_ip_info = {}; -static RTC_DATA_ATTR bool adress_is_valid{false}; +static RTC_DATA_ATTR WiFiBaseStation base_station{}; +static RTC_DATA_ATTR bool adress_is_valid{false}; +static RTC_DATA_ATTR bool bs_is_valid{false}; #ifndef AETHER_PREPARED_NONCE_RESERVE # define AETHER_PREPARED_NONCE_RESERVE 30 @@ -225,11 +228,17 @@ void WifiEventHandler(void*, esp_event_base_t event_base, rtc_ip_info.ip = event->ip_info.ip; rtc_ip_info.netmask = event->ip_info.netmask; rtc_ip_info.gw = event->ip_info.gw; - /*wifi_ap_record_t ap_info; + + wifi_ap_record_t ap_info; if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { - rtc_net_cfg.channel = ap_info.primary; - memcpy(rtc_net_cfg.bssid, ap_info.bssid, 6); - }*/ + base_station.target_channel = ap_info.primary; + memcpy(base_station.target_bssid, ap_info.bssid, sizeof(base_station.target_bssid)); + ESP_LOGD(kTag, + "Storing to cache BSSID:" MACSTR " CHN:%u", + MAC2STR(base_station.target_bssid), + static_cast(base_station.target_channel)); + bs_is_valid = true; + } adress_is_valid = true; } ESP_LOGI(kTag, "Wi-Fi hot path connected after %d retries", @@ -310,6 +319,9 @@ bool EnsureWifiConnectedForHotPath() { return false; #endif + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + wifi_config_t wifi_config{}; + // It is OK if these were already initialized by a previous attempt. auto err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || @@ -365,7 +377,10 @@ bool EnsureWifiConnectedForHotPath() { std::cout << "Restoring netif config filed\n"; } - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // We disable aggregation so that the packages go out one by one and quickly + cfg.ampdu_rx_enable = 0; + cfg.ampdu_tx_enable = 0; + err = esp_wifi_init(&cfg); if (err != ESP_OK) { ESP_LOGE(kTag, "esp_wifi_init failed: %s", esp_err_to_name(err)); @@ -393,7 +408,6 @@ bool EnsureWifiConnectedForHotPath() { return false; } - wifi_config_t wifi_config{}; std::strncpy(reinterpret_cast(wifi_config.sta.ssid), WIFI_SSID, sizeof(wifi_config.sta.ssid)); std::strncpy(reinterpret_cast(wifi_config.sta.password), WIFI_PASSWORD, @@ -423,10 +437,32 @@ bool EnsureWifiConnectedForHotPath() { } g_wifi_started = true; + if(bs_is_valid){ + ESP_LOGD(kTag, + "REStoring from cache BSSID:" MACSTR " CHN:%u", + MAC2STR(base_station.target_bssid), + static_cast(base_station.target_channel)); + wifi_config.sta.scan_method = WIFI_FAST_SCAN; // Fast scan + wifi_config.sta.bssid_set = true; // Enable BSSID binding + wifi_config.sta.channel = base_station.target_channel; // Set channel + // Copy the BSSID to the configuration + memcpy(wifi_config.sta.bssid, base_station.target_bssid, + sizeof(base_station.target_bssid)); + err = esp_wifi_set_channel(base_station.target_channel, WIFI_SECOND_CHAN_NONE); + if (err != ESP_OK) { + ESP_LOGE(kTag, "esp_wifi_set_channel: %s", esp_err_to_name(err)); + CleanupHotPathWifi(); + return false; + } + } + EventBits_t bits = xEventGroupWaitBits( g_wifi_event_group, kWifiConnectedBit | kWifiFailBit, pdFALSE, pdFALSE, pdMS_TO_TICKS(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS)); + esp_wifi_internal_set_fix_rate(WIFI_IF_STA, true, (wifi_phy_rate_t)0x0); + //esp_wifi_internal_set_retry_counter(3, 3); + if ((bits & kWifiConnectedBit) == 0) { ESP_LOGE(kTag, "Wi-Fi hot path connect timeout/fail"); CleanupHotPathWifi(); diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h index b7990f7..7290db6 100644 --- a/main/prepared_send/prepared_send.h +++ b/main/prepared_send/prepared_send.h @@ -54,6 +54,10 @@ bool ExportPreparedSendBlock(ae::AetherApp& app, void ClearPreparedSendBlock(); bool HasPreparedSendBlock(); +struct WiFiBaseStation { + uint8_t target_bssid[6]; + uint8_t target_channel; +}; } // namespace temp_sensor::prepared_send #endif // TEMP_SENSOR_PREPARED_SEND_H_ From 56eefb82c5726fe0437190fe44935963b6f43f24 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Wed, 22 Jul 2026 18:01:39 +0300 Subject: [PATCH 12/32] Fixing restoring BSSID + Channel. --- main/prepared_send/prepared_send.cpp | 31 +++++++++++----------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index 336a433..bb2752a 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -413,6 +413,18 @@ bool EnsureWifiConnectedForHotPath() { std::strncpy(reinterpret_cast(wifi_config.sta.password), WIFI_PASSWORD, sizeof(wifi_config.sta.password)); + if (bs_is_valid) { + ESP_LOGD(kTag, + "Restoring cached BSSID:" MACSTR " CHN:%u", + MAC2STR(base_station.target_bssid), + static_cast(base_station.target_channel)); + wifi_config.sta.scan_method = WIFI_FAST_SCAN; + wifi_config.sta.bssid_set = true; + wifi_config.sta.channel = base_station.target_channel; + std::memcpy(wifi_config.sta.bssid, base_station.target_bssid, + sizeof(base_station.target_bssid)); + } + err = esp_wifi_set_mode(WIFI_MODE_STA); if (err != ESP_OK) { ESP_LOGE(kTag, "esp_wifi_set_mode failed: %s", esp_err_to_name(err)); @@ -437,25 +449,6 @@ bool EnsureWifiConnectedForHotPath() { } g_wifi_started = true; - if(bs_is_valid){ - ESP_LOGD(kTag, - "REStoring from cache BSSID:" MACSTR " CHN:%u", - MAC2STR(base_station.target_bssid), - static_cast(base_station.target_channel)); - wifi_config.sta.scan_method = WIFI_FAST_SCAN; // Fast scan - wifi_config.sta.bssid_set = true; // Enable BSSID binding - wifi_config.sta.channel = base_station.target_channel; // Set channel - // Copy the BSSID to the configuration - memcpy(wifi_config.sta.bssid, base_station.target_bssid, - sizeof(base_station.target_bssid)); - err = esp_wifi_set_channel(base_station.target_channel, WIFI_SECOND_CHAN_NONE); - if (err != ESP_OK) { - ESP_LOGE(kTag, "esp_wifi_set_channel: %s", esp_err_to_name(err)); - CleanupHotPathWifi(); - return false; - } - } - EventBits_t bits = xEventGroupWaitBits( g_wifi_event_group, kWifiConnectedBit | kWifiFailBit, pdFALSE, pdFALSE, pdMS_TO_TICKS(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS)); From 0eda112a1cfa30b71badffc4bbb8bbb1e2346231 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Thu, 23 Jul 2026 17:27:14 +0300 Subject: [PATCH 13/32] Fixing mikrotik association. --- main/controller.cpp | 4 ++-- main/prepared_send/prepared_send.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/main/controller.cpp b/main/controller.cpp index ba72f46..a92373c 100644 --- a/main/controller.cpp +++ b/main/controller.cpp @@ -30,7 +30,7 @@ using namespace std::chrono_literals; * For real applications you should register your own uid \see aethernet.io */ static constexpr auto kParentUid = - ae::Uid::FromString("3ac93165-3d37-4970-87a6-fa4ee27744e4"); + ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); /** * \brief Uid of aether service for store the temperature values. * TODO: add actual uid @@ -39,7 +39,7 @@ static constexpr auto kServiceUid = #ifdef SERVICE_UID ae::Uid::FromString(SERVICE_UID); #else - ae::Uid::FromString("f763febf-b555-487c-bf0f-c3813a1398d2"); + ae::Uid::FromString("73791462-71a0-4014-b1aa-104ba9e62475"); #endif #ifdef ESP_PLATFORM diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index bb2752a..9a8a1ba 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -622,6 +622,7 @@ HotSendStatus TryHotWakePreparedSend(std::string temperature) { return fail_after_wifi(HotSendStatus::kSendFailed); } std::this_thread::sleep_for(std::chrono::milliseconds(450)); + CleanupHotPathWifi(); std::cout << "[prepared-send] hot path UDP sent " << sent << " bytes\n"; return HotSendStatus::kSent; #else From 85bd81dbded854563c14691032c1017f57f3bf14 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Fri, 24 Jul 2026 16:53:55 +0300 Subject: [PATCH 14/32] Changing the initialization method of the adress_is_valid and bs_is_valid variables. --- main/prepared_send/prepared_send.cpp | 38 ++++++++++++++++++++++++---- sdkconfig.defaults | 3 +++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index 9a8a1ba..cb33c2c 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -24,6 +24,7 @@ #include "aether/prepared_packet/packet_encoder.h" #if defined(ESP_PLATFORM) +# include # include # include # include @@ -41,11 +42,6 @@ namespace temp_sensor::prepared_send { namespace { -static constexpr char const* kTag = "prepared-send"; -static RTC_DATA_ATTR esp_netif_ip_info_t rtc_ip_info = {}; -static RTC_DATA_ATTR WiFiBaseStation base_station{}; -static RTC_DATA_ATTR bool adress_is_valid{false}; -static RTC_DATA_ATTR bool bs_is_valid{false}; #ifndef AETHER_PREPARED_NONCE_RESERVE # define AETHER_PREPARED_NONCE_RESERVE 30 @@ -60,9 +56,14 @@ static RTC_DATA_ATTR bool bs_is_valid{false}; #endif #if defined(ESP_PLATFORM) +static constexpr char const* kTag = "prepared-send"; // RTC_NOINIT_ATTR: do not zero on wake from deep sleep. // It may contain garbage on first boot, so magic/version/checksum validate it. RTC_NOINIT_ATTR ae::prepared_packet::RetainedPreparedBlock g_retained_prepared_block; +static RTC_DATA_ATTR esp_netif_ip_info_t rtc_ip_info = {}; +static RTC_DATA_ATTR WiFiBaseStation base_station{}; +static RTC_NOINIT_ATTR bool adress_is_valid; +static RTC_NOINIT_ATTR bool bs_is_valid; // ESP32-C6 exposes 8 KiB RTC slow memory; the linker asserts the segment fits. static_assert(sizeof(ae::prepared_packet::RetainedPreparedBlock) <= 8 * 1024, @@ -248,6 +249,8 @@ void WifiEventHandler(void*, esp_event_base_t event_base, } void CleanupHotPathWifi() { + adress_is_valid =false; + bs_is_valid = false; if (g_wifi_any_id_handler != nullptr) { auto err = esp_event_handler_instance_unregister( WIFI_EVENT, ESP_EVENT_ANY_ID, g_wifi_any_id_handler); @@ -449,6 +452,19 @@ bool EnsureWifiConnectedForHotPath() { } g_wifi_started = true; + err = esp_wifi_set_max_tx_power(80); + if (err != ESP_OK) { + ESP_LOGE(kTag, "esp_wifi_set_max_tx_power failed: %s", esp_err_to_name(err)); + CleanupHotPathWifi(); + return false; + } + err = esp_wifi_set_ps(WIFI_PS_MAX_MODEM); + if (err != ESP_OK) { + ESP_LOGE(kTag, "esp_wifi_set_ps failed: %s", esp_err_to_name(err)); + CleanupHotPathWifi(); + return false; + } + EventBits_t bits = xEventGroupWaitBits( g_wifi_event_group, kWifiConnectedBit | kWifiFailBit, pdFALSE, pdFALSE, pdMS_TO_TICKS(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS)); @@ -565,6 +581,18 @@ bool ExportPreparedSendBlock(ae::AetherApp& app, HotSendStatus TryHotWakePreparedSend(std::string temperature) { ae::prepared_packet::PreparedSendMessageBlock block; //sleep(10); + esp_reset_reason_t reset = esp_reset_reason(); + esp_sleep_wakeup_cause_t wakeup = esp_sleep_get_wakeup_cause(); + + ESP_LOGI(kTag, "reset_reason=%d, wakeup_cause=%d", + static_cast(reset), + static_cast(wakeup)); + + if (reset != ESP_RST_DEEPSLEEP) { + adress_is_valid =false; + bs_is_valid = false; + } + if (!DeserializeFromRetained(block)) { return HotSendStatus::kNoPreparedBlock; } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index b5fa9ce..50d28b8 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -60,6 +60,9 @@ CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=n CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y +# Disable BOD +#CONFIG_ESP_BROWNOUT_DET=n + #==============================Optimization========================================= # 1. Compiler optimization (Critical for execution speed) From 865b068e10dfcd8e76c89b210188b694f4abb0f3 Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Wed, 26 Aug 2026 12:48:08 +0500 Subject: [PATCH 15/32] update to adopt/prepared-packet-v0 --- main/CMakeLists.txt | 8 +- main/controller.cpp | 138 +++++------ main/prepared_send/prepared_send.cpp | 329 ++++++++++----------------- main/prepared_send/prepared_send.h | 15 +- main/sleeping/esp_main_sleep.cpp | 13 +- ulp/CMakeLists.txt | 2 +- 6 files changed, 209 insertions(+), 296 deletions(-) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index fad274e..0e9ddc7 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -48,7 +48,7 @@ if(NOT CM_PLATFORM) include(../cmake/CPM.cmake) - CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#prepared-packet-v0") + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#adopt/prepared-packet-v0") add_executable(${PROJECT_NAME} ${src_list} ${sleeping_src} ${sensors_src}) set(TARGET_NAME ${PROJECT_NAME}) @@ -78,7 +78,7 @@ else() set(TARGET_NAME "${COMPONENT_LIB}") include(../cmake/CPM.cmake) - CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#prepared-packet-v0") + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#adopt/prepared-packet-v0") target_link_libraries(${TARGET_NAME} PRIVATE aether) @@ -104,7 +104,7 @@ else() # We call the project addition as usual ulp_add_project("ulp_main" "${CMAKE_CURRENT_LIST_DIR}/../ulp") - + else() #Other platforms message(FATAL_ERROR "Platform ${CM_PLATFORM} is not supported") @@ -113,4 +113,4 @@ endif() if (NOT "${SERVICE_UID}" STREQUAL "") target_compile_definitions(${TARGET_NAME} PRIVATE "SERVICE_UID=\"${SERVICE_UID}\"") -endif() \ No newline at end of file +endif() diff --git a/main/controller.cpp b/main/controller.cpp index a92373c..06c7915 100644 --- a/main/controller.cpp +++ b/main/controller.cpp @@ -16,8 +16,10 @@ #include #include +#include #include "aether/all.h" +#include "aether/env.h" #include "sensors/sensors.h" #include "sleeping/sleeping.h" #include "prepared_send/prepared_send.h" @@ -81,13 +83,13 @@ void UpdateSensors(); // Message from aether service received void MessageReceived(ae::DataBuffer const& buffer); // Send the message value to the aether service -void SendValue(std::string temperature); +void SendValue(std::string const& temperature); // Make all required work and ready to sleep void SleepReady(); // Go to sleep method void GoToSleep(ae::TimePoint time_point); -static ae::RcPtr aether_app; +static std::shared_ptr aether_app; static ae::Client::ptr client; static std::unique_ptr message_stream; @@ -105,14 +107,14 @@ static constexpr std::size_t kPreparedNonceReserve = 32; #endif -void ReadHotSensors(std::string* temperature, uint32_t* humidity, uint32_t* pressure, - uint32_t* co2, uint32_t* gas_resistance) { - *temperature = "Prepared 25.67"; - } +void ReadHotSensors(std::string* temperature, uint32_t* humidity, + uint32_t* pressure, uint32_t* co2, + uint32_t* gas_resistance) { + *temperature = "Prepared 25.67"; +} void setup() { - std::cout << ae::Format("Setup {:%Y-%m-%d %H:%M:%S}") << ae::Now() - << std::endl; + std::cout << ae::Format("Setup {}") << ae::Now() << std::endl; #if defined(ESP_PLATFORM) // Prepared-send hot path: try to send current temperature without creating @@ -128,8 +130,8 @@ void setup() { temp_sensor::prepared_send::ToString(hot_status)); if (hot_status == temp_sensor::prepared_send::HotSendStatus::kSent) { - auto sleep_until = std::chrono::system_clock::now() + - kPreparedHotSleepSeconds; + auto sleep_until = + std::chrono::system_clock::now() + kPreparedHotSleepSeconds; DeepSleep(sleep_until, sleep_until, 3000); return; } @@ -154,7 +156,8 @@ void setup() { ); // select controller's client - aether_app->aether()->SelectClient(kParentUid, "Controller") + aether_app->aether() + ->SelectClient(kParentUid, "Controller") .result_event() .Subscribe(&ClientSelected); } @@ -170,7 +173,7 @@ void loop() { } else { // cleanup resources message_stream.reset(); - aether_app.Reset(); + aether_app.reset(); } } @@ -182,57 +185,56 @@ void ClientSelected(ae::Result res) { } client = std::move(res).value(); - auto r = client.WithLoaded([](ae::Ptr const& c) { - std::cout << ae::Format( - "\n\n>>>>>>>\n>>>>>>> Client Loaded UID:{} \n>>>>>>> Visit " - "https://aethernet.io/smarthub.html?uuid={} \n<<<<<\n\n", - c->uid(), kServiceUid); - - // Config connectivity policy, open 5s RX window every 60s. - c->connectivity_policy() - ->ConfigureRxTimings(ae::RequestPolicy::All{}) - .ForAllPriorities(ae::RxTimingConf::Every(60s).WithWindow(5s)); - - // check current work_mode - // it's always TX if we woke up - auto work_mode = WorkMode::kTx; - auto current_time = ae::Now(); - static constexpr ae::Duration threshold = 5s; - auto cp_status = c->connectivity_policy()->GetStatus(); - // if it's next_service_time it's also RX - if ((current_time + threshold) >= cp_status.next_service_time) { - work_mode = work_mode | WorkMode::kRx; - } - std::cout << ae::Format(">>>> Run in {} work mode\n", - WorkMode::ToText(work_mode)); - - if ((work_mode & WorkMode::kRx) != 0) { - // open message stream for receive and send - message_stream = std::make_unique( - *aether_app, c, kServiceUid, - c->message_stream_manager().CreatePort(kServiceUid)); - message_stream->out_data_event().Subscribe(MessageReceived); - } else { - // open message stream for send only - message_stream = std::make_unique( - *aether_app, c, kServiceUid, ae::P2pPortHandle{}); - } - - // measure temperature and send updated value - UpdateSensors(); - }); - - if (!r) { + auto client_ptr = client.Load(); + if (!client_ptr) { std::cerr << " !!! Client wasn't loaded"; aether_app->Exit(2); } + + std::cout << ae::Format( + "\n\n>>>>>>>\n>>>>>>> Client Loaded UID:{} \n>>>>>>> Visit " + "https://aethernet.io/smarthub.html?uuid={} \n<<<<<\n\n", + client_ptr->uid(), kServiceUid); + + // Config connectivity policy, open 5s RX window every 60s. + client_ptr->connectivity_policy() + ->ConfigureRxTimings(ae::RequestPolicy::All{}) + .ForAllPriorities(ae::RxTimingConf::Every(60s).WithWindow(5s)); + + // check current work_mode + // it's always TX if we woke up + auto work_mode = WorkMode::kTx; + auto current_time = ae::Now(); + static constexpr ae::Duration threshold = 5s; + auto cp_status = client_ptr->connectivity_policy()->GetStatus(); + // if it's next_service_time it's also RX + if ((current_time + threshold) >= cp_status.next_service_time) { + work_mode = work_mode | WorkMode::kRx; + } + std::cout << ae::Format(">>>> Run in {} work mode\n", + WorkMode::ToText(work_mode)); + + if ((work_mode & WorkMode::kRx) != 0) { + // open message stream for receive and send + message_stream = std::make_unique( + *aether_app, client_ptr, kServiceUid, + client_ptr->message_stream_manager().CreatePort(kServiceUid)); + message_stream->out_data_event().Subscribe(MessageReceived); + } else { + // open message stream for send only + message_stream = std::make_unique( + *aether_app, client_ptr, kServiceUid, ae::P2pPortHandle{}); + } + + // measure temperature and send updated value + UpdateSensors(); +} + +void ReadSensors(std::string* temperature, uint32_t* humidity, + uint32_t* pressure, uint32_t* co2, uint32_t* gas_resistance) { + *temperature = "Full 25.67"; } -void ReadSensors(std::string* temperature, uint32_t* humidity, uint32_t* pressure, - uint32_t* co2, uint32_t* gas_resistance) { - *temperature = "Full 25.67"; - } - // implemented in sensors/ void UpdateSensors() { std::string temperature = {}; @@ -247,22 +249,23 @@ void MessageReceived(ae::DataBuffer const& buffer) { std::cout << ae::Format(" >>> Received message from service: [{}]\n", buffer); } -void SendValue(std::string temperature) { +void SendValue(std::string const& temperature) { // The stream is not initialized yet if (!message_stream) { return; } - auto message = temp_sensor::prepared_send::MakeTemperaturePayload(temperature); + auto message = + temp_sensor::prepared_send::MakeTemperaturePayload(temperature); std::cout << ae::Format(" [CALL-CHAIN] SendValue payload_size={}\n", message.size()); message_stream->Write(std::move(message)).status_event().Subscribe([](auto) { - // Export/refresh prepared block after the full send path has a valid stream. - // If export fails, keep normal behavior and just sleep. - if (aether_app && message_stream) { + // Export/refresh prepared block after the full send path has a valid + // stream. If export fails, keep normal behavior and just sleep. + if (client.is_valid() && message_stream) { temp_sensor::prepared_send::ExportPreparedSendBlock( - *aether_app, *message_stream, kPreparedNonceReserve); + client, message_stream->destination(), kPreparedNonceReserve); } // with any result ready to sleep @@ -300,11 +303,10 @@ void GoToSleep(ae::TimePoint time_point) { } // save current aether state aether_app->aether().Save(); - + // Go to sleep - std::cout << ae::Format( - " >>> Sleep from {:%Y-%m-%d %H:%M:%S} until {:%Y-%m-%d %H:%M:%S}...\n", - ae::Now(), time_point); + std::cout << ae::Format(" >>> Sleep from {} until {}...\n", ae::Now(), + time_point); // TODO: add separate sleep duration DeepSleep(time_point, time_point, 3000); // wait till time or 30 deegrees } diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index cb33c2c..c392700 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -18,10 +18,12 @@ #include #include +#include "aether-miscpp/serialization/binary_archive.h" +#include "aether-miscpp/misc/defer.h" + #include "aether/all.h" -#include "aether/mstream.h" -#include "aether/mstream_buffers.h" #include "aether/prepared_packet/packet_encoder.h" +#include "aether/prepared_packet/prepared_send_message.h" #if defined(ESP_PLATFORM) # include @@ -57,117 +59,46 @@ namespace { #if defined(ESP_PLATFORM) static constexpr char const* kTag = "prepared-send"; + // RTC_NOINIT_ATTR: do not zero on wake from deep sleep. -// It may contain garbage on first boot, so magic/version/checksum validate it. -RTC_NOINIT_ATTR ae::prepared_packet::RetainedPreparedBlock g_retained_prepared_block; +// It may contain garbage on first boot, so magic validate it. +static RTC_NOINIT_ATTR ae::prepared_packet::PreparedSendMessageBlock + g_prepared_send_message_block; + static RTC_DATA_ATTR esp_netif_ip_info_t rtc_ip_info = {}; static RTC_DATA_ATTR WiFiBaseStation base_station{}; -static RTC_NOINIT_ATTR bool adress_is_valid; +static RTC_NOINIT_ATTR bool address_is_valid; static RTC_NOINIT_ATTR bool bs_is_valid; // ESP32-C6 exposes 8 KiB RTC slow memory; the linker asserts the segment fits. -static_assert(sizeof(ae::prepared_packet::RetainedPreparedBlock) <= 8 * 1024, - "RetainedPreparedBlock must fit in ESP32 RTC slow memory"); +static_assert(sizeof(ae::prepared_packet::PreparedSendMessageBlock) <= 8 * 1024, + "PreparedSendMessageBlock must fit in ESP32 RTC slow memory"); #else -ae::prepared_packet::RetainedPreparedBlock g_retained_prepared_block; +ae::prepared_packet::PreparedSendMessageBlock g_prepared_send_message_block; #endif -std::uint32_t Checksum(std::uint8_t const* data, std::size_t size) { - std::uint32_t h = 2166136261u; - for (std::size_t i = 0; i < size; ++i) { - h ^= data[i]; - h *= 16777619u; - } - return h; -} - -bool RetainedLooksValid() { - auto const& r = g_retained_prepared_block; - if (r.magic != ae::prepared_packet::kMagic || r.version != ae::prepared_packet::kVersion) { - return false; - } - if (r.size == 0 || r.size > r.bytes.size()) { - return false; - } - return Checksum(r.bytes.data(), r.size) == r.checksum; -} - -void SaveRawBlock(ae::DataBuffer const& bytes) { - auto& r = g_retained_prepared_block; - std::memset(&r, 0, sizeof(r)); - - r.magic = ae::prepared_packet::kMagic; - r.version = ae::prepared_packet::kVersion; - r.size = static_cast(bytes.size()); - std::memcpy(r.bytes.data(), bytes.data(), bytes.size()); - r.checksum = Checksum(r.bytes.data(), r.size); -} - -bool LoadRawBlock(ae::DataBuffer& bytes) { - if (!RetainedLooksValid()) { - return false; - } - - auto const& r = g_retained_prepared_block; - bytes.assign(r.bytes.begin(), r.bytes.begin() + r.size); - return true; -} - -template -bool SerializeToRetained(T const& value) { - ae::DataBuffer bytes; - bytes.reserve(512); - - { - auto writer = ae::VectorWriter<>{bytes}; - auto os = ae::omstream{writer}; - os << value; - } - - if (bytes.empty() || bytes.size() > ae::prepared_packet::kMaxPreparedBlockBytes) { - std::cerr << "[prepared-send] prepared block serialized size invalid: " - << bytes.size() << "\n"; - return false; - } - - SaveRawBlock(bytes); - return true; -} - -template -bool DeserializeFromRetained(T& value) { - ae::DataBuffer bytes; - if (!LoadRawBlock(bytes)) { - return false; - } - - auto reader = ae::VectorReader<>{bytes}; - auto is = ae::imstream{reader}; - is >> value; - - return ae::data_was_read(is); -} - #if defined(ESP_PLATFORM) bool FillUdpDestination(ae::prepared_packet::PreparedEndpoint const& endpoint, - sockaddr* dest_addr, socklen_t* dest_len) { - if (endpoint.version == ae::prepared_packet::PreparedIpVersion::kIpV4) { + sockaddr* dest_addr, socklen_t* dest_len) { + if (endpoint.address.Index() == ae::AddrVersion::kIpV4) { auto* dest = reinterpret_cast(dest_addr); std::memset(dest, 0, sizeof(*dest)); dest->sin_family = AF_INET; dest->sin_port = htons(endpoint.port); - std::memcpy(&dest->sin_addr.s_addr, endpoint.ip.data(), 4); + std::memcpy(&dest->sin_addr.s_addr, + &endpoint.address.Get().ipv4_value, 4); *dest_len = sizeof(*dest); return true; } - if (endpoint.version == ae::prepared_packet::PreparedIpVersion::kIpV6) { + if (endpoint.address.Index() == ae::AddrVersion::kIpV6) { auto* dest = reinterpret_cast(dest_addr); std::memset(dest, 0, sizeof(*dest)); dest->sin6_family = AF_INET6; dest->sin6_port = htons(endpoint.port); - std::memcpy(dest->sin6_addr.s6_addr, endpoint.ip.data(), 16); + std::memcpy(&dest->sin6_addr.s6_addr, + &endpoint.address.Get().ipv6_value, 16); *dest_len = sizeof(*dest); return true; } @@ -186,8 +117,8 @@ static int g_wifi_retry_count = 0; static constexpr EventBits_t kWifiConnectedBit = BIT0; static constexpr EventBits_t kWifiFailBit = BIT1; -void WifiEventHandler(void*, esp_event_base_t event_base, - std::int32_t event_id, void* event_data) { +void WifiEventHandler(void*, esp_event_base_t event_base, std::int32_t event_id, + void* event_data) { if (g_wifi_event_group == nullptr) { return; } @@ -208,8 +139,7 @@ void WifiEventHandler(void*, esp_event_base_t event_base, if (g_wifi_retry_count < AETHER_PREPARED_HOT_WIFI_MAX_RETRY) { ++g_wifi_retry_count; - ESP_LOGW(kTag, - "Wi-Fi hot path disconnected reason=%d; retry %d/%d", + ESP_LOGW(kTag, "Wi-Fi hot path disconnected reason=%d; retry %d/%d", reason, g_wifi_retry_count, static_cast(AETHER_PREPARED_HOT_WIFI_MAX_RETRY)); @@ -224,24 +154,24 @@ void WifiEventHandler(void*, esp_event_base_t event_base, xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { - ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; - if (!adress_is_valid) { - rtc_ip_info.ip = event->ip_info.ip; - rtc_ip_info.netmask = event->ip_info.netmask; - rtc_ip_info.gw = event->ip_info.gw; - - wifi_ap_record_t ap_info; - if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { - base_station.target_channel = ap_info.primary; - memcpy(base_station.target_bssid, ap_info.bssid, sizeof(base_station.target_bssid)); - ESP_LOGD(kTag, - "Storing to cache BSSID:" MACSTR " CHN:%u", - MAC2STR(base_station.target_bssid), - static_cast(base_station.target_channel)); - bs_is_valid = true; - } - adress_is_valid = true; - } + ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data; + if (!address_is_valid) { + rtc_ip_info.ip = event->ip_info.ip; + rtc_ip_info.netmask = event->ip_info.netmask; + rtc_ip_info.gw = event->ip_info.gw; + + wifi_ap_record_t ap_info; + if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { + base_station.target_channel = ap_info.primary; + memcpy(base_station.target_bssid, ap_info.bssid, + sizeof(base_station.target_bssid)); + ESP_LOGD(kTag, "Storing to cache BSSID:" MACSTR " CHN:%u", + MAC2STR(base_station.target_bssid), + static_cast(base_station.target_channel)); + bs_is_valid = true; + } + address_is_valid = true; + } ESP_LOGI(kTag, "Wi-Fi hot path connected after %d retries", g_wifi_retry_count); xEventGroupSetBits(g_wifi_event_group, kWifiConnectedBit); @@ -249,7 +179,7 @@ void WifiEventHandler(void*, esp_event_base_t event_base, } void CleanupHotPathWifi() { - adress_is_valid =false; + address_is_valid = false; bs_is_valid = false; if (g_wifi_any_id_handler != nullptr) { auto err = esp_event_handler_instance_unregister( @@ -313,14 +243,14 @@ void CleanupHotPathWifi() { } bool EnsureWifiConnectedForHotPath() { -#ifndef WIFI_SSID +# ifndef WIFI_SSID ESP_LOGE(kTag, "WIFI_SSID is not defined"); return false; -#endif -#ifndef WIFI_PASSWORD +# endif +# ifndef WIFI_PASSWORD ESP_LOGE(kTag, "WIFI_PASSWORD is not defined"); return false; -#endif +# endif wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_config_t wifi_config{}; @@ -367,17 +297,16 @@ bool EnsureWifiConnectedForHotPath() { return false; } - if (adress_is_valid) { - std::cout << "Restoring netif config\n"; + if (address_is_valid) { + std::cout << "Restoring netif config\n"; esp_netif_dhcpc_stop(g_wifi_netif); esp_netif_ip_info_t ip_info = { - .ip = {.addr = rtc_ip_info.ip.addr}, - .netmask = {.addr = rtc_ip_info.netmask.addr}, - .gw = {.addr = rtc_ip_info.gw.addr} - }; + .ip = {.addr = rtc_ip_info.ip.addr}, + .netmask = {.addr = rtc_ip_info.netmask.addr}, + .gw = {.addr = rtc_ip_info.gw.addr}}; esp_netif_set_ip_info(g_wifi_netif, &ip_info); } else { - std::cout << "Restoring netif config filed\n"; + std::cout << "Restoring netif config filed\n"; } // We disable aggregation so that the packages go out one by one and quickly @@ -392,19 +321,18 @@ bool EnsureWifiConnectedForHotPath() { } g_wifi_initialized = true; - err = esp_event_handler_instance_register( - WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiEventHandler, nullptr, - &g_wifi_any_id_handler); + err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + &WifiEventHandler, nullptr, + &g_wifi_any_id_handler); if (err != ESP_OK) { - ESP_LOGE(kTag, "failed to register WIFI handler: %s", - esp_err_to_name(err)); + ESP_LOGE(kTag, "failed to register WIFI handler: %s", esp_err_to_name(err)); CleanupHotPathWifi(); return false; } - err = esp_event_handler_instance_register( - IP_EVENT, IP_EVENT_STA_GOT_IP, &WifiEventHandler, nullptr, - &g_wifi_got_ip_handler); + err = esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, + &WifiEventHandler, nullptr, + &g_wifi_got_ip_handler); if (err != ESP_OK) { ESP_LOGE(kTag, "failed to register IP handler: %s", esp_err_to_name(err)); CleanupHotPathWifi(); @@ -417,8 +345,7 @@ bool EnsureWifiConnectedForHotPath() { sizeof(wifi_config.sta.password)); if (bs_is_valid) { - ESP_LOGD(kTag, - "Restoring cached BSSID:" MACSTR " CHN:%u", + ESP_LOGD(kTag, "Restoring cached BSSID:" MACSTR " CHN:%u", MAC2STR(base_station.target_bssid), static_cast(base_station.target_channel)); wifi_config.sta.scan_method = WIFI_FAST_SCAN; @@ -442,8 +369,9 @@ bool EnsureWifiConnectedForHotPath() { return false; } - esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N); - + esp_wifi_set_protocol( + WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N); + err = esp_wifi_start(); if (err != ESP_OK) { ESP_LOGE(kTag, "esp_wifi_start failed: %s", esp_err_to_name(err)); @@ -454,7 +382,8 @@ bool EnsureWifiConnectedForHotPath() { err = esp_wifi_set_max_tx_power(80); if (err != ESP_OK) { - ESP_LOGE(kTag, "esp_wifi_set_max_tx_power failed: %s", esp_err_to_name(err)); + ESP_LOGE(kTag, "esp_wifi_set_max_tx_power failed: %s", + esp_err_to_name(err)); CleanupHotPathWifi(); return false; } @@ -470,7 +399,7 @@ bool EnsureWifiConnectedForHotPath() { pdMS_TO_TICKS(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS)); esp_wifi_internal_set_fix_rate(WIFI_IF_STA, true, (wifi_phy_rate_t)0x0); - //esp_wifi_internal_set_retry_counter(3, 3); + // esp_wifi_internal_set_retry_counter(3, 3); if ((bits & kWifiConnectedBit) == 0) { ESP_LOGE(kTag, "Wi-Fi hot path connect timeout/fail"); @@ -483,9 +412,7 @@ bool EnsureWifiConnectedForHotPath() { #else -bool EnsureWifiConnectedForHotPath() { - return true; -} +bool EnsureWifiConnectedForHotPath() { return true; } void CleanupHotPathWifi() {} @@ -493,7 +420,7 @@ void CleanupHotPathWifi() {} } // namespace -char const* ToString(HotSendStatus status) { +std::string_view ToString(HotSendStatus status) { switch (status) { case HotSendStatus::kSent: return "sent"; @@ -515,85 +442,72 @@ char const* ToString(HotSendStatus status) { return "unknown"; } -ae::DataBuffer MakeTemperaturePayload(std::string temperature) { - struct Header { - std::uint8_t const root_code = 0x3; - std::uint8_t const size = sizeof(std::uint8_t) + sizeof(std::int16_t); - std::uint8_t const dev_code = 0x10; - AE_REFLECT_MEMBERS(root_code, size, dev_code) - }; +struct Header { + AE_REFLECT_MEMBERS(root_code, size, dev_code) + std::uint8_t const root_code = 0x3; + std::uint8_t const size = sizeof(std::uint8_t) + sizeof(std::int16_t); + std::uint8_t const dev_code = 0x10; +}; +ae::DataBuffer MakeTemperaturePayload(std::string const& temperature) { static constexpr auto header = Header{}; auto message = ae::DataBuffer{}; - message.reserve(sizeof(header) + sizeof(temperature)); + message.reserve(sizeof(header) + temperature.size()); { - auto writer = ae::VectorWriter<>{message}; - auto stream = ae::omstream{writer}; - stream << header << temperature; + auto archive = + ae::seri::BinaryArchive{ae::seri::BinaryVectorBuffer<>{message}}; + archive.Save(header); + archive.Save(temperature); } return message; } -bool HasPreparedSendBlock() { return RetainedLooksValid(); } +bool HasPreparedSendBlock() { return g_prepared_send_message_block.is_valid(); } void ClearPreparedSendBlock() { - std::memset(&g_retained_prepared_block, 0, sizeof(g_retained_prepared_block)); -} - -std::optional PrepareSendMessage( - ae::P2pStream& stream, std::size_t reserve_nonce_count) { - if (reserve_nonce_count == 0 || - reserve_nonce_count > std::numeric_limits::max()) { - return std::nullopt; - } - - return stream.ExportPreparedSendMessageBlock( - static_cast(reserve_nonce_count)); + // magic indicate if block is valid + // make it invalid + g_prepared_send_message_block.raw.magic = {}; } -bool ExportPreparedSendBlock(ae::AetherApp& app, - ae::P2pStream& stream, - std::size_t reserve_nonce_count) { - auto prepared_block = - PrepareSendMessage(stream, reserve_nonce_count); +bool ExportPreparedSendBlock(ae::Client::ptr const& client, ae::Uid destination, + std::size_t reserve_message_count) { + auto prep_res = ae::prepared_packet::PrepareSendMessageBlock( + client, destination, reserve_message_count); - if (!prepared_block) { - std::cerr << "[prepared-send] PrepareSendMessage failed\n"; + if (!prep_res) { + std::cerr << "[prepared-send] PrepareSendMessage failed with ec: " + << prep_res.error().ec << " error: " << prep_res.error().msg + << "\n"; return false; } - // Critical ordering: - // PrepareSendMessage has already reserved/burned a nonce range in the full - // Aether state. Persist full Aether state only after reserve. - app.aether().Save(); + g_prepared_send_message_block = std::move(prep_res).value(); - if (!SerializeToRetained(*prepared_block)) { - std::cerr << "[prepared-send] failed to retain prepared block\n"; - return false; - } + auto const resolved_block = g_prepared_send_message_block.Resolve(); - std::cout << "[prepared-send] exported prepared block, reserved " - << reserve_nonce_count << " nonces\n"; + std::cout << "[prepared-send] exported prepared block reserved " + << resolved_block->message_left << " messages\n"; return true; } -HotSendStatus TryHotWakePreparedSend(std::string temperature) { - ae::prepared_packet::PreparedSendMessageBlock block; - //sleep(10); +HotSendStatus TryHotWakePreparedSend( + [[maybe_unused]] std::string const& temperature) { +#if defined(ESP_PLATFORM) + // sleep(10); esp_reset_reason_t reset = esp_reset_reason(); esp_sleep_wakeup_cause_t wakeup = esp_sleep_get_wakeup_cause(); - ESP_LOGI(kTag, "reset_reason=%d, wakeup_cause=%d", - static_cast(reset), + ESP_LOGI(kTag, "reset_reason=%d, wakeup_cause=%d", static_cast(reset), static_cast(wakeup)); if (reset != ESP_RST_DEEPSLEEP) { - adress_is_valid =false; + address_is_valid = false; bs_is_valid = false; } - if (!DeserializeFromRetained(block)) { + if (!g_prepared_send_message_block.is_valid()) { return HotSendStatus::kNoPreparedBlock; } @@ -601,56 +515,49 @@ HotSendStatus TryHotWakePreparedSend(std::string temperature) { return HotSendStatus::kWifiFailed; } - auto fail_after_wifi = [](HotSendStatus status) { - CleanupHotPathWifi(); - return status; - }; + auto fail_after_wifi = ae_defer_at[] { CleanupHotPathWifi(); }; auto payload = MakeTemperaturePayload(temperature); ae::DataBuffer packet; - auto encode_result = ae::prepared_packet::EncodePacket(block, payload, packet); + auto encode_result = ae::prepared_packet::EncodePacket( + g_prepared_send_message_block, payload, packet); if (!encode_result) { ClearPreparedSendBlock(); - return fail_after_wifi(HotSendStatus::kEncodeFailed); - } - - // Persist immediately after EncodePacket, before UDP send/sleep, because - // EncodePacket consumes nonce state. - if (!SerializeToRetained(block)) { - return fail_after_wifi(HotSendStatus::kPersistFailed); + return HotSendStatus::kEncodeFailed; } -#if defined(ESP_PLATFORM) - auto const& endpoint = block.endpoint; + auto const resolved_block = g_prepared_send_message_block.Resolve(); + std::cout << "[prepared-send] reserved messages left " + << resolved_block->message_left << "\n"; + auto endpoint = resolved_block->endpoint; sockaddr_storage dest_storage{}; socklen_t dest_len = 0; if (!FillUdpDestination(endpoint, reinterpret_cast(&dest_storage), &dest_len)) { std::cerr << "[prepared-send] invalid endpoint address\n"; - return fail_after_wifi(HotSendStatus::kSendFailed); + return HotSendStatus::kSendFailed; } int sock = socket( - endpoint.version == ae::prepared_packet::PreparedIpVersion::kIpV6 - ? AF_INET6 - : AF_INET, + endpoint.address.Index() == ae::AddrVersion::kIpV6 ? AF_INET6 : AF_INET, SOCK_DGRAM, IPPROTO_IP); if (sock < 0) { - return fail_after_wifi(HotSendStatus::kSendFailed); + return HotSendStatus::kSendFailed; } auto sent = sendto(sock, packet.data(), packet.size(), 0, reinterpret_cast(&dest_storage), dest_len); close(sock); - if (sent != static_cast(packet.size())) { - return fail_after_wifi(HotSendStatus::kSendFailed); + if (sent != static_cast(packet.size())) { + return HotSendStatus::kSendFailed; } + std::this_thread::sleep_for(std::chrono::milliseconds(450)); - CleanupHotPathWifi(); + std::cout << "[prepared-send] hot path UDP sent " << sent << " bytes\n"; return HotSendStatus::kSent; #else diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h index 7290db6..0b9d656 100644 --- a/main/prepared_send/prepared_send.h +++ b/main/prepared_send/prepared_send.h @@ -15,6 +15,7 @@ #include #include +#include #include "aether/all.h" @@ -31,10 +32,10 @@ enum class HotSendStatus { kUnsupported, }; -char const* ToString(HotSendStatus status); +std::string_view ToString(HotSendStatus status); // Build the same binary temperature payload as SendValue(). -ae::DataBuffer MakeTemperaturePayload(std::string temperature); +ae::DataBuffer MakeTemperaturePayload(std::string const& temperature); // Try the MCU hot path. // Returns kSent only if: @@ -43,16 +44,12 @@ ae::DataBuffer MakeTemperaturePayload(std::string temperature); // - prepared packet was encoded; // - mutated block was persisted after nonce consumption; // - UDP datagram was sent. -HotSendStatus TryHotWakePreparedSend(std::string temperature); +HotSendStatus TryHotWakePreparedSend(std::string const& temperature); // Export a new prepared block from the already initialized full Aether stream. // Must be called only after full client/stream are usable. -bool ExportPreparedSendBlock(ae::AetherApp& app, - ae::P2pStream& stream, - std::size_t reserve_nonce_count); - -void ClearPreparedSendBlock(); -bool HasPreparedSendBlock(); +bool ExportPreparedSendBlock(ae::Client::ptr const& client, ae::Uid destination, + std::size_t reserve_message_count); struct WiFiBaseStation { uint8_t target_bssid[6]; diff --git a/main/sleeping/esp_main_sleep.cpp b/main/sleeping/esp_main_sleep.cpp index f6655aa..50eebdf 100644 --- a/main/sleeping/esp_main_sleep.cpp +++ b/main/sleeping/esp_main_sleep.cpp @@ -17,6 +17,7 @@ #include "sleeping/sleeping.h" #include +#include #include "aether/all.h" @@ -48,9 +49,15 @@ static const char* TAG = "ESP_MAIN_SLEEP"; int DeepSleep(time_point soft_sleep_tp, time_point, std::int16_t) { - auto time_us = std::chrono::duration_cast( - soft_sleep_tp - std::chrono::system_clock::now()) - .count(); + auto time_us = std::invoke([&]() -> std::uint64_t { + auto current_time = std::chrono::system_clock::now(); + if (soft_sleep_tp < current_time) { + return 0; + } + return std::chrono::duration_cast(soft_sleep_tp - + current_time) + .count(); + }); esp_sleep_enable_timer_wakeup(time_us); ESP_LOGI(TAG, "Timer wakeup enabled: %llu us", time_us); diff --git a/ulp/CMakeLists.txt b/ulp/CMakeLists.txt index 7522442..35c759c 100644 --- a/ulp/CMakeLists.txt +++ b/ulp/CMakeLists.txt @@ -52,7 +52,7 @@ ulp_add_build_binary_targets(${ULP_APP_NAME}) target_include_directories(${ULP_APP_NAME} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/../main") include(../cmake/CPM.cmake) -CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#prepared-packet-v0") +CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#adopt/prepared-packet-v0") # not link but only setup include directory for aether/config_consts.h target_include_directories(${ULP_APP_NAME} PUBLIC "${aether-client-cpp_SOURCE_DIR}") From af35277b49845e3137952a06c2685a48beae23fb Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Fri, 28 Aug 2026 09:26:34 -0700 Subject: [PATCH 16/32] Add Wi-Fi lifecycle bench and ignore-BSSID control experiment. Harness measures init_to_release_ms; control run keeps SPIFFS state and proves Connect(nullopt) restores ~3.5s cycles on ESP32-C6. Co-authored-by: Cursor --- experiments/run_ignore_bssid_control.ps1 | 102 +++++ experiments/run_wifi_bisect.ps1 | 40 ++ experiments/run_wifi_lifecycle.ps1 | 102 +++++ .../v30_ignore_bssid_capture.log | 47 +++ .../v30_ignore_bssid_reboot3.log | 17 + .../wifi_lifecycle/wifi_lifecycle_results.tsv | 11 + main/CMakeLists.txt | 101 ++++- main/wifi_lifecycle_bench.cpp | 376 ++++++++++++++++++ main/wifi_lifecycle_out.h | 46 +++ 9 files changed, 838 insertions(+), 4 deletions(-) create mode 100644 experiments/run_ignore_bssid_control.ps1 create mode 100644 experiments/run_wifi_bisect.ps1 create mode 100644 experiments/run_wifi_lifecycle.ps1 create mode 100644 experiments/wifi_lifecycle/v30_ignore_bssid_capture.log create mode 100644 experiments/wifi_lifecycle/v30_ignore_bssid_reboot3.log create mode 100644 experiments/wifi_lifecycle/wifi_lifecycle_results.tsv create mode 100644 main/wifi_lifecycle_bench.cpp create mode 100644 main/wifi_lifecycle_out.h diff --git a/experiments/run_ignore_bssid_control.ps1 b/experiments/run_ignore_bssid_control.ps1 new file mode 100644 index 0000000..b0b2e51 --- /dev/null +++ b/experiments/run_ignore_bssid_control.ps1 @@ -0,0 +1,102 @@ +param( + [string]$Port = 'COM7', + [string]$ServiceUid = '3d284a4f-ebb4-451e-a2c5-aecb0d647a45', + [int]$CaptureSec = 600 +) +$ErrorActionPreference = 'Stop' +$env:Path = 'C:\Program Files\Git\cmd;C:\Espressif\python_env\idf6.0_py3.11_env\Scripts;' + $env:Path +$env:IDF_PATH = 'C:\Espressif\frameworks\esp-idf-v6.0.2' +. "$env:IDF_PATH\export.ps1" | Out-Null + +$proj = 'C:\Users\nickc\Projects\temperature-sensor-prepared' +$aether = 'C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0' +$build = 'build-esp32c6-save-bench-smoke' +$exp = Join-Path $proj 'experiments\wifi_lifecycle' +New-Item -ItemType Directory -Force -Path $exp | Out-Null +$log = Join-Path $exp 'v30_ignore_bssid_capture.log' +$rebootLog = Join-Path $exp 'v30_ignore_bssid_reboot3.log' + +function Stop-EspMonitors { + Get-CimInstance Win32_Process -Filter "Name='python.exe'" | + Where-Object { $_.CommandLine -match 'esp_idf_monitor' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } + Start-Sleep -Seconds 2 +} + +Set-Location $proj +Stop-EspMonitors + +# Baseline driver + ignore BSSID only. Do NOT erase flash / SPIFFS. +$cmakeArgs = @( + '-B', $build, + '-D', 'WIFI_SSID=chirkov', + '-D', 'WIFI_PASSWORD=kcdjepWz51', + '-D', "SERVICE_UID=$ServiceUid", + '-D', "CPM_aether-client-cpp_SOURCE=$aether", + '-D', 'AE_EXP_WIFI_LIFECYCLE=1', + '-D', 'AE_EXP_WIFI_LIFECYCLE_VARIANT=30', + '-D', 'AE_EXP_WIFI_LIFECYCLE_CYCLES=10', + '-D', 'AE_EXP_SKIP_DTOR_SAVE=1', + '-D', 'AE_EXP_WIFI_COOLDOWN_MS=0', + '-D', 'AE_EXP_WIFI_CANONICAL=0', + '-D', 'AE_EXP_WIFI_IGNORE_BSSID=1', + '-D', 'AE_EXP_WIFI_FEAT_AMPDU_OFF=0', + '-D', 'AE_EXP_WIFI_FEAT_SCAN_THRESHOLD=0', + '-D', 'AE_EXP_WIFI_FEAT_CONNECT_RETRY10=0', + '-D', 'AE_EXP_WIFI_FEAT_FAIL_DISCONNECT=0', + '-D', 'AE_EXP_WIFI_FEAT_BSSID_CACHE=0', + '-D', 'AE_EXP_WIFI_FEAT_LEGACY_DEINIT=0', + '-D', 'AE_EXP_WIFI_FEAT_LEGACY_HANDLERS=0', + '-D', 'AE_EXP_FULL_CYCLES=', + '-D', 'AE_EXP_BENCH_STAGE=', + '-D', 'AE_EXP_SAVE_BENCH_N=', + '-D', 'AE_EXP_LEGACY_MAP_SYNC=' +) + +Write-Output 'BUILD variant=30 baseline+IGNORE_BSSID (no flash erase)' +idf.py @cmakeArgs build flash -p $Port +if ($LASTEXITCODE -ne 0) { throw 'build/flash failed' } + +Remove-Item $log -Force -ErrorAction SilentlyContinue +$elf = Join-Path $proj "$build\temperature_sensor.elf" +$mon = Start-Process -FilePath python -ArgumentList @('-u','-m','esp_idf_monitor','--port',$Port,'--baud','115200',$elf) -WorkingDirectory (Join-Path $proj $build) -RedirectStandardOutput $log -RedirectStandardError "$log.err" -PassThru -NoNewWindow +Start-Sleep -Seconds 2 +$prev = $ErrorActionPreference +$ErrorActionPreference = 'Continue' +python -m esptool --chip esp32c6 -p $Port run 2>&1 | Out-Null +$ErrorActionPreference = $prev + +$deadline = (Get-Date).AddSeconds($CaptureSec) +while ((Get-Date) -lt $deadline) { + if ((Test-Path $log) -and (Select-String -Path $log -Pattern 'WIFI_SUMMARY' -Quiet)) { break } + Start-Sleep -Seconds 5 +} +Stop-Process -Id $mon.Id -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 2 + +Write-Output '--- 10-cycle result ---' +Select-String -Path $log -Pattern '^(cycle=|raw=|min=|median=|max=|completed=|WIFI_SUMMARY)' | ForEach-Object { $_.Line } + +# Reboot check: 3 cycles, SPIFFS preserved (no erase) +Write-Output '--- reboot + 3 cycles (SPIFFS kept) ---' +Remove-Item $rebootLog -Force -ErrorAction SilentlyContinue +$mon2 = Start-Process -FilePath python -ArgumentList @('-u','-m','esp_idf_monitor','--port',$Port,'--baud','115200',$elf) -WorkingDirectory (Join-Path $proj $build) -RedirectStandardOutput $rebootLog -RedirectStandardError "$rebootLog.err" -PassThru -NoNewWindow +Start-Sleep -Seconds 2 +$ErrorActionPreference = 'Continue' +python -m esptool --chip esp32c6 -p $Port run 2>&1 | Out-Null +$ErrorActionPreference = $prev + +$deadline2 = (Get-Date).AddSeconds(180) +while ((Get-Date) -lt $deadline2) { + if (Test-Path $rebootLog) { + $n = @(Select-String -Path $rebootLog -Pattern '^cycle=\d+ init_to_release_ms=').Count + if ($n -ge 3) { break } + } + Start-Sleep -Seconds 3 +} +Stop-Process -Id $mon2.Id -Force -ErrorAction SilentlyContinue + +Write-Output '--- reboot 3-cycle lines ---' +Select-String -Path $rebootLog -Pattern '^cycle=\d+ init_to_release_ms=' | Select-Object -First 3 | ForEach-Object { $_.Line } +Write-Output "LOG10=$log" +Write-Output "LOG3=$rebootLog" diff --git a/experiments/run_wifi_bisect.ps1 b/experiments/run_wifi_bisect.ps1 new file mode 100644 index 0000000..190d3ff --- /dev/null +++ b/experiments/run_wifi_bisect.ps1 @@ -0,0 +1,40 @@ +$ErrorActionPreference = 'Stop' +$script = Join-Path $PSScriptRoot 'run_wifi_lifecycle.ps1' + +function Get-Feat($run, [string]$key) { + if ($run.ContainsKey($key)) { return [int]$run[$key] } + return 0 +} + +$runs = @( + @{ Variant = 10; Name = 'canonical+AMPDU_OFF'; FeatAmpdu = 1 }, + @{ Variant = 11; Name = 'canonical+SCAN_THRESHOLD'; FeatThreshold = 1 }, + @{ Variant = 12; Name = 'canonical+CONNECT_RETRY10'; FeatRetry10 = 1 }, + @{ Variant = 13; Name = 'canonical+FAIL_DISCONNECT'; FeatFailDisc = 1 }, + @{ Variant = 14; Name = 'canonical+BSSID_CACHE'; FeatBssid = 1 }, + @{ + Variant = 15 + Name = 'canonical+ALL_FEATS' + FeatAmpdu = 1 + FeatThreshold = 1 + FeatRetry10 = 1 + FeatFailDisc = 1 + FeatBssid = 1 + } +) + +foreach ($run in $runs) { + Write-Output "=== BISECT $($run.Name) variant=$($run.Variant) ===" + & $script -Variant $run.Variant -Canonical 1 -CooldownMs 0 ` + -FeatAmpdu (Get-Feat $run 'FeatAmpdu') ` + -FeatThreshold (Get-Feat $run 'FeatThreshold') ` + -FeatRetry10 (Get-Feat $run 'FeatRetry10') ` + -FeatFailDisc (Get-Feat $run 'FeatFailDisc') ` + -FeatBssid (Get-Feat $run 'FeatBssid') + if ($LASTEXITCODE -ne 0) { + Write-Output "WARN: variant $($run.Variant) script exit=$LASTEXITCODE (check log)" + } +} + +Write-Output '=== BISECT DONE ===' +Get-Content (Join-Path $PSScriptRoot 'wifi_lifecycle\wifi_lifecycle_results.tsv') diff --git a/experiments/run_wifi_lifecycle.ps1 b/experiments/run_wifi_lifecycle.ps1 new file mode 100644 index 0000000..fd74540 --- /dev/null +++ b/experiments/run_wifi_lifecycle.ps1 @@ -0,0 +1,102 @@ +param( + [int]$Variant = 0, + [int]$Canonical = 0, + [int]$CooldownMs = 0, + [int]$FeatAmpdu = 0, + [int]$FeatThreshold = 0, + [int]$FeatRetry10 = 0, + [int]$FeatFailDisc = 0, + [int]$FeatBssid = 0, + [int]$FeatLegacyDeinit = 0, + [int]$FeatLegacyHandlers = 0, + [string]$Port = 'COM7', + [string]$ServiceUid = '3d284a4f-ebb4-451e-a2c5-aecb0d647a45', + [int]$CaptureSec = 900 +) +$ErrorActionPreference = 'Stop' +$env:Path = 'C:\Program Files\Git\cmd;C:\Espressif\python_env\idf6.0_py3.11_env\Scripts;' + $env:Path +$env:IDF_PATH = 'C:\Espressif\frameworks\esp-idf-v6.0.2' +. "$env:IDF_PATH\export.ps1" | Out-Null + +$proj = 'C:\Users\nickc\Projects\temperature-sensor-prepared' +$aether = 'C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0' +$build = 'build-esp32c6-save-bench-smoke' +$exp = Join-Path $proj 'experiments\wifi_lifecycle' +New-Item -ItemType Directory -Force -Path $exp | Out-Null +$log = Join-Path $exp "v${Variant}_capture.log" +$tsv = Join-Path $exp 'wifi_lifecycle_results.tsv' + +function Stop-EspMonitors { + Get-CimInstance Win32_Process -Filter "Name='python.exe'" | + Where-Object { $_.CommandLine -match 'esp_idf_monitor' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } + Start-Sleep -Seconds 2 +} + +Set-Location $proj +$args = @( + '-B', $build, + '-D', 'WIFI_SSID=chirkov', + '-D', 'WIFI_PASSWORD=kcdjepWz51', + '-D', "SERVICE_UID=$ServiceUid", + '-D', "CPM_aether-client-cpp_SOURCE=$aether", + '-D', 'AE_EXP_WIFI_LIFECYCLE=1', + '-D', "AE_EXP_WIFI_LIFECYCLE_VARIANT=$Variant", + '-D', 'AE_EXP_WIFI_LIFECYCLE_CYCLES=10', + '-D', 'AE_EXP_SKIP_DTOR_SAVE=1', + '-D', "AE_EXP_WIFI_COOLDOWN_MS=$CooldownMs", + '-D', "AE_EXP_WIFI_CANONICAL=$Canonical", + '-D', "AE_EXP_WIFI_FEAT_AMPDU_OFF=$FeatAmpdu", + '-D', "AE_EXP_WIFI_FEAT_SCAN_THRESHOLD=$FeatThreshold", + '-D', "AE_EXP_WIFI_FEAT_CONNECT_RETRY10=$FeatRetry10", + '-D', "AE_EXP_WIFI_FEAT_FAIL_DISCONNECT=$FeatFailDisc", + '-D', "AE_EXP_WIFI_FEAT_BSSID_CACHE=$FeatBssid", + '-D', "AE_EXP_WIFI_FEAT_LEGACY_DEINIT=$FeatLegacyDeinit", + '-D', "AE_EXP_WIFI_FEAT_LEGACY_HANDLERS=$FeatLegacyHandlers", + '-D', 'AE_EXP_FULL_CYCLES=', + '-D', 'AE_EXP_BENCH_STAGE=', + '-D', 'AE_EXP_SAVE_BENCH_N=', + '-D', 'AE_EXP_LEGACY_MAP_SYNC=' +) +Write-Output "BUILD variant=$Variant canonical=$Canonical cooldown_ms=$CooldownMs" +Stop-EspMonitors +idf.py @args build flash -p $Port +if ($LASTEXITCODE -ne 0) { throw "build/flash failed" } + +Remove-Item $log -Force -ErrorAction SilentlyContinue +$elf = Join-Path $proj "$build\temperature_sensor.elf" +$mon = Start-Process -FilePath python -ArgumentList @('-u','-m','esp_idf_monitor','--port',$Port,'--baud','115200',$elf) -WorkingDirectory (Join-Path $proj $build) -RedirectStandardOutput $log -RedirectStandardError "$log.err" -PassThru -NoNewWindow +Start-Sleep -Seconds 2 +$prevEap = $ErrorActionPreference +$ErrorActionPreference = 'Continue' +python -m esptool --chip esp32c6 -p $Port run 2>&1 | Out-Null +$ErrorActionPreference = $prevEap +$deadline = (Get-Date).AddSeconds($CaptureSec) +$summary = $false +while ((Get-Date) -lt $deadline) { + if (Test-Path $log) { + if (Select-String -Path $log -Pattern 'WIFI_SUMMARY' -Quiet) { $summary = $true; break } + } + Start-Sleep -Seconds 10 +} +Stop-Process -Id $mon.Id -Force -ErrorAction SilentlyContinue + +$cycles = @() +if (Test-Path $log) { + $cycles = Select-String -Path $log -Pattern '^WIFI_CYCLE\t' | ForEach-Object { $_.Line } + $sumLine = (Select-String -Path $log -Pattern '^WIFI_SUMMARY\t' | Select-Object -Last 1).Line +} +Write-Output "SUMMARY=$sumLine" +$cycles | ForEach-Object { Write-Output $_ } + +if ($sumLine) { + if (-not (Test-Path $tsv) -or (Get-Item $tsv).Length -eq 0) { + "variant`tcompleted`tmin`tmedian`tmax`traw_times`tverdict" | Out-File $tsv -Encoding utf8 + } + $raw = ($cycles | ForEach-Object { ($_ -split "`t" | Where-Object { $_ -match 'init_to_release_ms=' }) -replace 'init_to_release_ms=','' }) -join ',' + $parts = $sumLine -split "`t" + $get = { param($k) ($parts | Where-Object { $_ -like "$k=*" }) -replace "$k=",'' } + $row = "{0}`t{1}`t{2}`t{3}`t{4}`t{5}`t" -f $Variant, (&$get 'completed'), (&$get 'min'), (&$get 'median'), (&$get 'max'), $raw + Add-Content $tsv $row +} +Write-Output "LOG=$log" diff --git a/experiments/wifi_lifecycle/v30_ignore_bssid_capture.log b/experiments/wifi_lifecycle/v30_ignore_bssid_capture.log new file mode 100644 index 0000000..eea2cdf --- /dev/null +++ b/experiments/wifi_lifecycle/v30_ignore_bssid_capture.log @@ -0,0 +1,47 @@ +ESP-ROM:esp32c6-20220919 +Build:Sep 19 2022 +rst:0x15 (USB_UART_HPSYS),boot:0x6f (SPI_FAST_FLASH_BOOT) +Saved PC:0x40808998 +SPIWP:0xee +mode:DIO, clock div:2 +load:0x40875730,len:0xf4 +load:0x4086b910,len:0x9f8 +load:0x4086e610,len:0x2694 +entry 0x4086b910 +WIFI_BENCH_START variant=30 cooldown_ms=0 +cycle=1 init_to_release_ms=3410 result=OK +WIFI_CYCLE variant=30 cycle=1 init_to_release_ms=3410 +cycle=2 init_to_release_ms=3369 result=OK +WIFI_CYCLE variant=30 cycle=2 init_to_release_ms=3369 +cycle=3 init_to_release_ms=3419 result=OK +WIFI_CYCLE variant=30 cycle=3 init_to_release_ms=3419 +cycle=4 init_to_release_ms=3389 result=OK +WIFI_CYCLE variant=30 cycle=4 init_to_release_ms=3389 +cycle=5 init_to_release_ms=3359 result=OK +WIFI_CYCLE variant=30 cycle=5 init_to_release_ms=3359 +cycle=6 init_to_release_ms=3349 result=OK +WIFI_CYCLE variant=30 cycle=6 init_to_release_ms=3349 +cycle=7 init_to_release_ms=3339 result=OK +WIFI_CYCLE variant=30 cycle=7 init_to_release_ms=3339 +cycle=8 init_to_release_ms=3319 result=OK +WIFI_CYCLE variant=30 cycle=8 init_to_release_ms=3319 +cycle=9 init_to_release_ms=3379 result=OK +WIFI_CYCLE variant=30 cycle=9 init_to_release_ms=3379 +cycle=10 init_to_release_ms=3439 result=OK +WIFI_CYCLE variant=30 cycle=10 init_to_release_ms=3439 +WIFI_SUMMARY variant=30 completed=10 min=3319 median=3374 max=3439 +raw=3410,3369,3419,3389,3359,3349,3339,3319,3379,3439 +min=3319 +median=3374 +max=3439 +completed=10 +WIFI_RAW variant=30 cycle=1 init_to_release_ms=3410 +WIFI_RAW variant=30 cycle=2 init_to_release_ms=3369 +WIFI_RAW variant=30 cycle=3 init_to_release_ms=3419 +WIFI_RAW variant=30 cycle=4 init_to_release_ms=3389 +WIFI_RAW variant=30 cycle=5 init_to_release_ms=3359 +WIFI_RAW variant=30 cycle=6 init_to_release_ms=3349 +WIFI_RAW variant=30 cycle=7 init_to_release_ms=3339 +WIFI_RAW variant=30 cycle=8 init_to_release_ms=3319 +WIFI_RAW variant=30 cycle=9 init_to_release_ms=3379 +WIFI_RAW variant=30 cycle=10 init_to_release_ms=3439 diff --git a/experiments/wifi_lifecycle/v30_ignore_bssid_reboot3.log b/experiments/wifi_lifecycle/v30_ignore_bssid_reboot3.log new file mode 100644 index 0000000..bbdbb73 --- /dev/null +++ b/experiments/wifi_lifecycle/v30_ignore_bssid_reboot3.log @@ -0,0 +1,17 @@ +ESP-ROM:esp32c6-20220919 +Build:Sep 19 2022 +rst:0x15 (USB_UART_HPSYS),boot:0x6f (SPI_FAST_FLASH_BOOT) +Saved PC:0x4201534a +SPIWP:0xee +mode:DIO, clock div:2 +load:0x40875730,len:0xf4 +load:0x4086b910,len:0x9f8 +load:0x4086e610,len:0x2694 +entry 0x4086b910 +WIFI_BENCH_START variant=30 cooldown_ms=0 +cycle=1 init_to_release_ms=3410 result=OK +WIFI_CYCLE variant=30 cycle=1 init_to_release_ms=3410 +cycle=2 init_to_release_ms=3339 result=OK +WIFI_CYCLE variant=30 cycle=2 init_to_release_ms=3339 +cycle=3 init_to_release_ms=3319 result=OK +WIFI_CYCLE variant=30 cycle=3 init_to_release_ms=3319 diff --git a/experiments/wifi_lifecycle/wifi_lifecycle_results.tsv b/experiments/wifi_lifecycle/wifi_lifecycle_results.tsv new file mode 100644 index 0000000..955bdc4 --- /dev/null +++ b/experiments/wifi_lifecycle/wifi_lifecycle_results.tsv @@ -0,0 +1,11 @@ +variant completed min median max raw_times verdict +0 10 29669 30114 31538 31538,29739,29729,30089,30139,30159,30159,29889,29669,30139 baseline ~30s/cycle +1 10 3359 3469 3629 3509,3519,3359,3579,3399,3459,3479,3459,3629,3439 canonical ~3.5s/cycle +2 10 3042 3532 3792 3792,3522,3562,3562,3482,3542,3632,3412,3092,3042 canonical+cooldown ~3.5s (cooldown outside timer) +10 10 3399 3484 4189 4189,3469,3399,3459,3499,3629,3429,3419,3729,3759 canonical+AMPDU_OFF ~3.5s +11 10 3429 3494 3579 3578,3509,3579,3459,3429,3499,3489,3579,3479,3459 canonical+SCAN_THRESHOLD ~3.5s +12 10 3319 3469 3569 3469,3469,3569,3479,3479,3429,3449,3479,3329,3319 canonical+CONNECT_RETRY10 ~3.5s +13 10 3409 3479 3559 3440,3469,3489,3509,3559,3469,3469,3559,3409,3519 canonical+FAIL_DISCONNECT ~3.5s +14 0 canonical+BSSID_CACHE hung after WIFI_BENCH_START +17 10 3349 3389 3490 3490,3349,3459,3399,3379,3379,3399,3369,3479,3369 +18 10 3349 3394 3479 3448,3409,3429,3479,3409,3349,3379,3379,3379,3379 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 0e9ddc7..01000e9 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -16,10 +16,19 @@ cmake_minimum_required(VERSION 3.16.0) list(APPEND src_list "main.cpp" - "controller.cpp" - "prepared_send/prepared_send.cpp" ) +if(AE_EXP_WIFI_LIFECYCLE) + list(APPEND src_list "wifi_lifecycle_bench.cpp") +elseif(AE_EXP_FULL_CYCLES) + list(APPEND src_list "thermometer_full_cycles.cpp") +else() + list(APPEND src_list + "controller.cpp" + "prepared_send/prepared_send.cpp" + ) +endif() + list(APPEND bme68x_srcs "../third-party/BME68x_SensorAPI/bme68x.c" ) @@ -48,7 +57,8 @@ if(NOT CM_PLATFORM) include(../cmake/CPM.cmake) - CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#adopt/prepared-packet-v0") + # Pin exact SHA. Override locally with -DCPM_aether-client-cpp_SOURCE=. + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#a2ee9ec97af4f83e8507a51b46bf2f03393984c1") add_executable(${PROJECT_NAME} ${src_list} ${sleeping_src} ${sensors_src}) set(TARGET_NAME ${PROJECT_NAME}) @@ -70,6 +80,7 @@ else() idf::esp_driver_i2c idf::esp_driver_tsens idf::esp_driver_gpio + idf::esp_driver_usb_serial_jtag idf::esp_http_server idf::esp_event idf::esp_timer @@ -78,7 +89,8 @@ else() set(TARGET_NAME "${COMPONENT_LIB}") include(../cmake/CPM.cmake) - CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#adopt/prepared-packet-v0") + # Pin exact SHA. Override locally with -DCPM_aether-client-cpp_SOURCE=. + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#a2ee9ec97af4f83e8507a51b46bf2f03393984c1") target_link_libraries(${TARGET_NAME} PRIVATE aether) @@ -114,3 +126,84 @@ endif() if (NOT "${SERVICE_UID}" STREQUAL "") target_compile_definitions(${TARGET_NAME} PRIVATE "SERVICE_UID=\"${SERVICE_UID}\"") endif() + +# Experiment 2 Save measurement optional flags (do NOT set AE_EXP_DIAG here). +# Apply PUBLIC on aether so library instrumentation sees the same defs. +# Example: -DAE_EXP_SAVE_STATS=1 -DAE_EXP_SKIP_DTOR_SAVE=1 +set(AE_EXP_SAVE_STATS "" CACHE STRING "Enable AE_EXP_SAVE_STATS (set to 1)") +set(AE_EXP_SKIP_DTOR_SAVE "" CACHE STRING "Skip AetherApp dtor Save (set to 1)") +set(AE_EXP_LEGACY_MAP_SYNC "" CACHE STRING "Enable AE_EXP_LEGACY_MAP_SYNC (set to 1)") +set(AE_EXP_CALL_COUNT_MODE "" CACHE STRING "Call-count mode 1/2/3 (optional)") +set(AE_EXP_FULL_CYCLES "" CACHE STRING "FULL cycle count (e.g. 30); disables Save bench when set") +set(AE_EXP_BENCH_STAGE "" CACHE STRING "Enable UART stage markers (set to 1)") +set(AE_EXP_SAVE_BENCH_N "" CACHE STRING "Save bench samples per scenario (default 50, smoke=2)") +set(AE_EXP_SAVE_BENCH_CD_CHUNK "" CACHE STRING "C/D chunk size (default 10)") +set(AE_EXP_WIFI_LIFECYCLE "" CACHE STRING "Wi-Fi lifecycle bench harness (set to 1)") +set(AE_EXP_WIFI_LIFECYCLE_VARIANT "" CACHE STRING "Wi-Fi lifecycle variant id (0=baseline)") +set(AE_EXP_WIFI_LIFECYCLE_CYCLES "" CACHE STRING "Wi-Fi lifecycle cycles (default 10)") +set(AE_EXP_WIFI_COOLDOWN_MS "" CACHE STRING "Cooldown between cycles, outside timer (ms)") +set(AE_EXP_WIFI_CANONICAL "" CACHE STRING "Canonical ESP-IDF Wi-Fi driver (set to 1)") +set(AE_EXP_WIFI_FEAT_AMPDU_OFF "" CACHE STRING "Canonical+bisect: AMPDU off") +set(AE_EXP_WIFI_FEAT_SCAN_THRESHOLD "" CACHE STRING "Canonical+bisect: scan threshold") +set(AE_EXP_WIFI_FEAT_CONNECT_RETRY10 "" CACHE STRING "Canonical+bisect: 10 connect retries") +set(AE_EXP_WIFI_FEAT_FAIL_DISCONNECT "" CACHE STRING "Canonical+bisect: fail disconnect path") +set(AE_EXP_WIFI_FEAT_BSSID_CACHE "" CACHE STRING "Canonical+bisect: BSSID cache") +set(AE_EXP_WIFI_FEAT_LEGACY_DEINIT "" CACHE STRING "Canonical+bisect: baseline Deinit") +set(AE_EXP_WIFI_FEAT_LEGACY_HANDLERS "" CACHE STRING "Canonical+bisect: non-instance handlers") +set(AE_EXP_WIFI_IGNORE_BSSID "" CACHE STRING "Ignore cached BSSID on Connect (experiment)") + +function(ae_exp_define_if_set flag_name) + if(NOT "${${flag_name}}" STREQUAL "") + target_compile_definitions(aether PUBLIC "${flag_name}=${${flag_name}}") + target_compile_definitions(${TARGET_NAME} PRIVATE "${flag_name}=${${flag_name}}") + endif() +endfunction() + +ae_exp_define_if_set(AE_EXP_SAVE_STATS) +ae_exp_define_if_set(AE_EXP_SKIP_DTOR_SAVE) +ae_exp_define_if_set(AE_EXP_LEGACY_MAP_SYNC) +ae_exp_define_if_set(AE_EXP_CALL_COUNT_MODE) +ae_exp_define_if_set(AE_EXP_FULL_CYCLES) +ae_exp_define_if_set(AE_EXP_BENCH_STAGE) +ae_exp_define_if_set(AE_EXP_SAVE_BENCH_N) +ae_exp_define_if_set(AE_EXP_SAVE_BENCH_CD_CHUNK) +ae_exp_define_if_set(AE_EXP_WIFI_CANONICAL) +ae_exp_define_if_set(AE_EXP_WIFI_FEAT_AMPDU_OFF) +ae_exp_define_if_set(AE_EXP_WIFI_FEAT_SCAN_THRESHOLD) +ae_exp_define_if_set(AE_EXP_WIFI_FEAT_CONNECT_RETRY10) +ae_exp_define_if_set(AE_EXP_WIFI_FEAT_FAIL_DISCONNECT) +ae_exp_define_if_set(AE_EXP_WIFI_FEAT_BSSID_CACHE) +ae_exp_define_if_set(AE_EXP_WIFI_FEAT_LEGACY_DEINIT) +ae_exp_define_if_set(AE_EXP_WIFI_FEAT_LEGACY_HANDLERS) +ae_exp_define_if_set(AE_EXP_WIFI_IGNORE_BSSID) +ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE) +ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE_VARIANT) +ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE_CYCLES) +ae_exp_define_if_set(AE_EXP_WIFI_COOLDOWN_MS) + +# Continuous prepared-send E2E test defaults (override via -D...). +if(NOT DEFINED AETHER_PREPARED_NONCE_RESERVE) + set(AETHER_PREPARED_NONCE_RESERVE 10 CACHE STRING "Prepared nonce reserve count") +endif() +if(NOT DEFINED AETHER_PREPARED_HOT_SLEEP_SECONDS) + set(AETHER_PREPARED_HOT_SLEEP_SECONDS 10 CACHE STRING "Deep sleep seconds after send") +endif() +if(NOT DEFINED AETHER_PREPARED_POST_SEND_HOLD_MS) + set(AETHER_PREPARED_POST_SEND_HOLD_MS 300 CACHE STRING "Post-send TX hold ms on prepared path") +endif() +if(NOT DEFINED AETHER_PREPARED_WIFI_PS) + set(AETHER_PREPARED_WIFI_PS 0 CACHE STRING "Prepared WiFi PS mode (0=NONE,1=MIN,2=MAX)") +endif() +if(NOT DEFINED AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS) + set(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS 15000 CACHE STRING "Prepared WiFi connect timeout ms") +endif() +if(NOT DEFINED AETHER_PREPARED_HOT_WIFI_MAX_RETRY) + set(AETHER_PREPARED_HOT_WIFI_MAX_RETRY 10 CACHE STRING "Prepared WiFi max association retries") +endif() +target_compile_definitions(${TARGET_NAME} PRIVATE + "AETHER_PREPARED_NONCE_RESERVE=${AETHER_PREPARED_NONCE_RESERVE}" + "AETHER_PREPARED_HOT_SLEEP_SECONDS=${AETHER_PREPARED_HOT_SLEEP_SECONDS}" + "AETHER_PREPARED_POST_SEND_HOLD_MS=${AETHER_PREPARED_POST_SEND_HOLD_MS}" + "AETHER_PREPARED_WIFI_PS=${AETHER_PREPARED_WIFI_PS}" + "AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS=${AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS}" + "AETHER_PREPARED_HOT_WIFI_MAX_RETRY=${AETHER_PREPARED_HOT_WIFI_MAX_RETRY}") diff --git a/main/wifi_lifecycle_bench.cpp b/main/wifi_lifecycle_bench.cpp new file mode 100644 index 0000000..13a89a6 --- /dev/null +++ b/main/wifi_lifecycle_bench.cpp @@ -0,0 +1,376 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Wi-Fi lifecycle benchmark: single metric init_to_release_ms, 10 cycles. + */ +#include +#include +#include +#include +#include +#include + +#include "aether/all.h" +#include "aether/ae_exp_wifi.h" +#include "aether/config.h" +#include "aether/env.h" +#include "aether-miscpp/serialization/binary_archive.h" +#include "wifi_lifecycle_out.h" + +#if defined(ESP_PLATFORM) +# include +# include +# include +# include +# include +# include +# include +#endif + +using namespace std::chrono_literals; + +namespace temp_sensor { +namespace { + +static constexpr auto kParentUid = + ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); +static constexpr char const* kClientId = "Controller"; + +#if defined(SERVICE_UID) +static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID); +#else +static constexpr auto kServiceUid = + ae::Uid::FromString("015f10b0-73cc-4917-8804-5ecb19ede984"); +#endif + +#if defined(AE_EXP_WIFI_LIFECYCLE_CYCLES) +static constexpr int kMeasureCycles = AE_EXP_WIFI_LIFECYCLE_CYCLES; +#else +static constexpr int kMeasureCycles = 10; +#endif + +#if defined(AE_EXP_WIFI_COOLDOWN_MS) +static constexpr int kCooldownMs = AE_EXP_WIFI_COOLDOWN_MS; +#else +static constexpr int kCooldownMs = 0; +#endif + +#if defined(AE_EXP_WIFI_LIFECYCLE_VARIANT) +static constexpr int kVariant = AE_EXP_WIFI_LIFECYCLE_VARIANT; +#else +static constexpr int kVariant = 0; +#endif + +#if defined(ESP_PLATFORM) +static const auto kWifiInit = ae::WiFiInit{ + std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}}, + {}, +}; + +static void PreConstructCleanup() { + if (kCooldownMs > 0) { + vTaskDelay(pdMS_TO_TICKS(kCooldownMs)); + } +# if !AE_WIFI_USE_FULL_DEINIT + // Legacy driver Deinit leaves default event loop / netif; clear before next + // Construct so the stack can be recreated. + esp_netif_deinit(); + esp_event_loop_delete_default(); +# endif +} + +static std::int64_t NowUs() { return esp_timer_get_time(); } +#endif + +struct Ping { + AE_REFLECT_MEMBERS(root_code, size, dev_code, index) + std::uint8_t root_code{0x3}; + std::uint8_t size{4}; + std::uint8_t dev_code{0x15}; + std::uint32_t index{0}; +}; + +enum class Phase : std::uint8_t { kRegister, kMeasure, kDone }; + +static std::shared_ptr g_app; +static ae::Client::ptr g_client; +static std::unique_ptr g_stream; +static ae::Subscription g_select_sub; +static ae::Subscription g_stream_sub; +static ae::Subscription g_write_sub; + +static Phase g_phase = Phase::kRegister; +static int g_cycle = 0; +static bool g_write_armed = false; +static bool g_done = false; + +static std::array g_times_ms{}; +static int g_completed = 0; + +#if defined(ESP_PLATFORM) +static std::int64_t g_t0 = 0; +#endif + +static ae::DataBuffer MakePing(std::uint32_t index) { + Ping ping{}; + ping.index = index; + ae::DataBuffer msg{}; + auto archive = ae::seri::BinaryArchive{ae::seri::BinaryVectorBuffer<>{msg}}; + archive.Save(ping); + return msg; +} + +static void ReleaseMeasuredApp() { + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + g_app.reset(); +} + +static void FinishCycleAndNext(); +static void StartMeasureCycle(int cycle); + +static void DoWrite() { + if (g_write_armed) { + return; + } + g_write_armed = true; + auto& wa = g_stream->Write(MakePing(static_cast(g_cycle))); + g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) { + if (st != ae::WriteAction::Status::kSuccess) { + WifiLifecyclePrintf("cycle=%d init_to_release_ms=0 result=FAIL\n", g_cycle); + WifiLifecyclePrintf("WIFI_CYCLE_ERR\tcycle=%d\twrite_fail\n", g_cycle); + g_app->Exit(1); + return; + } + g_app->aether().Save(); + g_app->Exit(0); + }); +} + +static void MaybeWrite() { + if (!g_stream || g_write_armed) { + return; + } + if (!g_stream->stream_info().is_writable) { + return; + } + DoWrite(); +} + +static void OnClientReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + if (g_phase == Phase::kRegister) { + g_app->aether().Save(); + g_app->Exit(0); + return; + } + + auto client = g_client.Load(); + g_stream = std::make_unique(*g_app, client, kServiceUid, + ae::P2pPortHandle{}); + g_stream_sub = + g_stream->stream_update_event().Subscribe([]() { MaybeWrite(); }); + MaybeWrite(); +} + +static void StartMeasureCycle(int cycle) { + g_phase = Phase::kMeasure; + g_cycle = cycle; + g_write_armed = false; + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + +#if defined(ESP_PLATFORM) + PreConstructCleanup(); + g_t0 = NowUs(); +#endif + + g_app = ae::AetherApp::Construct( + ae::AetherAppContext{} +#if AE_DISTILLATION && defined(ESP_PLATFORM) + .AddAdapterFactory([&](ae::AetherAppContext const& ctx) { + return ae::WifiAdapter::ptr::Create( + ae::CreateWith{ctx.domain()}.with_id( + ae::GlobalId::kWiFiAdapter), + ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit); + }) +#endif + ); + + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + WifiLifecyclePrintf( + "cycle=%d init_to_release_ms=0 result=FAIL\n", + g_cycle); + WifiLifecyclePrintf("WIFI_CYCLE_ERR\tcycle=%d\tselect\n", + g_cycle); + g_app->Exit(1); + return; + } + OnClientReady(std::move(res).value()); + }); +} + +static void StartRegisterCycle() { + g_phase = Phase::kRegister; + g_cycle = 0; + g_write_armed = false; +#if defined(ESP_PLATFORM) + PreConstructCleanup(); +#endif + g_app = ae::AetherApp::Construct( + ae::AetherAppContext{} +#if AE_DISTILLATION && defined(ESP_PLATFORM) + .AddAdapterFactory([&](ae::AetherAppContext const& ctx) { + return ae::WifiAdapter::ptr::Create( + ae::CreateWith{ctx.domain()}.with_id( + ae::GlobalId::kWiFiAdapter), + ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit); + }) +#endif + ); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + g_app->Exit(1); + return; + } + OnClientReady(std::move(res).value()); + }); +} + +static std::uint32_t MedianMs() { + std::array sorted{}; + for (int i = 0; i < g_completed; ++i) { + sorted[static_cast(i)] = g_times_ms[static_cast(i)]; + } + std::sort(sorted.begin(), sorted.begin() + g_completed); + if (g_completed == 0) { + return 0; + } + if (g_completed % 2 == 1) { + return sorted[static_cast(g_completed / 2)]; + } + auto const a = sorted[static_cast(g_completed / 2 - 1)]; + auto const b = sorted[static_cast(g_completed / 2)]; + return (a + b) / 2; +} + +static void PrintSummary() { + std::uint32_t min_ms = UINT32_MAX; + std::uint32_t max_ms = 0; + for (int i = 0; i < g_completed; ++i) { + auto const v = g_times_ms[static_cast(i)]; + min_ms = std::min(min_ms, v); + max_ms = std::max(max_ms, v); + } + if (g_completed == 0) { + min_ms = 0; + } + WifiLifecyclePrintf("WIFI_SUMMARY\tvariant=%d\tcompleted=%d\tmin=%lu\tmedian=%lu\tmax=%lu\n", + kVariant, g_completed, static_cast(min_ms), + static_cast(MedianMs()), + static_cast(max_ms)); + WifiLifecyclePrintf("raw="); + for (int i = 0; i < g_completed; ++i) { + if (i > 0) { + WifiLifecyclePrintf(","); + } + WifiLifecyclePrintf( + "%lu", + static_cast(g_times_ms[static_cast(i)])); + } + WifiLifecyclePrintf("\n"); + WifiLifecyclePrintf("min=%lu\n", static_cast(min_ms)); + WifiLifecyclePrintf("median=%lu\n", + static_cast(MedianMs())); + WifiLifecyclePrintf("max=%lu\n", static_cast(max_ms)); + WifiLifecyclePrintf("completed=%d\n", g_completed); + for (int i = 0; i < g_completed; ++i) { + WifiLifecyclePrintf("WIFI_RAW\tvariant=%d\tcycle=%d\tinit_to_release_ms=%lu\n", + kVariant, i + 1, + static_cast( + g_times_ms[static_cast(i)])); + } +} + +static void FinishCycleAndNext() { + if (g_phase == Phase::kRegister) { + ReleaseMeasuredApp(); + StartMeasureCycle(1); + return; + } + + ReleaseMeasuredApp(); + +#if defined(ESP_PLATFORM) + auto const ms = static_cast((NowUs() - g_t0) / 1000); +#else + auto const ms = 0U; +#endif + + if (g_completed < kMeasureCycles) { + g_times_ms[static_cast(g_completed)] = ms; + WifiLifecyclePrintf("cycle=%d init_to_release_ms=%lu result=OK\n", g_cycle, + static_cast(ms)); + WifiLifecyclePrintf("WIFI_CYCLE\tvariant=%d\tcycle=%d\tinit_to_release_ms=%lu\n", + kVariant, g_cycle, static_cast(ms)); + ++g_completed; + } + + if (g_cycle < kMeasureCycles) { + StartMeasureCycle(g_cycle + 1); + return; + } + + PrintSummary(); + g_done = true; + g_phase = Phase::kDone; +} + +} // namespace + +void BeginAppMainTiming() {} +void FinalizeCycleBeforeSleep() {} +#if defined(ESP_PLATFORM) +void EnterDeepSleep() {} +#endif + +void setup() { +#if defined(ESP_PLATFORM) + nvs_flash_init(); +#endif + WifiLifecyclePrintf("WIFI_BENCH_START\tvariant=%d\tcooldown_ms=%d\n", kVariant, + kCooldownMs); + g_done = false; + g_completed = 0; + StartRegisterCycle(); +} + +void loop() { + if (g_done || !g_app) { + return; + } + if (!g_app->IsExited()) { + auto t = g_app->Update(ae::Now()); + g_app->WaitUntil(t); + return; + } + FinishCycleAndNext(); +} + +} // namespace temp_sensor + +void setup() { temp_sensor::setup(); } +void loop() { temp_sensor::loop(); } diff --git a/main/wifi_lifecycle_out.h b/main/wifi_lifecycle_out.h new file mode 100644 index 0000000..e970b61 --- /dev/null +++ b/main/wifi_lifecycle_out.h @@ -0,0 +1,46 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * USB Serial JTAG output for Wi-Fi lifecycle benchmark (works with CONSOLE_NONE). + */ +#ifndef TEMP_SENSOR_WIFI_LIFECYCLE_OUT_H_ +#define TEMP_SENSOR_WIFI_LIFECYCLE_OUT_H_ + +#if defined(ESP_PLATFORM) +# include +# include + +# include +# include + +inline void WifiLifecycleEnsureUsbSerial() { + static bool installed = false; + if (!installed) { + usb_serial_jtag_driver_config_t cfg = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT(); + usb_serial_jtag_driver_install(&cfg); + installed = true; + } +} + +inline void WifiLifecycleWriteRaw(char const* data, size_t len) { + if (data != nullptr && len > 0) { + WifiLifecycleEnsureUsbSerial(); + usb_serial_jtag_write_bytes(data, len, portMAX_DELAY); + } +} + +inline void WifiLifecyclePrintf(char const* fmt, ...) { + char buf[160]; + va_list args; + va_start(args, fmt); + int const n = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + if (n > 0) { + WifiLifecycleWriteRaw(buf, static_cast(n)); + } +} +#else +inline void WifiLifecyclePrintf(char const* /*fmt*/, ...) {} +#endif + +#endif // TEMP_SENSOR_WIFI_LIFECYCLE_OUT_H_ From bfb86c0988d771a7418c26350412db44c877402f Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Fri, 28 Aug 2026 10:50:05 -0700 Subject: [PATCH 17/32] Pin Aether BSSID-cache removal and record ESP32-C6 verify runs. Hardware median ~3.6s for 10 FULL cycles without SPIFFS erase; reboot 3 cycles stay in the same band. Co-authored-by: Cursor --- experiments/run_bssid_removed_verify.ps1 | 95 +++++++++++++++++++ .../v40_bssid_removed_capture.log | 47 +++++++++ .../v40_bssid_removed_reboot3.log | 17 ++++ main/CMakeLists.txt | 6 +- 4 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 experiments/run_bssid_removed_verify.ps1 create mode 100644 experiments/wifi_lifecycle/v40_bssid_removed_capture.log create mode 100644 experiments/wifi_lifecycle/v40_bssid_removed_reboot3.log diff --git a/experiments/run_bssid_removed_verify.ps1 b/experiments/run_bssid_removed_verify.ps1 new file mode 100644 index 0000000..8ab943d --- /dev/null +++ b/experiments/run_bssid_removed_verify.ps1 @@ -0,0 +1,95 @@ +param( + [string]$Port = 'COM7', + [string]$ServiceUid = '3d284a4f-ebb4-451e-a2c5-aecb0d647a45', + [int]$CaptureSec = 600 +) +$ErrorActionPreference = 'Stop' +$env:Path = 'C:\Program Files\Git\cmd;C:\Espressif\python_env\idf6.0_py3.11_env\Scripts;' + $env:Path +$env:IDF_PATH = 'C:\Espressif\frameworks\esp-idf-v6.0.2' +. "$env:IDF_PATH\export.ps1" | Out-Null + +$proj = 'C:\Users\nickc\Projects\temperature-sensor-prepared' +$aether = 'C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0' +$build = 'build-esp32c6-save-bench-smoke' +$exp = Join-Path $proj 'experiments\wifi_lifecycle' +New-Item -ItemType Directory -Force -Path $exp | Out-Null +$log = Join-Path $exp 'v40_bssid_removed_capture.log' +$rebootLog = Join-Path $exp 'v40_bssid_removed_reboot3.log' + +function Stop-EspMonitors { + Get-CimInstance Win32_Process -Filter "Name='python.exe'" | + Where-Object { $_.CommandLine -match 'esp_idf_monitor' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } + Start-Sleep -Seconds 2 +} + +Set-Location $proj +Stop-EspMonitors + +# Production baseline driver after BSSID removal. Do NOT erase SPIFFS. +$cmakeArgs = @( + '-B', $build, + '-D', 'WIFI_SSID=chirkov', + '-D', 'WIFI_PASSWORD=kcdjepWz51', + '-D', "SERVICE_UID=$ServiceUid", + '-D', "CPM_aether-client-cpp_SOURCE=$aether", + '-D', 'AE_EXP_WIFI_LIFECYCLE=1', + '-D', 'AE_EXP_WIFI_LIFECYCLE_VARIANT=40', + '-D', 'AE_EXP_WIFI_LIFECYCLE_CYCLES=10', + '-D', 'AE_EXP_SKIP_DTOR_SAVE=1', + '-D', 'AE_EXP_WIFI_COOLDOWN_MS=0', + '-D', 'AE_EXP_WIFI_CANONICAL=0', + '-D', 'AE_EXP_FULL_CYCLES=', + '-D', 'AE_EXP_BENCH_STAGE=', + '-D', 'AE_EXP_SAVE_BENCH_N=', + '-D', 'AE_EXP_LEGACY_MAP_SYNC=', + '-D', 'AE_EXP_WIFI_IGNORE_BSSID=', + '-D', 'AE_EXP_WIFI_FEAT_BSSID_CACHE=' +) + +Write-Output 'BUILD variant=40 production BSSID-removed (no flash erase)' +idf.py @cmakeArgs build flash -p $Port +if ($LASTEXITCODE -ne 0) { throw 'build/flash failed' } + +Remove-Item $log -Force -ErrorAction SilentlyContinue +$elf = Join-Path $proj "$build\temperature_sensor.elf" +$mon = Start-Process -FilePath python -ArgumentList @('-u','-m','esp_idf_monitor','--port',$Port,'--baud','115200',$elf) -WorkingDirectory (Join-Path $proj $build) -RedirectStandardOutput $log -RedirectStandardError "$log.err" -PassThru -NoNewWindow +Start-Sleep -Seconds 2 +$prev = $ErrorActionPreference +$ErrorActionPreference = 'Continue' +python -m esptool --chip esp32c6 -p $Port run 2>&1 | Out-Null +$ErrorActionPreference = $prev + +$deadline = (Get-Date).AddSeconds($CaptureSec) +while ((Get-Date) -lt $deadline) { + if ((Test-Path $log) -and (Select-String -Path $log -Pattern 'WIFI_SUMMARY' -Quiet)) { break } + Start-Sleep -Seconds 5 +} +Stop-Process -Id $mon.Id -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 2 + +Write-Output '--- 10-cycle result ---' +Select-String -Path $log -Pattern '^(cycle=|raw=|min=|median=|max=|completed=|WIFI_SUMMARY)' | ForEach-Object { $_.Line } + +Write-Output '--- reboot + 3 cycles (SPIFFS kept) ---' +Remove-Item $rebootLog -Force -ErrorAction SilentlyContinue +$mon2 = Start-Process -FilePath python -ArgumentList @('-u','-m','esp_idf_monitor','--port',$Port,'--baud','115200',$elf) -WorkingDirectory (Join-Path $proj $build) -RedirectStandardOutput $rebootLog -RedirectStandardError "$rebootLog.err" -PassThru -NoNewWindow +Start-Sleep -Seconds 2 +$ErrorActionPreference = 'Continue' +python -m esptool --chip esp32c6 -p $Port run 2>&1 | Out-Null +$ErrorActionPreference = $prev + +$deadline2 = (Get-Date).AddSeconds(180) +while ((Get-Date) -lt $deadline2) { + if (Test-Path $rebootLog) { + $n = @(Select-String -Path $rebootLog -Pattern '^cycle=\d+ init_to_release_ms=').Count + if ($n -ge 3) { break } + } + Start-Sleep -Seconds 3 +} +Stop-Process -Id $mon2.Id -Force -ErrorAction SilentlyContinue + +Write-Output '--- reboot 3-cycle lines ---' +Select-String -Path $rebootLog -Pattern '^cycle=\d+ init_to_release_ms=' | Select-Object -First 3 | ForEach-Object { $_.Line } +Write-Output "LOG10=$log" +Write-Output "LOG3=$rebootLog" diff --git a/experiments/wifi_lifecycle/v40_bssid_removed_capture.log b/experiments/wifi_lifecycle/v40_bssid_removed_capture.log new file mode 100644 index 0000000..6be91ea --- /dev/null +++ b/experiments/wifi_lifecycle/v40_bssid_removed_capture.log @@ -0,0 +1,47 @@ +ESP-ROM:esp32c6-20220919 +Build:Sep 19 2022 +rst:0x15 (USB_UART_HPSYS),boot:0x6f (SPI_FAST_FLASH_BOOT) +Saved PC:0x40802414 +SPIWP:0xee +mode:DIO, clock div:2 +load:0x40875730,len:0xf4 +load:0x4086b910,len:0x9f8 +load:0x4086e610,len:0x2694 +entry 0x4086b910 +WIFI_BENCH_START variant=40 cooldown_ms=0 +cycle=1 init_to_release_ms=3705 result=OK +WIFI_CYCLE variant=40 cycle=1 init_to_release_ms=3705 +cycle=2 init_to_release_ms=3579 result=OK +WIFI_CYCLE variant=40 cycle=2 init_to_release_ms=3579 +cycle=3 init_to_release_ms=3499 result=OK +WIFI_CYCLE variant=40 cycle=3 init_to_release_ms=3499 +cycle=4 init_to_release_ms=3489 result=OK +WIFI_CYCLE variant=40 cycle=4 init_to_release_ms=3489 +cycle=5 init_to_release_ms=3769 result=OK +WIFI_CYCLE variant=40 cycle=5 init_to_release_ms=3769 +cycle=6 init_to_release_ms=3599 result=OK +WIFI_CYCLE variant=40 cycle=6 init_to_release_ms=3599 +cycle=7 init_to_release_ms=3779 result=OK +WIFI_CYCLE variant=40 cycle=7 init_to_release_ms=3779 +cycle=8 init_to_release_ms=3489 result=OK +WIFI_CYCLE variant=40 cycle=8 init_to_release_ms=3489 +cycle=9 init_to_release_ms=3779 result=OK +WIFI_CYCLE variant=40 cycle=9 init_to_release_ms=3779 +cycle=10 init_to_release_ms=3579 result=OK +WIFI_CYCLE variant=40 cycle=10 init_to_release_ms=3579 +WIFI_SUMMARY variant=40 completed=10 min=3489 median=3589 max=3779 +raw=3705,3579,3499,3489,3769,3599,3779,3489,3779,3579 +min=3489 +median=3589 +max=3779 +completed=10 +WIFI_RAW variant=40 cycle=1 init_to_release_ms=3705 +WIFI_RAW variant=40 cycle=2 init_to_release_ms=3579 +WIFI_RAW variant=40 cycle=3 init_to_release_ms=3499 +WIFI_RAW variant=40 cycle=4 init_to_release_ms=3489 +WIFI_RAW variant=40 cycle=5 init_to_release_ms=3769 +WIFI_RAW variant=40 cycle=6 init_to_release_ms=3599 +WIFI_RAW variant=40 cycle=7 init_to_release_ms=3779 +WIFI_RAW variant=40 cycle=8 init_to_release_ms=3489 +WIFI_RAW variant=40 cycle=9 init_to_release_ms=3779 +WIFI_RAW variant=40 cycle=10 init_to_release_ms=3579 diff --git a/experiments/wifi_lifecycle/v40_bssid_removed_reboot3.log b/experiments/wifi_lifecycle/v40_bssid_removed_reboot3.log new file mode 100644 index 0000000..ab19c38 --- /dev/null +++ b/experiments/wifi_lifecycle/v40_bssid_removed_reboot3.log @@ -0,0 +1,17 @@ +ESP-ROM:esp32c6-20220919 +Build:Sep 19 2022 +rst:0x15 (USB_UART_HPSYS),boot:0x6f (SPI_FAST_FLASH_BOOT) +Saved PC:0x420168e4 +SPIWP:0xee +mode:DIO, clock div:2 +load:0x40875730,len:0xf4 +load:0x4086b910,len:0x9f8 +load:0x4086e610,len:0x2694 +entry 0x4086b910 +WIFI_BENCH_START variant=40 cooldown_ms=0 +cycle=1 init_to_release_ms=3650 result=OK +WIFI_CYCLE variant=40 cycle=1 init_to_release_ms=3650 +cycle=2 init_to_release_ms=3519 result=OK +WIFI_CYCLE variant=40 cycle=2 init_to_release_ms=3519 +cycle=3 init_to_release_ms=3559 result=OK +WIFI_CYCLE variant=40 cycle=3 init_to_release_ms=3559 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 01000e9..927a184 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -90,7 +90,7 @@ else() set(TARGET_NAME "${COMPONENT_LIB}") include(../cmake/CPM.cmake) # Pin exact SHA. Override locally with -DCPM_aether-client-cpp_SOURCE=. - CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#a2ee9ec97af4f83e8507a51b46bf2f03393984c1") + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#157aadbec8e7b852d0f89274307ff7cb8103e5f7") target_link_libraries(${TARGET_NAME} PRIVATE aether) @@ -147,10 +147,8 @@ set(AE_EXP_WIFI_FEAT_AMPDU_OFF "" CACHE STRING "Canonical+bisect: AMPDU off") set(AE_EXP_WIFI_FEAT_SCAN_THRESHOLD "" CACHE STRING "Canonical+bisect: scan threshold") set(AE_EXP_WIFI_FEAT_CONNECT_RETRY10 "" CACHE STRING "Canonical+bisect: 10 connect retries") set(AE_EXP_WIFI_FEAT_FAIL_DISCONNECT "" CACHE STRING "Canonical+bisect: fail disconnect path") -set(AE_EXP_WIFI_FEAT_BSSID_CACHE "" CACHE STRING "Canonical+bisect: BSSID cache") set(AE_EXP_WIFI_FEAT_LEGACY_DEINIT "" CACHE STRING "Canonical+bisect: baseline Deinit") set(AE_EXP_WIFI_FEAT_LEGACY_HANDLERS "" CACHE STRING "Canonical+bisect: non-instance handlers") -set(AE_EXP_WIFI_IGNORE_BSSID "" CACHE STRING "Ignore cached BSSID on Connect (experiment)") function(ae_exp_define_if_set flag_name) if(NOT "${${flag_name}}" STREQUAL "") @@ -172,10 +170,8 @@ ae_exp_define_if_set(AE_EXP_WIFI_FEAT_AMPDU_OFF) ae_exp_define_if_set(AE_EXP_WIFI_FEAT_SCAN_THRESHOLD) ae_exp_define_if_set(AE_EXP_WIFI_FEAT_CONNECT_RETRY10) ae_exp_define_if_set(AE_EXP_WIFI_FEAT_FAIL_DISCONNECT) -ae_exp_define_if_set(AE_EXP_WIFI_FEAT_BSSID_CACHE) ae_exp_define_if_set(AE_EXP_WIFI_FEAT_LEGACY_DEINIT) ae_exp_define_if_set(AE_EXP_WIFI_FEAT_LEGACY_HANDLERS) -ae_exp_define_if_set(AE_EXP_WIFI_IGNORE_BSSID) ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE) ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE_VARIANT) ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE_CYCLES) From 8b887ce0ac7f262d2ba2169ae27755232567cf76 Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Fri, 28 Aug 2026 10:50:24 -0700 Subject: [PATCH 18/32] Align desktop CPM aether pin with BSSID-removal SHA. Co-authored-by: Cursor --- main/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 927a184..6ed4b49 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -58,7 +58,7 @@ if(NOT CM_PLATFORM) include(../cmake/CPM.cmake) # Pin exact SHA. Override locally with -DCPM_aether-client-cpp_SOURCE=. - CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#a2ee9ec97af4f83e8507a51b46bf2f03393984c1") + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#157aadbec8e7b852d0f89274307ff7cb8103e5f7") add_executable(${PROJECT_NAME} ${src_list} ${sleeping_src} ${sensors_src}) set(TARGET_NAME ${PROJECT_NAME}) From cf33566a0e683c0a99862adab2ec5379c8ff6499 Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Fri, 28 Aug 2026 14:23:32 -0700 Subject: [PATCH 19/32] Add no-sleep prepared-message E2E bench and hardware results. Extract SendPreparedOnce for shared hot-path encoding, add ESP harness (AE_EXP_PREPARED_MESSAGE_E2E), desktop receiver, and document the ESP32-C6 coordinated run with timings in experiments/. --- CMakeLists.txt | 8 + experiments/PREPARED_MESSAGE_E2E_REPORT.md | 57 +++ experiments/prepared_message_e2e.tsv | 28 ++ .../prepared_message_receiver/CMakeLists.txt | 25 + .../prepared_message_receiver/main.cpp | 191 +++++++ .../prepared_message_receiver/user_config.h | 22 + experiments/prepared_message_receiver_uid.txt | 1 + experiments/run_prepared_message_e2e.ps1 | 112 +++++ main/CMakeLists.txt | 14 +- main/prepared_message_e2e_bench.cpp | 465 ++++++++++++++++++ main/prepared_send/prepared_send.cpp | 150 ++++-- main/prepared_send/prepared_send.h | 29 +- sdkconfig.defaults.bench | 13 + temperature_receiver/CMakeLists.txt | 29 ++ temperature_receiver/main.cpp | 191 +++++++ temperature_receiver/user_config.h | 22 + ulp/CMakeLists.txt | 2 +- 17 files changed, 1319 insertions(+), 40 deletions(-) create mode 100644 experiments/PREPARED_MESSAGE_E2E_REPORT.md create mode 100644 experiments/prepared_message_e2e.tsv create mode 100644 experiments/prepared_message_receiver/CMakeLists.txt create mode 100644 experiments/prepared_message_receiver/main.cpp create mode 100644 experiments/prepared_message_receiver/user_config.h create mode 100644 experiments/prepared_message_receiver_uid.txt create mode 100644 experiments/run_prepared_message_e2e.ps1 create mode 100644 main/prepared_message_e2e_bench.cpp create mode 100644 sdkconfig.defaults.bench create mode 100644 temperature_receiver/CMakeLists.txt create mode 100644 temperature_receiver/main.cpp create mode 100644 temperature_receiver/user_config.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 199d7c2..a6ab3ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,14 @@ if (ESP_PLATFORM OR IDF_TARGET) set(CM_PLATFORM "ESP32") endif() +# Bench harness sdkconfig (main task stack, USB console). Must be set before project(). +set(AE_EXP_PREPARED_MESSAGE_E2E "" CACHE STRING + "No-sleep prepared-message E2E bench (set to 1)") +if(AE_EXP_PREPARED_MESSAGE_E2E STREQUAL "1") + list(APPEND SDKCONFIG_DEFAULTS + "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.bench") +endif() + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/experiments/PREPARED_MESSAGE_E2E_REPORT.md b/experiments/PREPARED_MESSAGE_E2E_REPORT.md new file mode 100644 index 0000000..bd43aa2 --- /dev/null +++ b/experiments/PREPARED_MESSAGE_E2E_REPORT.md @@ -0,0 +1,57 @@ +# Prepared message E2E (ESP32-C6, no sleep) + +Hardware: ESP32-C6, ESP-IDF v6.0.2, COM7 +Aether: `exp/esp32c6-wifi-lifecycle-diag` @ `157aadbec8e7b852d0f89274307ff7cb8103e5f7` +Firmware build: `build-esp32c6-save-bench-smoke` (`AE_EXP_PREPARED_MESSAGE_E2E=1`) +Receiver: `temperature_receiver/build-prepared-e2e` (`prepared_message_bench_rx_v1`) +Session: `experiments/prepared_message_rx_session` + +## Coordinated run (authoritative) + +| Item | Value | +|------|-------| +| Bench client | `prepared_message_bench_v6` | +| Sender UID | `bb28fcdf-3872-4214-adb1-9ee63f05f85e` | +| Receiver UID | `fd4e309f-1619-4cbb-ada7-c69134901490` | + +### Metrics (ESP serial) + +``` +REGISTRATION + time_ms=1048 +FULL CYCLE + time_ms=11715 +PREPARED + raw=[2250, 2250, 2190, 2340, 2390, 2240, 2290, 2340, 2230] + min=2190 + median=2250 + p90=2340 + max=2390 + completed=9/10 +prepared block: + reserved=10 + remaining=1 +``` + +PREPARED #1: `wifi-failed` / `connect_timeout` (~15090 ms, nonce not consumed). +PREPARED #2–#10: `sent` (~2.2–2.4 s each). + +### Receiver (desktop console) + +``` +RECEIVER + full=1/1 + prepared=9/10 + missing=1 [1] + duplicates=0 +``` + +Observed RECV: `FULL:0`, `PREPARED:2` … `PREPARED:10` (no `PREPARED:1`). + +## Notes + +- Single boot: registration → destroy Æther → FULL + block(10) → destroy Æther → 10 hot-path sends (1 s gap outside timer). +- `prepared_packet/` identical between experiment SHA and main `07841f3c`; no cherry-pick. +- No-sleep bench calls `ReleaseFullAetherWifiForHotPath()` before prepared loop (Aether leaves ESP-IDF Wi-Fi up; production uses deep-sleep reboot). +- First prepared send after release hits hot-path Wi-Fi `connect_timeout`; local RTC Wi-Fi cache not warm until after first successful connect. +- `prepared_message_e2e.tsv` — raw timings for this run. diff --git a/experiments/prepared_message_e2e.tsv b/experiments/prepared_message_e2e.tsv new file mode 100644 index 0000000..85e710e --- /dev/null +++ b/experiments/prepared_message_e2e.tsv @@ -0,0 +1,28 @@ +metric value +registration_ms 1048 +full_cycle_ms 11715 +prepared_message_ms_1 15090 +prepared_message_ms_2 2250 +prepared_message_ms_3 2250 +prepared_message_ms_4 2190 +prepared_message_ms_5 2340 +prepared_message_ms_6 2390 +prepared_message_ms_7 2240 +prepared_message_ms_8 2290 +prepared_message_ms_9 2340 +prepared_message_ms_10 2230 +prepared_min_ms 2190 +prepared_median_ms 2250 +prepared_p90_ms 2340 +prepared_max_ms 2390 +prepared_completed_esp 9 +prepared_completed_receiver 9 +receiver_full 1 +receiver_prepared 9 +receiver_missing 1 +receiver_duplicates 0 +bench_client_id prepared_message_bench_v6 +receiver_uid fd4e309f-1619-4cbb-ada7-c69134901490 +sender_uid bb28fcdf-3872-4214-adb1-9ee63f05f85e +prepared_block_reserved 10 +prepared_block_remaining 1 diff --git a/experiments/prepared_message_receiver/CMakeLists.txt b/experiments/prepared_message_receiver/CMakeLists.txt new file mode 100644 index 0000000..ed02ffc --- /dev/null +++ b/experiments/prepared_message_receiver/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright 2026 Aethernet Inc. +cmake_minimum_required(VERSION 3.16) +project(prepared_message_receiver LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(USER_CONFIG "${CMAKE_CURRENT_LIST_DIR}/user_config.h" CACHE PATH "" FORCE) +set(AE_DISTILLATION OFF CACHE BOOL "" FORCE) +set(AE_FILTRATION ON CACHE BOOL "" FORCE) +set(AE_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(AE_BUILD_TESTS OFF CACHE BOOL "" FORCE) + +include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/CPM.cmake") +# Pin exact SHA. Override locally with -DCPM_aether-client-cpp_SOURCE=. +CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#157aadbec8e7b852d0f89274307ff7cb8103e5f7") + +if(TARGET aether AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(aether PRIVATE -Wno-error -Wno-cpp) +endif() + +add_executable(prepared_message_receiver main.cpp) +target_compile_definitions(prepared_message_receiver PRIVATE + "USER_CONFIG=\"${CMAKE_CURRENT_LIST_DIR}/user_config.h\"") +target_link_libraries(prepared_message_receiver PRIVATE aether) diff --git a/experiments/prepared_message_receiver/main.cpp b/experiments/prepared_message_receiver/main.cpp new file mode 100644 index 0000000..d898fc1 --- /dev/null +++ b/experiments/prepared_message_receiver/main.cpp @@ -0,0 +1,191 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Desktop Æther console receiver for prepared-message E2E bench. + * Expects UTF-8 payloads: FULL:0 and PREPARED:1..10 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +# include +#endif + +#include "aether/all.h" + +using namespace std::chrono_literals; + +namespace { + +static constexpr auto kParentUid = + ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); +static constexpr char const* kClientName = "prepared_message_bench_rx_v1"; +static constexpr int kExpectedPrepared = 10; + +std::mutex g_mu; +std::vector> g_streams; + +bool g_full_seen = false; +std::array g_prepared_hits{}; // 1..10 +int g_prepared_unique = 0; +int g_duplicates = 0; +std::vector g_order; + +std::int64_t NowMs() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + +void PrintSummary() { + int missing = 0; + std::string missing_list; + for (int i = 1; i <= kExpectedPrepared; ++i) { + if (g_prepared_hits[static_cast(i)] == 0) { + ++missing; + if (!missing_list.empty()) { + missing_list += ","; + } + missing_list += std::to_string(i); + } + } + std::cout << "RECEIVER\n"; + std::cout << " full=" << (g_full_seen ? 1 : 0) << "/1\n"; + std::cout << " prepared=" << g_prepared_unique << "/" << kExpectedPrepared + << "\n"; + std::cout << " missing=" << missing; + if (missing > 0) { + std::cout << " [" << missing_list << "]"; + } + std::cout << "\n"; + std::cout << " duplicates=" << g_duplicates << "\n"; + std::cout << " order="; + for (size_t i = 0; i < g_order.size(); ++i) { + if (i > 0) { + std::cout << ","; + } + std::cout << g_order[i]; + } + std::cout << "\n"; + std::cout.flush(); +} + +void OnMessage(ae::Uid sender, ae::DataBuffer const& data) { + auto text = std::string_view{reinterpret_cast(data.data()), + data.size()}; + auto const ts = NowMs(); + auto const sender_text = ae::Format("{}", sender); + + std::lock_guard lock{g_mu}; + if (text == "FULL:0") { + if (g_full_seen) { + ++g_duplicates; + } + g_full_seen = true; + g_order.emplace_back("FULL:0"); + std::cout << ae::Format( + "RECV sender={} sequence=0 type=FULL receive_ts_ms={}\n", sender_text, + ts); + } else if (text.rfind("PREPARED:", 0) == 0) { + auto const seq_sv = text.substr(std::string_view{"PREPARED:"}.size()); + int seq = 0; + try { + seq = std::stoi(std::string{seq_sv}); + } catch (...) { + seq = -1; + } + if (seq >= 1 && seq <= kExpectedPrepared) { + if (g_prepared_hits[static_cast(seq)] > 0) { + ++g_duplicates; + } else { + ++g_prepared_unique; + } + ++g_prepared_hits[static_cast(seq)]; + g_order.emplace_back(std::string{text}); + } + std::cout << ae::Format( + "RECV sender={} sequence={} type=PREPARED receive_ts_ms={}\n", + sender_text, seq, ts); + } else { + std::cout << ae::Format( + "RECV sender={} sequence=? type=UNKNOWN receive_ts_ms={} text={}\n", + sender_text, ts, text); + } + std::cout.flush(); + + if (g_full_seen && g_prepared_unique == kExpectedPrepared) { + PrintSummary(); + } +} + +std::filesystem::path ResolveSessionRoot() { +#if defined(_WIN32) + if (char const* env = std::getenv("AE_RECEIVER_SESSION_DIR")) { + return std::filesystem::path{env}; + } +#endif + return std::filesystem::current_path(); +} + +} // namespace + +int main() { + std::cout.setf(std::ios::unitbuf); +#if defined(_WIN32) + setvbuf(stdout, nullptr, _IONBF, 0); +#endif + auto const session_root = ResolveSessionRoot(); + std::filesystem::create_directories(session_root / "state"); + std::filesystem::current_path(session_root); + std::cerr << ae::Format("receiver_session_dir={}\n", session_root.string()); + std::cerr.flush(); + + auto aether_app = ae::AetherApp::Construct(ae::AetherAppContext{}); + ae::Client::ptr client; + aether_app->aether() + ->SelectClient(kParentUid, kClientName) + .result_event() + .Subscribe([&](ae::Result const& res) { + if (!res) { + std::cerr << "SelectClient failed\n"; + aether_app->Exit(1); + return; + } + client = res.value(); + std::cout << ae::Format("RECEIVER_UID={}\n", client->uid()); + std::cout.flush(); + client->connectivity_policy()->ResetRxTimings(); + client->connectivity_policy() + ->ConfigureRxTimings(ae::RequestPolicy::All{}) + .ForAllPriorities(ae::RxTimingConf::Every(1s).WithWindow(1s)); + client->message_stream_manager().new_port_event().Subscribe( + [&](ae::P2pPortHandle handle) { + auto sender = handle.destination(); + auto stream = std::make_unique( + *aether_app, client.Load(), sender, std::move(handle)); + stream->out_data_event().Subscribe( + [sender](auto const& d) { OnMessage(sender, d); }); + std::lock_guard lock{g_mu}; + g_streams.push_back(std::move(stream)); + }); + }); + + while (!aether_app->IsExited()) { + auto next = aether_app->Update(ae::Now()); + aether_app->WaitUntil(next); + } + { + std::lock_guard lock{g_mu}; + PrintSummary(); + } + return aether_app->ExitCode(); +} diff --git a/experiments/prepared_message_receiver/user_config.h b/experiments/prepared_message_receiver/user_config.h new file mode 100644 index 0000000..da553dd --- /dev/null +++ b/experiments/prepared_message_receiver/user_config.h @@ -0,0 +1,22 @@ +/* + * Copyright 2026 Aethernet Inc. + */ +#ifndef USER_CONFIG_H_ +#define USER_CONFIG_H_ + +#include "aether/config_consts.h" + +#define AE_CRYPTO_ASYNC AE_HYDRO_CRYPTO_PK +#define AE_CRYPTO_SYNC AE_HYDRO_CRYPTO_SK +#define AE_SIGNATURE AE_HYDRO_SIGNATURE +#define AE_KDF AE_HYDRO_KDF + +#define AE_TELE_ENABLED 1 +#define AE_TELE_LOG_CONSOLE 1 +#if defined NDEBUG +# define AE_TELE_DEBUG_MODULES 0 +#else +# define AE_TELE_DEBUG_MODULES AE_ALL +#endif + +#endif // USER_CONFIG_H_ diff --git a/experiments/prepared_message_receiver_uid.txt b/experiments/prepared_message_receiver_uid.txt new file mode 100644 index 0000000..7229664 --- /dev/null +++ b/experiments/prepared_message_receiver_uid.txt @@ -0,0 +1 @@ +fd4e309f-1619-4cbb-ada7-c69134901490 diff --git a/experiments/run_prepared_message_e2e.ps1 b/experiments/run_prepared_message_e2e.ps1 new file mode 100644 index 0000000..a1f50d3 --- /dev/null +++ b/experiments/run_prepared_message_e2e.ps1 @@ -0,0 +1,112 @@ +param( + [string]$Port = 'COM7', + [string]$ServiceUid = '', + [string]$BenchClientId = 'prepared_message_bench_v6', + [int]$CaptureSec = 900 +) +$ErrorActionPreference = 'Stop' +$env:Path = 'C:\Program Files\Git\cmd;C:\msys64\ucrt64\bin;C:\Espressif\python_env\idf6.0_py3.11_env\Scripts;' + $env:Path +$env:IDF_PATH = 'C:\Espressif\frameworks\esp-idf-v6.0.2' +. "$env:IDF_PATH\export.ps1" | Out-Null + +$proj = 'C:\Users\nickc\Projects\temperature-sensor-prepared' +$aether = 'C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0' +$buildEsp = 'build-esp32c6-save-bench-smoke' +$rxRoot = Join-Path $proj 'experiments\prepared_message_receiver' +$rxBuild = Join-Path $rxRoot 'build-mingw' +$rxSession = Join-Path $proj 'experiments\prepared_message_rx_session' +$exp = Join-Path $proj 'experiments' +$espLog = Join-Path $exp 'prepared_message_e2e_esp.log' +$rxLog = Join-Path $exp 'prepared_message_e2e_receiver.log' + +function Stop-EspMonitors { + Get-CimInstance Win32_Process -Filter "Name='python.exe'" | + Where-Object { $_.CommandLine -match 'esp_idf_monitor|idf_monitor|serial' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } + Get-Process prepared_message_receiver -ErrorAction SilentlyContinue | + Stop-Process -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 +} + +New-Item -ItemType Directory -Force -Path $exp, $rxSession | Out-Null +Set-Location $proj +Stop-EspMonitors + +Write-Output 'BUILD desktop prepared_message_receiver' +New-Item -ItemType Directory -Force -Path $rxBuild | Out-Null +cmake -S $rxRoot -B $rxBuild -G Ninja ` + -DCMAKE_BUILD_TYPE=Release ` + "-DCPM_aether-client-cpp_SOURCE=$aether" +if ($LASTEXITCODE -ne 0) { throw 'receiver cmake failed' } +cmake --build $rxBuild --parallel +if ($LASTEXITCODE -ne 0) { throw 'receiver build failed' } + +$env:AE_RECEIVER_SESSION_DIR = $rxSession +$rxExe = Join-Path $proj 'temperature_receiver\build-prepared-e2e\temperature_receiver.exe' +Remove-Item $rxLog -ErrorAction SilentlyContinue +$rxProc = Start-Process -FilePath $rxExe -RedirectStandardOutput $rxLog ` + -RedirectStandardError ($rxLog + '.err') -PassThru -NoNewWindow +Write-Output "receiver pid=$($rxProc.Id)" + +# Wait for RECEIVER_UID (allow cloud registration on desktop) +$uid = $ServiceUid +$deadline = (Get-Date).AddMinutes(10) +while ([string]::IsNullOrWhiteSpace($uid) -and (Get-Date) -lt $deadline) { + Start-Sleep -Seconds 2 + if (Test-Path $rxLog) { + $m = Select-String -Path $rxLog -Pattern 'RECEIVER_UID=([0-9a-fA-F\-]+)' | + Select-Object -Last 1 + if ($m) { $uid = $m.Matches[0].Groups[1].Value } + } +} +if ([string]::IsNullOrWhiteSpace($uid)) { + throw 'receiver UID not ready' +} +Set-Content -Path (Join-Path $exp 'prepared_message_receiver_uid.txt') -Value $uid +Write-Output "SERVICE_UID=$uid" + +Write-Output 'BUILD ESP prepared-message E2E (no flash erase)' +$cmakeArgs = @( + '-B', $buildEsp, + '-D', 'WIFI_SSID=chirkov', + '-D', 'WIFI_PASSWORD=kcdjepWz51', + '-D', "SERVICE_UID=$uid", + '-D', "BENCH_CLIENT_ID=$BenchClientId", + '-D', "CPM_aether-client-cpp_SOURCE=$aether", + '-D', 'AE_EXP_PREPARED_MESSAGE_E2E=1', + '-D', 'AE_EXP_SKIP_DTOR_SAVE=1', + '-D', 'AE_EXP_WIFI_LIFECYCLE=', + '-D', 'AE_EXP_FULL_CYCLES=', + '-D', 'AETHER_PREPARED_NONCE_RESERVE=10' +) +idf.py @cmakeArgs reconfigure +if ($LASTEXITCODE -ne 0) { throw 'esp reconfigure failed' } +idf.py -B $buildEsp build +if ($LASTEXITCODE -ne 0) { throw 'esp build failed' } +idf.py -B $buildEsp -p $Port flash +if ($LASTEXITCODE -ne 0) { throw 'esp flash failed' } + +Remove-Item $espLog -ErrorAction SilentlyContinue +$mon = Start-Process -FilePath 'python' -ArgumentList @( + "$env:IDF_PATH\tools\idf_monitor.py", '-p', $Port, '-b', '115200', + '--print_filter', '*:I' +) -RedirectStandardOutput $espLog -RedirectStandardError ($espLog + '.err') ` + -PassThru -NoNewWindow + +Write-Output "monitor pid=$($mon.Id); capture ${CaptureSec}s" +$done = $false +$end = (Get-Date).AddSeconds($CaptureSec) +while ((Get-Date) -lt $end) { + Start-Sleep -Seconds 5 + if (Test-Path $espLog) { + if (Select-String -Path $espLog -Pattern 'PREPARED_E2E_DONE' -Quiet) { + $done = $true + break + } + } +} + +Stop-EspMonitors +Write-Output "done=$done" +Write-Output "esp_log=$espLog" +Write-Output "rx_log=$rxLog" diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 6ed4b49..f3044ad 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -18,7 +18,12 @@ list(APPEND src_list "main.cpp" ) -if(AE_EXP_WIFI_LIFECYCLE) +if(AE_EXP_PREPARED_MESSAGE_E2E) + list(APPEND src_list + "prepared_message_e2e_bench.cpp" + "prepared_send/prepared_send.cpp" + ) +elseif(AE_EXP_WIFI_LIFECYCLE) list(APPEND src_list "wifi_lifecycle_bench.cpp") elseif(AE_EXP_FULL_CYCLES) list(APPEND src_list "thermometer_full_cycles.cpp") @@ -142,6 +147,8 @@ set(AE_EXP_WIFI_LIFECYCLE "" CACHE STRING "Wi-Fi lifecycle bench harness (set to set(AE_EXP_WIFI_LIFECYCLE_VARIANT "" CACHE STRING "Wi-Fi lifecycle variant id (0=baseline)") set(AE_EXP_WIFI_LIFECYCLE_CYCLES "" CACHE STRING "Wi-Fi lifecycle cycles (default 10)") set(AE_EXP_WIFI_COOLDOWN_MS "" CACHE STRING "Cooldown between cycles, outside timer (ms)") +set(AE_EXP_PREPARED_MESSAGE_E2E "" CACHE STRING "No-sleep prepared-message E2E bench (set to 1)") +set(BENCH_CLIENT_ID "" CACHE STRING "Bench SelectClient id (default prepared_message_bench_v1)") set(AE_EXP_WIFI_CANONICAL "" CACHE STRING "Canonical ESP-IDF Wi-Fi driver (set to 1)") set(AE_EXP_WIFI_FEAT_AMPDU_OFF "" CACHE STRING "Canonical+bisect: AMPDU off") set(AE_EXP_WIFI_FEAT_SCAN_THRESHOLD "" CACHE STRING "Canonical+bisect: scan threshold") @@ -176,6 +183,11 @@ ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE) ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE_VARIANT) ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE_CYCLES) ae_exp_define_if_set(AE_EXP_WIFI_COOLDOWN_MS) +ae_exp_define_if_set(AE_EXP_PREPARED_MESSAGE_E2E) +if(NOT "${BENCH_CLIENT_ID}" STREQUAL "") + target_compile_definitions(${TARGET_NAME} PRIVATE + "BENCH_CLIENT_ID=\"${BENCH_CLIENT_ID}\"") +endif() # Continuous prepared-send E2E test defaults (override via -D...). if(NOT DEFINED AETHER_PREPARED_NONCE_RESERVE) diff --git a/main/prepared_message_e2e_bench.cpp b/main/prepared_message_e2e_bench.cpp new file mode 100644 index 0000000..e0428e9 --- /dev/null +++ b/main/prepared_message_e2e_bench.cpp @@ -0,0 +1,465 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * No-sleep prepared-message E2E bench on ESP32-C6: + * 1) one registration + * 2) one FULL cycle + PrepareSendMessageBlock(10) + * 3) ten prepared sends without AetherApp (1s gap outside timer) + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "aether/all.h" +#include "aether/ae_exp_wifi.h" +#include "aether/config.h" +#include "aether/env.h" +#include "prepared_send/prepared_send.h" +#include "wifi_lifecycle_out.h" + +#if defined(ESP_PLATFORM) +# include +# include +# include +# include +# include +# include +#endif + +using namespace std::chrono_literals; + +namespace temp_sensor { +namespace { + +static constexpr auto kParentUid = + ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); + +#ifndef BENCH_CLIENT_ID +# define BENCH_CLIENT_ID "prepared_message_bench_v1" +#endif +static constexpr char const* kBenchClientId = BENCH_CLIENT_ID; + +#if defined(SERVICE_UID) +static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID); +#else +static constexpr auto kServiceUid = + ae::Uid::FromString("3d284a4f-ebb4-451e-a2c5-aecb0d647a45"); +#endif + +static constexpr int kPreparedCount = 10; +static constexpr int kPreparedGapMs = 1000; + +#if defined(ESP_PLATFORM) +static const auto kWifiInit = ae::WiFiInit{ + std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}}, + {}, +}; + +static bool g_had_aether_app = false; + +static void PreConstructCleanup() { + if (!g_had_aether_app) { + return; + } +# if !AE_WIFI_USE_FULL_DEINIT + esp_netif_deinit(); + esp_event_loop_delete_default(); +# endif +} + +static std::int64_t NowUs() { return esp_timer_get_time(); } +#endif + +enum class Phase : std::uint8_t { + kRegister, + kFullCycle, + kPrepared, + kDone, +}; + +static std::shared_ptr g_app; +static ae::Client::ptr g_client; +static std::unique_ptr g_stream; +static ae::Subscription g_select_sub; +static ae::Subscription g_stream_sub; +static ae::Subscription g_write_sub; + +static Phase g_phase = Phase::kRegister; +static bool g_registration_pending = false; +static bool g_write_armed = false; +static bool g_done = false; +static int g_prepared_index = 0; // next PREPARED sequence to send (1..10) +static bool g_prepared_waiting_gap = false; +#if defined(ESP_PLATFORM) +static TickType_t g_prepared_gap_until = 0; +#endif + +static std::uint32_t g_registration_ms = 0; +static std::uint32_t g_full_cycle_ms = 0; +static std::array g_prepared_ms{}; +static int g_prepared_completed = 0; + +#if defined(ESP_PLATFORM) +static std::int64_t g_t0 = 0; +#endif + +static void ReleaseApp() { + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + g_app.reset(); +} + +static void StartRegister(); +static void StartFullCycle(); +static void StartPreparedPhase(); +static void TickPreparedPhase(); +static void PrintFinal(); + +static void OnRegisterReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + auto const uid_text = ae::Format("{}", g_client->uid()); + WifiLifecyclePrintf("REGISTRATION_CLIENT_UID=%s\n", uid_text.c_str()); + g_app->aether().Save(); + g_app->Exit(0); +} + +static void DoFullWrite() { + if (g_write_armed) { + return; + } + g_write_armed = true; + auto payload = prepared_send::MakeBenchPayload("FULL", 0); + auto& wa = g_stream->Write(std::move(payload)); + g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) { + if (st != ae::WriteAction::Status::kSuccess) { + WifiLifecyclePrintf("FULL_CYCLE_ERR write_fail\n"); + g_app->Exit(1); + return; + } + + if (!prepared_send::ExportPreparedSendBlock(g_client, kServiceUid, + kPreparedCount)) { + WifiLifecyclePrintf("FULL_CYCLE_ERR prepare_block_fail\n"); + g_app->Exit(1); + return; + } + + auto const left = prepared_send::PreparedMessageLeft(); + if (!prepared_send::HasPreparedSendBlock() || left != kPreparedCount) { + WifiLifecyclePrintf( + "FULL_CYCLE_ERR block_invalid left=%lu expected=%d\n", + static_cast(left), kPreparedCount); + g_app->Exit(1); + return; + } + + WifiLifecyclePrintf("PREPARED_BLOCK reserved=%d remaining=%lu\n", + kPreparedCount, static_cast(left)); + g_app->aether().Save(); + g_app->Exit(0); + }); +} + +static void MaybeFullWrite() { + if (!g_stream || g_write_armed) { + return; + } + if (!g_stream->stream_info().is_writable) { + return; + } + DoFullWrite(); +} + +static void OnFullClientReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + auto client = g_client.Load(); + g_stream = std::make_unique(*g_app, client, kServiceUid, + ae::P2pPortHandle{}); + g_stream_sub = + g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); }); + MaybeFullWrite(); +} + +static void StartRegister() { + g_phase = Phase::kRegister; + g_write_armed = false; +#if defined(ESP_PLATFORM) + PreConstructCleanup(); + g_t0 = NowUs(); +#endif + WifiLifecyclePrintf("REGISTRATION_START client_id=%s\n", kBenchClientId); + WifiLifecyclePrintf("REGISTRATION_BEFORE_CONSTRUCT\n"); + g_had_aether_app = true; + g_app = ae::AetherApp::Construct( + ae::AetherAppContext{} +#if AE_DISTILLATION && defined(ESP_PLATFORM) + .AddAdapterFactory([&](ae::AetherAppContext const& ctx) { + return ae::WifiAdapter::ptr::Create( + ae::CreateWith{ctx.domain()}.with_id( + ae::GlobalId::kWiFiAdapter), + ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit); + }) +#endif + ); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + WifiLifecyclePrintf("REGISTRATION_ERR select_fail\n"); + g_app->Exit(1); + return; + } + OnRegisterReady(std::move(res).value()); + }); + WifiLifecyclePrintf("REGISTRATION_AFTER_CONSTRUCT\n"); +} + +static void StartFullCycle() { + g_phase = Phase::kFullCycle; + g_write_armed = false; + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; +#if defined(ESP_PLATFORM) + PreConstructCleanup(); + g_t0 = NowUs(); +#endif + WifiLifecyclePrintf("FULL_CYCLE_START\n"); + g_app = ae::AetherApp::Construct( + ae::AetherAppContext{} +#if AE_DISTILLATION && defined(ESP_PLATFORM) + .AddAdapterFactory([&](ae::AetherAppContext const& ctx) { + return ae::WifiAdapter::ptr::Create( + ae::CreateWith{ctx.domain()}.with_id( + ae::GlobalId::kWiFiAdapter), + ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit); + }) +#endif + ); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + WifiLifecyclePrintf("FULL_CYCLE_ERR select_fail\n"); + g_app->Exit(1); + return; + } + OnFullClientReady(std::move(res).value()); + }); +} + +static std::uint32_t Percentile(std::array vals, + int count, int pct) { + if (count <= 0) { + return 0; + } + std::sort(vals.begin(), vals.begin() + count); + auto const idx = (pct * (count - 1) + 99) / 100; + return vals[static_cast(idx)]; +} + +static void PrintFinal() { + std::uint32_t min_ms = UINT32_MAX; + std::uint32_t max_ms = 0; + for (int i = 0; i < g_prepared_completed; ++i) { + auto const v = g_prepared_ms[static_cast(i)]; + min_ms = std::min(min_ms, v); + max_ms = std::max(max_ms, v); + } + if (g_prepared_completed == 0) { + min_ms = 0; + } + auto const median = + Percentile(g_prepared_ms, g_prepared_completed, 50); + auto const p90 = Percentile(g_prepared_ms, g_prepared_completed, 90); + + WifiLifecyclePrintf("REGISTRATION\n"); + WifiLifecyclePrintf(" time_ms=%lu\n", + static_cast(g_registration_ms)); + WifiLifecyclePrintf("FULL CYCLE\n"); + WifiLifecyclePrintf(" time_ms=%lu\n", + static_cast(g_full_cycle_ms)); + WifiLifecyclePrintf("PREPARED\n"); + WifiLifecyclePrintf(" raw=["); + for (int i = 0; i < g_prepared_completed; ++i) { + if (i > 0) { + WifiLifecyclePrintf(", "); + } + WifiLifecyclePrintf( + "%lu", + static_cast(g_prepared_ms[static_cast(i)])); + } + WifiLifecyclePrintf("]\n"); + WifiLifecyclePrintf(" min=%lu\n", static_cast(min_ms)); + WifiLifecyclePrintf(" median=%lu\n", static_cast(median)); + WifiLifecyclePrintf(" p90=%lu\n", static_cast(p90)); + WifiLifecyclePrintf(" max=%lu\n", static_cast(max_ms)); + WifiLifecyclePrintf(" completed=%d/%d\n", g_prepared_completed, + kPreparedCount); + WifiLifecyclePrintf("prepared block:\n"); + WifiLifecyclePrintf(" reserved=%d\n", kPreparedCount); + WifiLifecyclePrintf( + " remaining=%lu\n", + static_cast(prepared_send::PreparedMessageLeft())); + WifiLifecyclePrintf("PREPARED_E2E_DONE\n"); +} + +static void StartPreparedPhase() { + g_phase = Phase::kPrepared; + g_prepared_index = 1; + g_prepared_waiting_gap = false; +#if defined(ESP_PLATFORM) + prepared_send::ReleaseFullAetherWifiForHotPath(); +#endif + WifiLifecyclePrintf("PREPARED_LOOP_START count=%d\n", kPreparedCount); +} + +static void TickPreparedPhase() { + if (g_done || g_phase != Phase::kPrepared) { + return; + } + +#if defined(ESP_PLATFORM) + if (g_prepared_waiting_gap) { + if (xTaskGetTickCount() < g_prepared_gap_until) { + vTaskDelay(pdMS_TO_TICKS(20)); + return; + } + g_prepared_waiting_gap = false; + } +#endif + + if (g_prepared_index > kPreparedCount) { + PrintFinal(); + g_phase = Phase::kDone; + g_done = true; + return; + } + + int const i = g_prepared_index; +#if defined(ESP_PLATFORM) + auto const t0 = NowUs(); +#endif + auto payload = prepared_send::MakeBenchPayload("PREPARED", i); + auto const status = prepared_send::SendPreparedOnce(payload); +#if defined(ESP_PLATFORM) + auto const ms = static_cast((NowUs() - t0) / 1000); +#else + auto const ms = 0U; +#endif + auto const left = prepared_send::PreparedMessageLeft(); + auto const status_text = std::string{prepared_send::ToString(status)}; + WifiLifecyclePrintf( + "PREPARED %d time_ms=%lu status=%s message_left=%lu\n", i, + static_cast(ms), status_text.c_str(), + static_cast(left)); + if (status == prepared_send::HotSendStatus::kSent && + g_prepared_completed < kPreparedCount) { + g_prepared_ms[static_cast(g_prepared_completed)] = ms; + ++g_prepared_completed; + } + + ++g_prepared_index; + if (g_prepared_index <= kPreparedCount) { +#if defined(ESP_PLATFORM) + g_prepared_waiting_gap = true; + g_prepared_gap_until = + xTaskGetTickCount() + pdMS_TO_TICKS(kPreparedGapMs); +#else + g_prepared_waiting_gap = false; +#endif + } +} + +void BeginAppMainTiming() {} +void FinalizeCycleBeforeSleep() {} +#if defined(ESP_PLATFORM) +void EnterDeepSleep() {} +#endif + +void setup() { +#if defined(ESP_PLATFORM) + nvs_flash_init(); +#endif + auto const service_text = ae::Format("{}", kServiceUid); + WifiLifecyclePrintf("PREPARED_E2E_START client_id=%s service=%s\n", + kBenchClientId, service_text.c_str()); + g_done = false; + g_prepared_index = 0; + g_prepared_waiting_gap = false; + g_prepared_completed = 0; + g_registration_pending = true; +} + +void loop() { + if (g_done) { + return; + } + + if (g_registration_pending) { + g_registration_pending = false; + StartRegister(); + return; + } + + if (g_phase == Phase::kPrepared) { + TickPreparedPhase(); + return; + } + + if (!g_app) { + return; + } + + if (!g_app->IsExited()) { + auto t = g_app->Update(ae::Now()); + g_app->WaitUntil(t); + return; + } + + if (g_phase == Phase::kRegister) { + ReleaseApp(); +#if defined(ESP_PLATFORM) + g_registration_ms = static_cast((NowUs() - g_t0) / 1000); +#else + g_registration_ms = 0; +#endif + WifiLifecyclePrintf("REGISTRATION time_ms=%lu\n", + static_cast(g_registration_ms)); + StartFullCycle(); + return; + } + + if (g_phase == Phase::kFullCycle) { + ReleaseApp(); +#if defined(ESP_PLATFORM) + g_full_cycle_ms = static_cast((NowUs() - g_t0) / 1000); +#else + g_full_cycle_ms = 0; +#endif + WifiLifecyclePrintf("FULL CYCLE time_ms=%lu\n", + static_cast(g_full_cycle_ms)); + // No further AetherApp construction — prepared hot path only. + StartPreparedPhase(); + } +} + +} // namespace + +} // namespace temp_sensor + +void setup() { temp_sensor::setup(); } +void loop() { temp_sensor::loop(); } diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index c392700..97eb935 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -25,6 +24,10 @@ #include "aether/prepared_packet/packet_encoder.h" #include "aether/prepared_packet/prepared_send_message.h" +#if !defined(ESP_PLATFORM) +# include +#endif + #if defined(ESP_PLATFORM) # include # include @@ -40,9 +43,19 @@ # include # include # include +# include "wifi_lifecycle_out.h" #endif namespace temp_sensor::prepared_send { +#if defined(ESP_PLATFORM) +static char g_last_hot_wifi_fail[64] = {}; + +static void SetHotWifiFail(char const* msg) { + std::strncpy(g_last_hot_wifi_fail, msg, sizeof(g_last_hot_wifi_fail) - 1); + g_last_hot_wifi_fail[sizeof(g_last_hot_wifi_fail) - 1] = '\0'; +} +#endif + namespace { #ifndef AETHER_PREPARED_NONCE_RESERVE @@ -244,14 +257,17 @@ void CleanupHotPathWifi() { bool EnsureWifiConnectedForHotPath() { # ifndef WIFI_SSID + SetHotWifiFail("WIFI_SSID undefined"); ESP_LOGE(kTag, "WIFI_SSID is not defined"); return false; # endif # ifndef WIFI_PASSWORD + SetHotWifiFail("WIFI_PASSWORD undefined"); ESP_LOGE(kTag, "WIFI_PASSWORD is not defined"); return false; # endif + SetHotWifiFail(""); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_config_t wifi_config{}; @@ -263,6 +279,7 @@ bool EnsureWifiConnectedForHotPath() { err = nvs_flash_init(); } if (err != ESP_OK && err != ESP_ERR_NVS_NO_FREE_PAGES) { + SetHotWifiFail("nvs_flash_init"); ESP_LOGE(kTag, "nvs_flash_init failed: %s", esp_err_to_name(err)); return false; } @@ -278,6 +295,7 @@ bool EnsureWifiConnectedForHotPath() { } else if (err == ESP_ERR_INVALID_STATE) { ESP_LOGW(kTag, "event loop already exists; continuing"); } else { + SetHotWifiFail("event_loop_create"); ESP_LOGE(kTag, "esp_event_loop_create_default failed: %s", esp_err_to_name(err)); return false; @@ -285,20 +303,25 @@ bool EnsureWifiConnectedForHotPath() { g_wifi_event_group = xEventGroupCreate(); if (g_wifi_event_group == nullptr) { + SetHotWifiFail("event_group_create"); ESP_LOGE(kTag, "failed to create Wi-Fi event group"); CleanupHotPathWifi(); return false; } - g_wifi_netif = esp_netif_create_default_wifi_sta(); + g_wifi_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (g_wifi_netif == nullptr) { + g_wifi_netif = esp_netif_create_default_wifi_sta(); + } if (g_wifi_netif == nullptr) { + SetHotWifiFail("create_default_wifi_sta"); ESP_LOGE(kTag, "failed to create default Wi-Fi STA netif"); CleanupHotPathWifi(); return false; } if (address_is_valid) { - std::cout << "Restoring netif config\n"; + ESP_LOGI(kTag, "Restoring netif config"); esp_netif_dhcpc_stop(g_wifi_netif); esp_netif_ip_info_t ip_info = { .ip = {.addr = rtc_ip_info.ip.addr}, @@ -306,7 +329,7 @@ bool EnsureWifiConnectedForHotPath() { .gw = {.addr = rtc_ip_info.gw.addr}}; esp_netif_set_ip_info(g_wifi_netif, &ip_info); } else { - std::cout << "Restoring netif config filed\n"; + ESP_LOGI(kTag, "No cached netif config"); } // We disable aggregation so that the packages go out one by one and quickly @@ -314,12 +337,16 @@ bool EnsureWifiConnectedForHotPath() { cfg.ampdu_tx_enable = 0; err = esp_wifi_init(&cfg); - if (err != ESP_OK) { + if (err == ESP_ERR_WIFI_INIT_STATE) { + g_wifi_initialized = true; + } else if (err != ESP_OK) { + SetHotWifiFail("esp_wifi_init"); ESP_LOGE(kTag, "esp_wifi_init failed: %s", esp_err_to_name(err)); CleanupHotPathWifi(); return false; + } else { + g_wifi_initialized = true; } - g_wifi_initialized = true; err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiEventHandler, nullptr, @@ -402,6 +429,7 @@ bool EnsureWifiConnectedForHotPath() { // esp_wifi_internal_set_retry_counter(3, 3); if ((bits & kWifiConnectedBit) == 0) { + SetHotWifiFail("connect_timeout"); ESP_LOGE(kTag, "Wi-Fi hot path connect timeout/fail"); CleanupHotPathWifi(); return false; @@ -465,6 +493,13 @@ ae::DataBuffer MakeTemperaturePayload(std::string const& temperature) { bool HasPreparedSendBlock() { return g_prepared_send_message_block.is_valid(); } +std::uint32_t PreparedMessageLeft() { + if (!g_prepared_send_message_block.is_valid()) { + return 0; + } + return g_prepared_send_message_block.Resolve()->message_left; +} + void ClearPreparedSendBlock() { // magic indicate if block is valid // make it invalid @@ -477,9 +512,10 @@ bool ExportPreparedSendBlock(ae::Client::ptr const& client, ae::Uid destination, client, destination, reserve_message_count); if (!prep_res) { - std::cerr << "[prepared-send] PrepareSendMessage failed with ec: " - << prep_res.error().ec << " error: " << prep_res.error().msg - << "\n"; + ESP_LOGE(kTag, "PrepareSendMessage failed ec=%d msg=%.*s", + prep_res.error().ec, + static_cast(prep_res.error().msg.size()), + prep_res.error().msg.data()); return false; } @@ -487,38 +523,61 @@ bool ExportPreparedSendBlock(ae::Client::ptr const& client, ae::Uid destination, auto const resolved_block = g_prepared_send_message_block.Resolve(); - std::cout << "[prepared-send] exported prepared block reserved " - << resolved_block->message_left << " messages\n"; + ESP_LOGI(kTag, "exported prepared block reserved %lu messages", + static_cast(resolved_block->message_left)); return true; } -HotSendStatus TryHotWakePreparedSend( - [[maybe_unused]] std::string const& temperature) { -#if defined(ESP_PLATFORM) - // sleep(10); - esp_reset_reason_t reset = esp_reset_reason(); - esp_sleep_wakeup_cause_t wakeup = esp_sleep_get_wakeup_cause(); - - ESP_LOGI(kTag, "reset_reason=%d, wakeup_cause=%d", static_cast(reset), - static_cast(wakeup)); +ae::DataBuffer MakeBenchPayload(std::string_view kind, int sequence) { + auto text = ae::Format("{}:{}", kind, sequence); + return ae::DataBuffer{text.begin(), text.end()}; +} - if (reset != ESP_RST_DEEPSLEEP) { - address_is_valid = false; - bs_is_valid = false; +#if defined(ESP_PLATFORM) +void ReleaseFullAetherWifiForHotPath() { + auto err = esp_wifi_stop(); + if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_STARTED && + err != ESP_ERR_WIFI_NOT_INIT) { + ESP_LOGW(kTag, "esp_wifi_stop during release failed: %s", + esp_err_to_name(err)); + } + err = esp_wifi_deinit(); + if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_INIT) { + ESP_LOGW(kTag, "esp_wifi_deinit during release failed: %s", + esp_err_to_name(err)); + } + if (esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + netif != nullptr) { + esp_netif_destroy(netif); } + esp_netif_deinit(); + err = esp_event_loop_delete_default(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGW(kTag, "esp_event_loop_delete_default failed: %s", + esp_err_to_name(err)); + } + vTaskDelay(pdMS_TO_TICKS(50)); +} +#endif +HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload) { +#if defined(ESP_PLATFORM) if (!g_prepared_send_message_block.is_valid()) { return HotSendStatus::kNoPreparedBlock; } + if (g_prepared_send_message_block.Resolve()->message_left == 0) { + return HotSendStatus::kNonceExhausted; + } + if (!EnsureWifiConnectedForHotPath()) { + WifiLifecyclePrintf("PREPARED_WIFI_FAIL reason=%s\n", + LastHotWifiFailReason()); return HotSendStatus::kWifiFailed; } auto fail_after_wifi = ae_defer_at[] { CleanupHotPathWifi(); }; - auto payload = MakeTemperaturePayload(temperature); - ae::DataBuffer packet; auto encode_result = ae::prepared_packet::EncodePacket( g_prepared_send_message_block, payload, packet); @@ -529,15 +588,15 @@ HotSendStatus TryHotWakePreparedSend( } auto const resolved_block = g_prepared_send_message_block.Resolve(); - std::cout << "[prepared-send] reserved messages left " - << resolved_block->message_left << "\n"; + ESP_LOGI(kTag, "reserved messages left %lu", + static_cast(resolved_block->message_left)); auto endpoint = resolved_block->endpoint; sockaddr_storage dest_storage{}; socklen_t dest_len = 0; if (!FillUdpDestination(endpoint, reinterpret_cast(&dest_storage), &dest_len)) { - std::cerr << "[prepared-send] invalid endpoint address\n"; + ESP_LOGE(kTag, "invalid endpoint address"); return HotSendStatus::kSendFailed; } @@ -556,13 +615,44 @@ HotSendStatus TryHotWakePreparedSend( return HotSendStatus::kSendFailed; } - std::this_thread::sleep_for(std::chrono::milliseconds(450)); +# ifndef AETHER_PREPARED_POST_SEND_HOLD_MS +# define AETHER_PREPARED_POST_SEND_HOLD_MS 450 +# endif + std::this_thread::sleep_for( + std::chrono::milliseconds(AETHER_PREPARED_POST_SEND_HOLD_MS)); - std::cout << "[prepared-send] hot path UDP sent " << sent << " bytes\n"; + ESP_LOGI(kTag, "hot path UDP sent %d bytes", static_cast(sent)); return HotSendStatus::kSent; #else + (void)payload; return HotSendStatus::kUnsupported; #endif } +HotSendStatus TryHotWakePreparedSend( + [[maybe_unused]] std::string const& temperature) { +#if defined(ESP_PLATFORM) + esp_reset_reason_t reset = esp_reset_reason(); + esp_sleep_wakeup_cause_t wakeup = esp_sleep_get_wakeup_cause(); + + ESP_LOGI(kTag, "reset_reason=%d, wakeup_cause=%d", static_cast(reset), + static_cast(wakeup)); + + // Production deep-sleep semantics: only preserve local Wi-Fi RTC cache across + // deep-sleep wakes. Cold / other resets invalidate the local prepared cache. + if (reset != ESP_RST_DEEPSLEEP) { + address_is_valid = false; + bs_is_valid = false; + } + + return SendPreparedOnce(MakeTemperaturePayload(temperature)); +#else + return HotSendStatus::kUnsupported; +#endif +} + +#if defined(ESP_PLATFORM) +char const* LastHotWifiFailReason() { return g_last_hot_wifi_fail; } +#endif + } // namespace temp_sensor::prepared_send diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h index 0b9d656..197a3d2 100644 --- a/main/prepared_send/prepared_send.h +++ b/main/prepared_send/prepared_send.h @@ -7,7 +7,8 @@ * - full boot prepares/exports PreparedSendMessageBlock for the service stream; * - following ESP32 wakeups try to send one UDP prepared packet without * constructing full AetherApp; - * - any error falls back to normal full Aether boot. + * - any error falls back to normal full Aether boot; + * - no-sleep benchmarks call SendPreparedOnce() with the same encode/UDP path. */ #ifndef TEMP_SENSOR_PREPARED_SEND_H_ @@ -37,13 +38,22 @@ std::string_view ToString(HotSendStatus status); // Build the same binary temperature payload as SendValue(). ae::DataBuffer MakeTemperaturePayload(std::string const& temperature); -// Try the MCU hot path. -// Returns kSent only if: -// - retained prepared block exists; -// - Wi-Fi was connected; -// - prepared packet was encoded; -// - mutated block was persisted after nonce consumption; -// - UDP datagram was sent. +// UTF-8 benchmark payload: "FULL:0" / "PREPARED:N". +ae::DataBuffer MakeBenchPayload(std::string_view kind, int sequence); + +// Shared encode + Wi-Fi + UDP + post-send hold + Wi-Fi cleanup. +// Used by deep-sleep hot wake and no-sleep prepared-message bench. +HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload); + +#if defined(ESP_PLATFORM) +// No-sleep bench: AetherApp release may leave ESP-IDF Wi-Fi/netif up; hot +// path expects the same clean stack as after deep-sleep reboot. +void ReleaseFullAetherWifiForHotPath(); + +char const* LastHotWifiFailReason(); +#endif + +// Try the MCU hot path (deep-sleep gated). Production semantics unchanged. HotSendStatus TryHotWakePreparedSend(std::string const& temperature); // Export a new prepared block from the already initialized full Aether stream. @@ -51,6 +61,9 @@ HotSendStatus TryHotWakePreparedSend(std::string const& temperature); bool ExportPreparedSendBlock(ae::Client::ptr const& client, ae::Uid destination, std::size_t reserve_message_count); +bool HasPreparedSendBlock(); +std::uint32_t PreparedMessageLeft(); + struct WiFiBaseStation { uint8_t target_bssid[6]; uint8_t target_channel; diff --git a/sdkconfig.defaults.bench b/sdkconfig.defaults.bench new file mode 100644 index 0000000..6670f42 --- /dev/null +++ b/sdkconfig.defaults.bench @@ -0,0 +1,13 @@ +# Bench harness overrides: USB console for AE_EXP_BENCH_STAGE printf markers. +# Applied after sdkconfig.defaults when SDKCONFIG_DEFAULTS includes this file. + +# CONFIG_ESP_CONSOLE_NONE is not set +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y + +CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 + +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y +CONFIG_BOOTLOADER_LOG_LEVEL=3 diff --git a/temperature_receiver/CMakeLists.txt b/temperature_receiver/CMakeLists.txt new file mode 100644 index 0000000..038e8ca --- /dev/null +++ b/temperature_receiver/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright 2026 Aethernet Inc. +cmake_minimum_required(VERSION 3.16) +project(temperature_receiver LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(USER_CONFIG "${CMAKE_CURRENT_LIST_DIR}/user_config.h" CACHE PATH "" FORCE) +set(AE_DISTILLATION OFF CACHE BOOL "" FORCE) +set(AE_FILTRATION ON CACHE BOOL "" FORCE) + +include("${CMAKE_CURRENT_LIST_DIR}/../cmake/CPM.cmake") +# Pin exact SHA. Override locally with -DCPM_aether-client-cpp_SOURCE=. + CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#157aadbec8e7b852d0f89274307ff7cb8103e5f7") + +# MinGW + Parallel STL emits #pragma message noise that breaks -Werror builds. +if(TARGET aether AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(aether PRIVATE -Wno-error -Wno-cpp) +endif() + +add_executable(temperature_receiver main.cpp) +target_include_directories(temperature_receiver PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/../main") +target_compile_definitions(temperature_receiver PRIVATE + "USER_CONFIG=\"${CMAKE_CURRENT_LIST_DIR}/user_config.h\"") +target_link_libraries(temperature_receiver PRIVATE aether) + +# Keep aether state under ./state next to the binary cwd. +# FileSystemStdStorage always uses relative "state/" directory. diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp new file mode 100644 index 0000000..d898fc1 --- /dev/null +++ b/temperature_receiver/main.cpp @@ -0,0 +1,191 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Desktop Æther console receiver for prepared-message E2E bench. + * Expects UTF-8 payloads: FULL:0 and PREPARED:1..10 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +# include +#endif + +#include "aether/all.h" + +using namespace std::chrono_literals; + +namespace { + +static constexpr auto kParentUid = + ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); +static constexpr char const* kClientName = "prepared_message_bench_rx_v1"; +static constexpr int kExpectedPrepared = 10; + +std::mutex g_mu; +std::vector> g_streams; + +bool g_full_seen = false; +std::array g_prepared_hits{}; // 1..10 +int g_prepared_unique = 0; +int g_duplicates = 0; +std::vector g_order; + +std::int64_t NowMs() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + +void PrintSummary() { + int missing = 0; + std::string missing_list; + for (int i = 1; i <= kExpectedPrepared; ++i) { + if (g_prepared_hits[static_cast(i)] == 0) { + ++missing; + if (!missing_list.empty()) { + missing_list += ","; + } + missing_list += std::to_string(i); + } + } + std::cout << "RECEIVER\n"; + std::cout << " full=" << (g_full_seen ? 1 : 0) << "/1\n"; + std::cout << " prepared=" << g_prepared_unique << "/" << kExpectedPrepared + << "\n"; + std::cout << " missing=" << missing; + if (missing > 0) { + std::cout << " [" << missing_list << "]"; + } + std::cout << "\n"; + std::cout << " duplicates=" << g_duplicates << "\n"; + std::cout << " order="; + for (size_t i = 0; i < g_order.size(); ++i) { + if (i > 0) { + std::cout << ","; + } + std::cout << g_order[i]; + } + std::cout << "\n"; + std::cout.flush(); +} + +void OnMessage(ae::Uid sender, ae::DataBuffer const& data) { + auto text = std::string_view{reinterpret_cast(data.data()), + data.size()}; + auto const ts = NowMs(); + auto const sender_text = ae::Format("{}", sender); + + std::lock_guard lock{g_mu}; + if (text == "FULL:0") { + if (g_full_seen) { + ++g_duplicates; + } + g_full_seen = true; + g_order.emplace_back("FULL:0"); + std::cout << ae::Format( + "RECV sender={} sequence=0 type=FULL receive_ts_ms={}\n", sender_text, + ts); + } else if (text.rfind("PREPARED:", 0) == 0) { + auto const seq_sv = text.substr(std::string_view{"PREPARED:"}.size()); + int seq = 0; + try { + seq = std::stoi(std::string{seq_sv}); + } catch (...) { + seq = -1; + } + if (seq >= 1 && seq <= kExpectedPrepared) { + if (g_prepared_hits[static_cast(seq)] > 0) { + ++g_duplicates; + } else { + ++g_prepared_unique; + } + ++g_prepared_hits[static_cast(seq)]; + g_order.emplace_back(std::string{text}); + } + std::cout << ae::Format( + "RECV sender={} sequence={} type=PREPARED receive_ts_ms={}\n", + sender_text, seq, ts); + } else { + std::cout << ae::Format( + "RECV sender={} sequence=? type=UNKNOWN receive_ts_ms={} text={}\n", + sender_text, ts, text); + } + std::cout.flush(); + + if (g_full_seen && g_prepared_unique == kExpectedPrepared) { + PrintSummary(); + } +} + +std::filesystem::path ResolveSessionRoot() { +#if defined(_WIN32) + if (char const* env = std::getenv("AE_RECEIVER_SESSION_DIR")) { + return std::filesystem::path{env}; + } +#endif + return std::filesystem::current_path(); +} + +} // namespace + +int main() { + std::cout.setf(std::ios::unitbuf); +#if defined(_WIN32) + setvbuf(stdout, nullptr, _IONBF, 0); +#endif + auto const session_root = ResolveSessionRoot(); + std::filesystem::create_directories(session_root / "state"); + std::filesystem::current_path(session_root); + std::cerr << ae::Format("receiver_session_dir={}\n", session_root.string()); + std::cerr.flush(); + + auto aether_app = ae::AetherApp::Construct(ae::AetherAppContext{}); + ae::Client::ptr client; + aether_app->aether() + ->SelectClient(kParentUid, kClientName) + .result_event() + .Subscribe([&](ae::Result const& res) { + if (!res) { + std::cerr << "SelectClient failed\n"; + aether_app->Exit(1); + return; + } + client = res.value(); + std::cout << ae::Format("RECEIVER_UID={}\n", client->uid()); + std::cout.flush(); + client->connectivity_policy()->ResetRxTimings(); + client->connectivity_policy() + ->ConfigureRxTimings(ae::RequestPolicy::All{}) + .ForAllPriorities(ae::RxTimingConf::Every(1s).WithWindow(1s)); + client->message_stream_manager().new_port_event().Subscribe( + [&](ae::P2pPortHandle handle) { + auto sender = handle.destination(); + auto stream = std::make_unique( + *aether_app, client.Load(), sender, std::move(handle)); + stream->out_data_event().Subscribe( + [sender](auto const& d) { OnMessage(sender, d); }); + std::lock_guard lock{g_mu}; + g_streams.push_back(std::move(stream)); + }); + }); + + while (!aether_app->IsExited()) { + auto next = aether_app->Update(ae::Now()); + aether_app->WaitUntil(next); + } + { + std::lock_guard lock{g_mu}; + PrintSummary(); + } + return aether_app->ExitCode(); +} diff --git a/temperature_receiver/user_config.h b/temperature_receiver/user_config.h new file mode 100644 index 0000000..da553dd --- /dev/null +++ b/temperature_receiver/user_config.h @@ -0,0 +1,22 @@ +/* + * Copyright 2026 Aethernet Inc. + */ +#ifndef USER_CONFIG_H_ +#define USER_CONFIG_H_ + +#include "aether/config_consts.h" + +#define AE_CRYPTO_ASYNC AE_HYDRO_CRYPTO_PK +#define AE_CRYPTO_SYNC AE_HYDRO_CRYPTO_SK +#define AE_SIGNATURE AE_HYDRO_SIGNATURE +#define AE_KDF AE_HYDRO_KDF + +#define AE_TELE_ENABLED 1 +#define AE_TELE_LOG_CONSOLE 1 +#if defined NDEBUG +# define AE_TELE_DEBUG_MODULES 0 +#else +# define AE_TELE_DEBUG_MODULES AE_ALL +#endif + +#endif // USER_CONFIG_H_ diff --git a/ulp/CMakeLists.txt b/ulp/CMakeLists.txt index 35c759c..d3d6cbf 100644 --- a/ulp/CMakeLists.txt +++ b/ulp/CMakeLists.txt @@ -52,7 +52,7 @@ ulp_add_build_binary_targets(${ULP_APP_NAME}) target_include_directories(${ULP_APP_NAME} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/../main") include(../cmake/CPM.cmake) -CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#adopt/prepared-packet-v0") +CPMAddPackage(URI "https://github.com/aethernetio/aether-client-cpp.git#157aadbec8e7b852d0f89274307ff7cb8103e5f7") # not link but only setup include directory for aether/config_consts.h target_include_directories(${ULP_APP_NAME} PUBLIC "${aether-client-cpp_SOURCE_DIR}") From 669ccdc3f69146d4d1c63fa2ffcf8531226b93f0 Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Fri, 28 Aug 2026 15:17:51 -0700 Subject: [PATCH 20/32] Fix prepared Wi-Fi cache and add silent 5x20 E2E experiment. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split runtime Wi-Fi cleanup from cache invalidation, use BSSID/channel/static-IP fast connect, and measure 5 FULL + 100 prepared sends via binary Æther payloads with no UART. --- CMakeLists.txt | 9 +- .../PREPARED_WIFI_CACHE_5X20_REPORT.md | 97 ++++ experiments/prepared_wifi_cache_5x20.tsv | 38 ++ .../prepared_wifi_cache_receiver_uid.txt | 1 + main/CMakeLists.txt | 13 +- main/bench_payload.h | 73 +++ main/prepared_send/prepared_send.cpp | 333 ++++++------- main/prepared_send/prepared_send.h | 17 +- main/prepared_wifi_cache_5x20_bench.cpp | 444 ++++++++++++++++++ sdkconfig.defaults.silent | 13 + temperature_receiver/main.cpp | 293 +++++++++--- 11 files changed, 1062 insertions(+), 269 deletions(-) create mode 100644 experiments/PREPARED_WIFI_CACHE_5X20_REPORT.md create mode 100644 experiments/prepared_wifi_cache_5x20.tsv create mode 100644 experiments/prepared_wifi_cache_receiver_uid.txt create mode 100644 main/bench_payload.h create mode 100644 main/prepared_wifi_cache_5x20_bench.cpp create mode 100644 sdkconfig.defaults.silent diff --git a/CMakeLists.txt b/CMakeLists.txt index a6ab3ff..94694c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,10 +24,15 @@ if (ESP_PLATFORM OR IDF_TARGET) set(CM_PLATFORM "ESP32") endif() -# Bench harness sdkconfig (main task stack, USB console). Must be set before project(). +# Bench / silent measurement sdkconfig. Must be set before project(). set(AE_EXP_PREPARED_MESSAGE_E2E "" CACHE STRING "No-sleep prepared-message E2E bench (set to 1)") -if(AE_EXP_PREPARED_MESSAGE_E2E STREQUAL "1") +set(AE_EXP_PREPARED_WIFI_CACHE_5X20 "" CACHE STRING + "Silent 5x20 prepared Wi-Fi cache bench (set to 1)") +if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1") + list(APPEND SDKCONFIG_DEFAULTS + "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent") +elseif(AE_EXP_PREPARED_MESSAGE_E2E STREQUAL "1") list(APPEND SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.bench") endif() diff --git a/experiments/PREPARED_WIFI_CACHE_5X20_REPORT.md b/experiments/PREPARED_WIFI_CACHE_5X20_REPORT.md new file mode 100644 index 0000000..b4f3970 --- /dev/null +++ b/experiments/PREPARED_WIFI_CACHE_5X20_REPORT.md @@ -0,0 +1,97 @@ +# Prepared Wi-Fi cache 5×20 (ESP32-C6, no sleep, silent) + +Hardware: ESP32-C6, ESP-IDF v6.0.2, COM7 +Aether: `exp/esp32c6-wifi-lifecycle-diag` @ `157aadbec8e7b852d0f89274307ff7cb8103e5f7` (unchanged) +Firmware: `AE_EXP_PREPARED_WIFI_CACHE_5X20=1`, `AE_EXP_SILENT=1`, console/log NONE +Client: `prepared_wifi_cache_5x20_v1` +Receiver UID: `5aade50f-00d9-4624-b097-e203cdcf1e38` +Post-send hold: **300 ms included in every prepared timing** + +## REGISTRATION + +``` +time_us=13962906 +``` + +(~14.0 s; first Construct + network registration + Save + full release) + +## FULL + +``` +raw=[5637828, 4124003, 4014012, 4104022, 4184009] +min=4014012 +median=4124003 +max=5637828 +n=5 +``` + +(~4.0–5.6 s; Construct → Select → FULL write → PrepareSendMessageBlock(20) → Save → release) + +## FIRST PREPARED + +Only **1/5** first-of-cycle timings were recovered (timing of prepared #1 is carried in prepared #2; most #2 UDP deliveries were lost). + +``` +raw=[15100453] +median=15100453 +n=1 +``` + +(~15.1 s) — cold Wi-Fi after Aether release, before local cache population. + +## WARM PREPARED + +``` +n=46 +min=610471 +median=640476 +p90=690482 +p99=730476 +max=730476 +``` + +(~0.61–0.73 s including 300 ms hold ⇒ ~0.31–0.43 s net) + +## ALL PREPARED (recovered timings) + +``` +n=47 +min=610471 +median=640476 +p90=690531 +p99=15100453 +max=15100453 +``` + +## DELIVERY + +``` +full=5/5 +prepared=44/100 +final=1/1 +missing=56 +duplicates=0 +out_of_order=0 +``` + +Fire-and-forget prepared UDP path; losses not retried. Application sequence gaps match missing prepared deliveries. + +## CACHE + +``` +BSSID reuse confirmed=yes (hits=46) +channel reuse=yes (same flag path) +static IP reuse confirmed=yes (hits=46) +DHCP skipped confirmed=yes (hits=46) +fallbacks=0 +``` + +`cache_flags=7` = `UsedBssid | UsedStaticIp | DhcpSkipped`. + +## Notes + +- `CleanupHotPathWifiRuntime()` no longer clears `address_is_valid` / `bs_is_valid`. +- Fast path waits on `WIFI_EVENT_STA_CONNECTED` when static IP is cached; GOT_IP only on cold/DHCP path. +- Cached BSSID failure invalidates cache and falls back once (not observed this run). +- Silent build: no UART results; all timings via binary Æther payloads. +- Warm prepared ~640 ms vs prior no-cache ~2250 ms — local prepared Wi-Fi cache is effective. diff --git a/experiments/prepared_wifi_cache_5x20.tsv b/experiments/prepared_wifi_cache_5x20.tsv new file mode 100644 index 0000000..79a2b8d --- /dev/null +++ b/experiments/prepared_wifi_cache_5x20.tsv @@ -0,0 +1,38 @@ +metric value +registration_us 13962906 +full_us_1 5637828 +full_us_2 4124003 +full_us_3 4014012 +full_us_4 4104022 +full_us_5 4184009 +full_min_us 4014012 +full_median_us 4124003 +full_max_us 5637828 +first_prepared_us 15100453 +warm_prepared_n 46 +warm_prepared_min_us 610471 +warm_prepared_median_us 640476 +warm_prepared_p90_us 690482 +warm_prepared_p99_us 730476 +warm_prepared_max_us 730476 +all_prepared_n 47 +all_prepared_min_us 610471 +all_prepared_median_us 640476 +all_prepared_p90_us 690531 +all_prepared_p99_us 15100453 +all_prepared_max_us 15100453 +delivery_full 5/5 +delivery_prepared 44/100 +delivery_final 1/1 +delivery_missing 56 +delivery_duplicates 0 +delivery_out_of_order 0 +cache_bssid_hits 46 +cache_static_ip_hits 46 +cache_dhcp_skip_hits 46 +cache_fallbacks 0 +post_send_hold_ms 300 +receiver_uid 5aade50f-00d9-4624-b097-e203cdcf1e38 +bench_client_id prepared_wifi_cache_5x20_v1 +aether_sha 157aadbec8e7b852d0f89274307ff7cb8103e5f7 +warm_prepared_raw_us 640479 630478 630470 650471 650469 650471 640475 640476 640471 620471 660484 630474 640472 640470 630470 640477 670485 730467 630481 650412 680482 610471 650477 630471 630478 680473 620470 620475 650478 630477 690531 630479 650513 640481 640414 630477 690482 630481 730472 630485 640474 630472 670477 730476 680475 660470 diff --git a/experiments/prepared_wifi_cache_receiver_uid.txt b/experiments/prepared_wifi_cache_receiver_uid.txt new file mode 100644 index 0000000..df6bc95 --- /dev/null +++ b/experiments/prepared_wifi_cache_receiver_uid.txt @@ -0,0 +1 @@ +5aade50f-00d9-4624-b097-e203cdcf1e38 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f3044ad..8b0a222 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -18,7 +18,12 @@ list(APPEND src_list "main.cpp" ) -if(AE_EXP_PREPARED_MESSAGE_E2E) +if(AE_EXP_PREPARED_WIFI_CACHE_5X20) + list(APPEND src_list + "prepared_wifi_cache_5x20_bench.cpp" + "prepared_send/prepared_send.cpp" + ) +elseif(AE_EXP_PREPARED_MESSAGE_E2E) list(APPEND src_list "prepared_message_e2e_bench.cpp" "prepared_send/prepared_send.cpp" @@ -148,6 +153,7 @@ set(AE_EXP_WIFI_LIFECYCLE_VARIANT "" CACHE STRING "Wi-Fi lifecycle variant id (0 set(AE_EXP_WIFI_LIFECYCLE_CYCLES "" CACHE STRING "Wi-Fi lifecycle cycles (default 10)") set(AE_EXP_WIFI_COOLDOWN_MS "" CACHE STRING "Cooldown between cycles, outside timer (ms)") set(AE_EXP_PREPARED_MESSAGE_E2E "" CACHE STRING "No-sleep prepared-message E2E bench (set to 1)") +set(AE_EXP_PREPARED_WIFI_CACHE_5X20 "" CACHE STRING "Silent 5x20 prepared Wi-Fi cache bench (set to 1)") set(BENCH_CLIENT_ID "" CACHE STRING "Bench SelectClient id (default prepared_message_bench_v1)") set(AE_EXP_WIFI_CANONICAL "" CACHE STRING "Canonical ESP-IDF Wi-Fi driver (set to 1)") set(AE_EXP_WIFI_FEAT_AMPDU_OFF "" CACHE STRING "Canonical+bisect: AMPDU off") @@ -184,6 +190,11 @@ ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE_VARIANT) ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE_CYCLES) ae_exp_define_if_set(AE_EXP_WIFI_COOLDOWN_MS) ae_exp_define_if_set(AE_EXP_PREPARED_MESSAGE_E2E) +ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_CACHE_5X20) +if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1") + target_compile_definitions(aether PUBLIC "AE_EXP_SILENT=1") + target_compile_definitions(${TARGET_NAME} PRIVATE "AE_EXP_SILENT=1") +endif() if(NOT "${BENCH_CLIENT_ID}" STREQUAL "") target_compile_definitions(${TARGET_NAME} PRIVATE "BENCH_CLIENT_ID=\"${BENCH_CLIENT_ID}\"") diff --git a/main/bench_payload.h b/main/bench_payload.h new file mode 100644 index 0000000..f30c71a --- /dev/null +++ b/main/bench_payload.h @@ -0,0 +1,73 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Compact binary benchmark payload for prepared Wi-Fi cache 5x20 experiment. + * All multi-byte fields are little-endian. + */ + +#ifndef TEMP_SENSOR_BENCH_PAYLOAD_H_ +#define TEMP_SENSOR_BENCH_PAYLOAD_H_ + +#include +#include +#include + +namespace temp_sensor::bench { + +static constexpr std::uint8_t kMagic = 0xAE; + +enum class MsgType : std::uint8_t { + kFull = 1, + kPrepared = 2, + kFinal = 3, +}; + +enum class CacheFlags : std::uint8_t { + kNone = 0, + kUsedBssid = 1 << 0, + kUsedStaticIp = 1 << 1, + kDhcpSkipped = 1 << 2, + kFallback = 1 << 3, +}; + +#pragma pack(push, 1) +struct Payload { + std::uint8_t magic{kMagic}; + std::uint8_t type{0}; + std::uint8_t outer_cycle{0}; + std::uint8_t prepared_index{0}; + std::uint16_t sequence_global{0}; + std::uint32_t registration_us{0}; + std::uint32_t previous_full_us{0}; + std::uint32_t previous_prepared_us{0}; + std::uint8_t cache_flags{0}; +}; +#pragma pack(pop) + +static_assert(sizeof(Payload) == 19, "bench payload size"); + +inline std::vector EncodeVec(Payload const& p) { + std::vector out(sizeof(Payload)); + std::memcpy(out.data(), &p, sizeof(Payload)); + return out; +} + +template +inline Buffer Encode(Payload const& p) { + Buffer out(sizeof(Payload)); + std::memcpy(out.data(), &p, sizeof(Payload)); + return out; +} + +template +inline bool Decode(Buffer const& data, Payload& out) { + if (data.size() < sizeof(Payload)) { + return false; + } + std::memcpy(&out, data.data(), sizeof(Payload)); + return out.magic == kMagic; +} + +} // namespace temp_sensor::bench + +#endif // TEMP_SENSOR_BENCH_PAYLOAD_H_ diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index 97eb935..ddd63a6 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -23,6 +23,7 @@ #include "aether/all.h" #include "aether/prepared_packet/packet_encoder.h" #include "aether/prepared_packet/prepared_send_message.h" +#include "bench_payload.h" #if !defined(ESP_PLATFORM) # include @@ -32,7 +33,6 @@ # include # include # include -# include # include # include # include @@ -43,21 +43,34 @@ # include # include # include -# include "wifi_lifecycle_out.h" +# if !defined(AE_EXP_SILENT) +# include +# endif #endif namespace temp_sensor::prepared_send { -#if defined(ESP_PLATFORM) -static char g_last_hot_wifi_fail[64] = {}; -static void SetHotWifiFail(char const* msg) { - std::strncpy(g_last_hot_wifi_fail, msg, sizeof(g_last_hot_wifi_fail) - 1); - g_last_hot_wifi_fail[sizeof(g_last_hot_wifi_fail) - 1] = '\0'; -} +#if defined(ESP_PLATFORM) && !defined(AE_EXP_SILENT) +static constexpr char const* kTag = "prepared-send"; +# define PS_LOGI(...) ESP_LOGI(kTag, __VA_ARGS__) +# define PS_LOGW(...) ESP_LOGW(kTag, __VA_ARGS__) +# define PS_LOGE(...) ESP_LOGE(kTag, __VA_ARGS__) +# define PS_LOGD(...) ESP_LOGD(kTag, __VA_ARGS__) +#else +# define PS_LOGI(...) \ + do { \ + } while (0) +# define PS_LOGW(...) \ + do { \ + } while (0) +# define PS_LOGE(...) \ + do { \ + } while (0) +# define PS_LOGD(...) \ + do { \ + } while (0) #endif -namespace { - #ifndef AETHER_PREPARED_NONCE_RESERVE # define AETHER_PREPARED_NONCE_RESERVE 30 #endif @@ -70,11 +83,9 @@ namespace { # define AETHER_PREPARED_HOT_WIFI_MAX_RETRY 10 #endif -#if defined(ESP_PLATFORM) -static constexpr char const* kTag = "prepared-send"; +static std::uint8_t g_last_send_cache_flags = 0; -// RTC_NOINIT_ATTR: do not zero on wake from deep sleep. -// It may contain garbage on first boot, so magic validate it. +#if defined(ESP_PLATFORM) static RTC_NOINIT_ATTR ae::prepared_packet::PreparedSendMessageBlock g_prepared_send_message_block; @@ -83,13 +94,18 @@ static RTC_DATA_ATTR WiFiBaseStation base_station{}; static RTC_NOINIT_ATTR bool address_is_valid; static RTC_NOINIT_ATTR bool bs_is_valid; -// ESP32-C6 exposes 8 KiB RTC slow memory; the linker asserts the segment fits. static_assert(sizeof(ae::prepared_packet::PreparedSendMessageBlock) <= 8 * 1024, "PreparedSendMessageBlock must fit in ESP32 RTC slow memory"); #else -ae::prepared_packet::PreparedSendMessageBlock g_prepared_send_message_block; +static ae::prepared_packet::PreparedSendMessageBlock g_prepared_send_message_block; #endif +#if defined(ESP_PLATFORM) +void InvalidatePreparedWifiCache(); // defined below +#endif + +namespace { + #if defined(ESP_PLATFORM) bool FillUdpDestination(ae::prepared_packet::PreparedEndpoint const& endpoint, @@ -127,9 +143,23 @@ static bool g_wifi_initialized = false; static bool g_wifi_started = false; static bool g_default_event_loop_created = false; static int g_wifi_retry_count = 0; -static constexpr EventBits_t kWifiConnectedBit = BIT0; +static bool g_wait_got_ip = true; +static bool g_using_bssid_cache = false; +static int g_max_wifi_retry = AETHER_PREPARED_HOT_WIFI_MAX_RETRY; +static constexpr EventBits_t kWifiReadyBit = BIT0; static constexpr EventBits_t kWifiFailBit = BIT1; +void CaptureApIntoCache() { + wifi_ap_record_t ap_info{}; + if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK) { + return; + } + base_station.target_channel = ap_info.primary; + std::memcpy(base_station.target_bssid, ap_info.bssid, + sizeof(base_station.target_bssid)); + bs_is_valid = true; +} + void WifiEventHandler(void*, esp_event_base_t event_base, std::int32_t event_id, void* event_data) { if (g_wifi_event_group == nullptr) { @@ -140,96 +170,68 @@ void WifiEventHandler(void*, esp_event_base_t event_base, std::int32_t event_id, g_wifi_retry_count = 0; auto err = esp_wifi_connect(); if (err != ESP_OK) { - ESP_LOGE(kTag, "Wi-Fi hot path connect start failed: %s", - esp_err_to_name(err)); + PS_LOGE("Wi-Fi hot path connect start failed: %s", esp_err_to_name(err)); xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); } + } else if (event_base == WIFI_EVENT && + event_id == WIFI_EVENT_STA_CONNECTED) { + CaptureApIntoCache(); + if (!g_wait_got_ip) { + xEventGroupSetBits(g_wifi_event_group, kWifiReadyBit); + } } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { auto const* event = static_cast(event_data); auto const reason = event != nullptr ? static_cast(event->reason) : -1; - if (g_wifi_retry_count < AETHER_PREPARED_HOT_WIFI_MAX_RETRY) { + if (g_wifi_retry_count < g_max_wifi_retry) { ++g_wifi_retry_count; - ESP_LOGW(kTag, "Wi-Fi hot path disconnected reason=%d; retry %d/%d", - reason, g_wifi_retry_count, - static_cast(AETHER_PREPARED_HOT_WIFI_MAX_RETRY)); - + PS_LOGW("Wi-Fi hot path disconnected reason=%d; retry %d/%d", reason, + g_wifi_retry_count, g_max_wifi_retry); auto err = esp_wifi_connect(); if (err != ESP_OK) { - ESP_LOGE(kTag, "Wi-Fi hot path reconnect failed: %s", - esp_err_to_name(err)); + PS_LOGE("Wi-Fi hot path reconnect failed: %s", esp_err_to_name(err)); xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); } } else { - ESP_LOGE(kTag, "Wi-Fi hot path retry limit reached; reason=%d", reason); + PS_LOGE("Wi-Fi hot path retry limit reached; reason=%d", reason); xEventGroupSetBits(g_wifi_event_group, kWifiFailBit); } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { - ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data; - if (!address_is_valid) { - rtc_ip_info.ip = event->ip_info.ip; - rtc_ip_info.netmask = event->ip_info.netmask; - rtc_ip_info.gw = event->ip_info.gw; - - wifi_ap_record_t ap_info; - if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { - base_station.target_channel = ap_info.primary; - memcpy(base_station.target_bssid, ap_info.bssid, - sizeof(base_station.target_bssid)); - ESP_LOGD(kTag, "Storing to cache BSSID:" MACSTR " CHN:%u", - MAC2STR(base_station.target_bssid), - static_cast(base_station.target_channel)); - bs_is_valid = true; - } - address_is_valid = true; - } - ESP_LOGI(kTag, "Wi-Fi hot path connected after %d retries", - g_wifi_retry_count); - xEventGroupSetBits(g_wifi_event_group, kWifiConnectedBit); + auto* event = static_cast(event_data); + rtc_ip_info.ip = event->ip_info.ip; + rtc_ip_info.netmask = event->ip_info.netmask; + rtc_ip_info.gw = event->ip_info.gw; + address_is_valid = true; + CaptureApIntoCache(); + PS_LOGI("Wi-Fi hot path GOT_IP after %d retries", g_wifi_retry_count); + xEventGroupSetBits(g_wifi_event_group, kWifiReadyBit); } } -void CleanupHotPathWifi() { - address_is_valid = false; - bs_is_valid = false; +void CleanupHotPathWifiRuntime() { if (g_wifi_any_id_handler != nullptr) { - auto err = esp_event_handler_instance_unregister( - WIFI_EVENT, ESP_EVENT_ANY_ID, g_wifi_any_id_handler); - if (err != ESP_OK) { - ESP_LOGW(kTag, "failed to unregister WIFI handler: %s", - esp_err_to_name(err)); - } + esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, + g_wifi_any_id_handler); g_wifi_any_id_handler = nullptr; } if (g_wifi_got_ip_handler != nullptr) { - auto err = esp_event_handler_instance_unregister( - IP_EVENT, IP_EVENT_STA_GOT_IP, g_wifi_got_ip_handler); - if (err != ESP_OK) { - ESP_LOGW(kTag, "failed to unregister IP handler: %s", - esp_err_to_name(err)); - } + esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, + g_wifi_got_ip_handler); g_wifi_got_ip_handler = nullptr; } if (g_wifi_started) { auto err = esp_wifi_stop(); - if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_STARTED && - err != ESP_ERR_WIFI_NOT_INIT) { - ESP_LOGW(kTag, "esp_wifi_stop failed during cleanup: %s", - esp_err_to_name(err)); - } + (void)err; g_wifi_started = false; } if (g_wifi_initialized) { auto err = esp_wifi_deinit(); - if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_INIT) { - ESP_LOGW(kTag, "esp_wifi_deinit failed during cleanup: %s", - esp_err_to_name(err)); - } + (void)err; g_wifi_initialized = false; } @@ -246,32 +248,28 @@ void CleanupHotPathWifi() { g_wifi_retry_count = 0; if (g_default_event_loop_created) { - auto err = esp_event_loop_delete_default(); - if (err != ESP_OK) { - ESP_LOGW(kTag, "esp_event_loop_delete_default failed: %s", - esp_err_to_name(err)); - } + esp_event_loop_delete_default(); g_default_event_loop_created = false; } } -bool EnsureWifiConnectedForHotPath() { +bool StartWifiAttempt(bool use_bssid_cache, bool use_static_ip) { # ifndef WIFI_SSID - SetHotWifiFail("WIFI_SSID undefined"); - ESP_LOGE(kTag, "WIFI_SSID is not defined"); return false; # endif # ifndef WIFI_PASSWORD - SetHotWifiFail("WIFI_PASSWORD undefined"); - ESP_LOGE(kTag, "WIFI_PASSWORD is not defined"); return false; # endif - SetHotWifiFail(""); + CleanupHotPathWifiRuntime(); + + g_wait_got_ip = !use_static_ip; + g_using_bssid_cache = use_bssid_cache; + g_max_wifi_retry = use_bssid_cache ? 1 : AETHER_PREPARED_HOT_WIFI_MAX_RETRY; + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_config_t wifi_config{}; - // It is OK if these were already initialized by a previous attempt. auto err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { @@ -279,33 +277,20 @@ bool EnsureWifiConnectedForHotPath() { err = nvs_flash_init(); } if (err != ESP_OK && err != ESP_ERR_NVS_NO_FREE_PAGES) { - SetHotWifiFail("nvs_flash_init"); - ESP_LOGE(kTag, "nvs_flash_init failed: %s", esp_err_to_name(err)); return false; } - CleanupHotPathWifi(); - - if (esp_netif_init() != ESP_OK) { - ESP_LOGW(kTag, "esp_netif_init returned non-OK; continuing"); - } + (void)esp_netif_init(); err = esp_event_loop_create_default(); if (err == ESP_OK) { g_default_event_loop_created = true; - } else if (err == ESP_ERR_INVALID_STATE) { - ESP_LOGW(kTag, "event loop already exists; continuing"); - } else { - SetHotWifiFail("event_loop_create"); - ESP_LOGE(kTag, "esp_event_loop_create_default failed: %s", - esp_err_to_name(err)); + } else if (err != ESP_ERR_INVALID_STATE) { return false; } g_wifi_event_group = xEventGroupCreate(); if (g_wifi_event_group == nullptr) { - SetHotWifiFail("event_group_create"); - ESP_LOGE(kTag, "failed to create Wi-Fi event group"); - CleanupHotPathWifi(); + CleanupHotPathWifiRuntime(); return false; } @@ -314,25 +299,19 @@ bool EnsureWifiConnectedForHotPath() { g_wifi_netif = esp_netif_create_default_wifi_sta(); } if (g_wifi_netif == nullptr) { - SetHotWifiFail("create_default_wifi_sta"); - ESP_LOGE(kTag, "failed to create default Wi-Fi STA netif"); - CleanupHotPathWifi(); + CleanupHotPathWifiRuntime(); return false; } - if (address_is_valid) { - ESP_LOGI(kTag, "Restoring netif config"); + if (use_static_ip && address_is_valid) { esp_netif_dhcpc_stop(g_wifi_netif); esp_netif_ip_info_t ip_info = { .ip = {.addr = rtc_ip_info.ip.addr}, .netmask = {.addr = rtc_ip_info.netmask.addr}, .gw = {.addr = rtc_ip_info.gw.addr}}; esp_netif_set_ip_info(g_wifi_netif, &ip_info); - } else { - ESP_LOGI(kTag, "No cached netif config"); } - // We disable aggregation so that the packages go out one by one and quickly cfg.ampdu_rx_enable = 0; cfg.ampdu_tx_enable = 0; @@ -340,9 +319,7 @@ bool EnsureWifiConnectedForHotPath() { if (err == ESP_ERR_WIFI_INIT_STATE) { g_wifi_initialized = true; } else if (err != ESP_OK) { - SetHotWifiFail("esp_wifi_init"); - ESP_LOGE(kTag, "esp_wifi_init failed: %s", esp_err_to_name(err)); - CleanupHotPathWifi(); + CleanupHotPathWifiRuntime(); return false; } else { g_wifi_initialized = true; @@ -352,8 +329,7 @@ bool EnsureWifiConnectedForHotPath() { &WifiEventHandler, nullptr, &g_wifi_any_id_handler); if (err != ESP_OK) { - ESP_LOGE(kTag, "failed to register WIFI handler: %s", esp_err_to_name(err)); - CleanupHotPathWifi(); + CleanupHotPathWifiRuntime(); return false; } @@ -361,8 +337,7 @@ bool EnsureWifiConnectedForHotPath() { &WifiEventHandler, nullptr, &g_wifi_got_ip_handler); if (err != ESP_OK) { - ESP_LOGE(kTag, "failed to register IP handler: %s", esp_err_to_name(err)); - CleanupHotPathWifi(); + CleanupHotPathWifiRuntime(); return false; } @@ -371,10 +346,7 @@ bool EnsureWifiConnectedForHotPath() { std::strncpy(reinterpret_cast(wifi_config.sta.password), WIFI_PASSWORD, sizeof(wifi_config.sta.password)); - if (bs_is_valid) { - ESP_LOGD(kTag, "Restoring cached BSSID:" MACSTR " CHN:%u", - MAC2STR(base_station.target_bssid), - static_cast(base_station.target_channel)); + if (use_bssid_cache && bs_is_valid) { wifi_config.sta.scan_method = WIFI_FAST_SCAN; wifi_config.sta.bssid_set = true; wifi_config.sta.channel = base_station.target_channel; @@ -384,15 +356,13 @@ bool EnsureWifiConnectedForHotPath() { err = esp_wifi_set_mode(WIFI_MODE_STA); if (err != ESP_OK) { - ESP_LOGE(kTag, "esp_wifi_set_mode failed: %s", esp_err_to_name(err)); - CleanupHotPathWifi(); + CleanupHotPathWifiRuntime(); return false; } err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); if (err != ESP_OK) { - ESP_LOGE(kTag, "esp_wifi_set_config failed: %s", esp_err_to_name(err)); - CleanupHotPathWifi(); + CleanupHotPathWifiRuntime(); return false; } @@ -401,40 +371,54 @@ bool EnsureWifiConnectedForHotPath() { err = esp_wifi_start(); if (err != ESP_OK) { - ESP_LOGE(kTag, "esp_wifi_start failed: %s", esp_err_to_name(err)); - CleanupHotPathWifi(); + CleanupHotPathWifiRuntime(); return false; } g_wifi_started = true; - err = esp_wifi_set_max_tx_power(80); - if (err != ESP_OK) { - ESP_LOGE(kTag, "esp_wifi_set_max_tx_power failed: %s", - esp_err_to_name(err)); - CleanupHotPathWifi(); - return false; - } - err = esp_wifi_set_ps(WIFI_PS_MAX_MODEM); - if (err != ESP_OK) { - ESP_LOGE(kTag, "esp_wifi_set_ps failed: %s", esp_err_to_name(err)); - CleanupHotPathWifi(); - return false; - } + (void)esp_wifi_set_max_tx_power(80); + (void)esp_wifi_set_ps(WIFI_PS_MAX_MODEM); EventBits_t bits = xEventGroupWaitBits( - g_wifi_event_group, kWifiConnectedBit | kWifiFailBit, pdFALSE, pdFALSE, + g_wifi_event_group, kWifiReadyBit | kWifiFailBit, pdFALSE, pdFALSE, pdMS_TO_TICKS(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS)); esp_wifi_internal_set_fix_rate(WIFI_IF_STA, true, (wifi_phy_rate_t)0x0); - // esp_wifi_internal_set_retry_counter(3, 3); - if ((bits & kWifiConnectedBit) == 0) { - SetHotWifiFail("connect_timeout"); - ESP_LOGE(kTag, "Wi-Fi hot path connect timeout/fail"); - CleanupHotPathWifi(); - return false; + return (bits & kWifiReadyBit) != 0; +} + +bool EnsureWifiConnectedForHotPath() { + g_last_send_cache_flags = 0; + + bool const have_bssid = bs_is_valid; + bool const have_ip = address_is_valid; + + if (have_bssid || have_ip) { + if (StartWifiAttempt(have_bssid, have_ip)) { + if (have_bssid) { + g_last_send_cache_flags |= + static_cast(bench::CacheFlags::kUsedBssid); + } + if (have_ip) { + g_last_send_cache_flags |= + static_cast(bench::CacheFlags::kUsedStaticIp) | + static_cast(bench::CacheFlags::kDhcpSkipped); + } + return true; + } + + // Cached BSSID/channel (and/or static IP) failed — invalidate and fall back. + CleanupHotPathWifiRuntime(); + InvalidatePreparedWifiCache(); + g_last_send_cache_flags = + static_cast(bench::CacheFlags::kFallback); } + if (!StartWifiAttempt(/*use_bssid_cache=*/false, /*use_static_ip=*/false)) { + CleanupHotPathWifiRuntime(); + return false; + } return true; } @@ -442,7 +426,7 @@ bool EnsureWifiConnectedForHotPath() { bool EnsureWifiConnectedForHotPath() { return true; } -void CleanupHotPathWifi() {} +void CleanupHotPathWifiRuntime() {} #endif @@ -470,6 +454,8 @@ std::string_view ToString(HotSendStatus status) { return "unknown"; } +std::uint8_t LastSendCacheFlags() { return g_last_send_cache_flags; } + struct Header { AE_REFLECT_MEMBERS(root_code, size, dev_code) std::uint8_t const root_code = 0x3; @@ -501,8 +487,6 @@ std::uint32_t PreparedMessageLeft() { } void ClearPreparedSendBlock() { - // magic indicate if block is valid - // make it invalid g_prepared_send_message_block.raw.magic = {}; } @@ -512,19 +496,11 @@ bool ExportPreparedSendBlock(ae::Client::ptr const& client, ae::Uid destination, client, destination, reserve_message_count); if (!prep_res) { - ESP_LOGE(kTag, "PrepareSendMessage failed ec=%d msg=%.*s", - prep_res.error().ec, - static_cast(prep_res.error().msg.size()), - prep_res.error().msg.data()); + PS_LOGE("PrepareSendMessage failed ec=%d", prep_res.error().ec); return false; } g_prepared_send_message_block = std::move(prep_res).value(); - - auto const resolved_block = g_prepared_send_message_block.Resolve(); - - ESP_LOGI(kTag, "exported prepared block reserved %lu messages", - static_cast(resolved_block->message_left)); return true; } @@ -534,28 +510,25 @@ ae::DataBuffer MakeBenchPayload(std::string_view kind, int sequence) { } #if defined(ESP_PLATFORM) +void InvalidatePreparedWifiCache() { + address_is_valid = false; + bs_is_valid = false; + std::memset(&rtc_ip_info, 0, sizeof(rtc_ip_info)); + std::memset(&base_station, 0, sizeof(base_station)); +} + void ReleaseFullAetherWifiForHotPath() { auto err = esp_wifi_stop(); - if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_STARTED && - err != ESP_ERR_WIFI_NOT_INIT) { - ESP_LOGW(kTag, "esp_wifi_stop during release failed: %s", - esp_err_to_name(err)); - } + (void)err; err = esp_wifi_deinit(); - if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_INIT) { - ESP_LOGW(kTag, "esp_wifi_deinit during release failed: %s", - esp_err_to_name(err)); - } + (void)err; if (esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); netif != nullptr) { esp_netif_destroy(netif); } esp_netif_deinit(); err = esp_event_loop_delete_default(); - if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { - ESP_LOGW(kTag, "esp_event_loop_delete_default failed: %s", - esp_err_to_name(err)); - } + (void)err; vTaskDelay(pdMS_TO_TICKS(50)); } #endif @@ -570,13 +543,12 @@ HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload) { return HotSendStatus::kNonceExhausted; } + // Connect BEFORE EncodePacket so a failed Wi-Fi attempt does not burn a nonce. if (!EnsureWifiConnectedForHotPath()) { - WifiLifecyclePrintf("PREPARED_WIFI_FAIL reason=%s\n", - LastHotWifiFailReason()); return HotSendStatus::kWifiFailed; } - auto fail_after_wifi = ae_defer_at[] { CleanupHotPathWifi(); }; + auto fail_after_wifi = ae_defer_at[] { CleanupHotPathWifiRuntime(); }; ae::DataBuffer packet; auto encode_result = ae::prepared_packet::EncodePacket( @@ -588,15 +560,12 @@ HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload) { } auto const resolved_block = g_prepared_send_message_block.Resolve(); - ESP_LOGI(kTag, "reserved messages left %lu", - static_cast(resolved_block->message_left)); auto endpoint = resolved_block->endpoint; sockaddr_storage dest_storage{}; socklen_t dest_len = 0; if (!FillUdpDestination(endpoint, reinterpret_cast(&dest_storage), &dest_len)) { - ESP_LOGE(kTag, "invalid endpoint address"); return HotSendStatus::kSendFailed; } @@ -621,7 +590,6 @@ HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload) { std::this_thread::sleep_for( std::chrono::milliseconds(AETHER_PREPARED_POST_SEND_HOLD_MS)); - ESP_LOGI(kTag, "hot path UDP sent %d bytes", static_cast(sent)); return HotSendStatus::kSent; #else (void)payload; @@ -633,16 +601,11 @@ HotSendStatus TryHotWakePreparedSend( [[maybe_unused]] std::string const& temperature) { #if defined(ESP_PLATFORM) esp_reset_reason_t reset = esp_reset_reason(); - esp_sleep_wakeup_cause_t wakeup = esp_sleep_get_wakeup_cause(); - - ESP_LOGI(kTag, "reset_reason=%d, wakeup_cause=%d", static_cast(reset), - static_cast(wakeup)); // Production deep-sleep semantics: only preserve local Wi-Fi RTC cache across // deep-sleep wakes. Cold / other resets invalidate the local prepared cache. if (reset != ESP_RST_DEEPSLEEP) { - address_is_valid = false; - bs_is_valid = false; + InvalidatePreparedWifiCache(); } return SendPreparedOnce(MakeTemperaturePayload(temperature)); @@ -651,8 +614,4 @@ HotSendStatus TryHotWakePreparedSend( #endif } -#if defined(ESP_PLATFORM) -char const* LastHotWifiFailReason() { return g_last_hot_wifi_fail; } -#endif - } // namespace temp_sensor::prepared_send diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h index 197a3d2..4ef0af4 100644 --- a/main/prepared_send/prepared_send.h +++ b/main/prepared_send/prepared_send.h @@ -38,26 +38,25 @@ std::string_view ToString(HotSendStatus status); // Build the same binary temperature payload as SendValue(). ae::DataBuffer MakeTemperaturePayload(std::string const& temperature); -// UTF-8 benchmark payload: "FULL:0" / "PREPARED:N". +// UTF-8 benchmark payload: "FULL:0" / "PREPARED:N" (legacy E2E). ae::DataBuffer MakeBenchPayload(std::string_view kind, int sequence); -// Shared encode + Wi-Fi + UDP + post-send hold + Wi-Fi cleanup. -// Used by deep-sleep hot wake and no-sleep prepared-message bench. +// Shared encode + Wi-Fi + UDP + post-send hold + Wi-Fi runtime cleanup. HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload); +// Last send cache usage bits (bench CacheFlags). Valid after SendPreparedOnce. +std::uint8_t LastSendCacheFlags(); + #if defined(ESP_PLATFORM) -// No-sleep bench: AetherApp release may leave ESP-IDF Wi-Fi/netif up; hot -// path expects the same clean stack as after deep-sleep reboot. +// No-sleep bench: AetherApp release may leave ESP-IDF Wi-Fi/netif up. void ReleaseFullAetherWifiForHotPath(); -char const* LastHotWifiFailReason(); +// Invalidate retained BSSID/channel/IP cache (cold boot / failed cache path). +void InvalidatePreparedWifiCache(); #endif -// Try the MCU hot path (deep-sleep gated). Production semantics unchanged. HotSendStatus TryHotWakePreparedSend(std::string const& temperature); -// Export a new prepared block from the already initialized full Aether stream. -// Must be called only after full client/stream are usable. bool ExportPreparedSendBlock(ae::Client::ptr const& client, ae::Uid destination, std::size_t reserve_message_count); diff --git a/main/prepared_wifi_cache_5x20_bench.cpp b/main/prepared_wifi_cache_5x20_bench.cpp new file mode 100644 index 0000000..e871025 --- /dev/null +++ b/main/prepared_wifi_cache_5x20_bench.cpp @@ -0,0 +1,444 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Silent 5x20 prepared Wi-Fi cache experiment (ESP32-C6): + * 1 registration, 5 FULL cycles each preparing 20 messages, 100 prepared sends. + * All timings travel in binary application payloads (no UART results). + */ + +#include +#include + +#include "aether/all.h" +#include "aether/ae_exp_wifi.h" +#include "aether/config.h" +#include "aether/env.h" +#include "bench_payload.h" +#include "prepared_send/prepared_send.h" + +#if defined(ESP_PLATFORM) +# include +# include +# include +# include +# include +# include +#endif + +using namespace std::chrono_literals; + +namespace temp_sensor { +namespace { + +static constexpr auto kParentUid = + ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); + +#ifndef BENCH_CLIENT_ID +# define BENCH_CLIENT_ID "prepared_wifi_cache_5x20_v1" +#endif +static constexpr char const* kBenchClientId = BENCH_CLIENT_ID; + +#if defined(SERVICE_UID) +static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID); +#else +static constexpr auto kServiceUid = + ae::Uid::FromString("3d284a4f-ebb4-451e-a2c5-aecb0d647a45"); +#endif + +static constexpr int kOuterCycles = 5; +static constexpr int kPreparedPerCycle = 20; +static constexpr int kPreparedGapMs = 1000; + +#if defined(ESP_PLATFORM) +static const auto kWifiInit = ae::WiFiInit{ + std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}}, + {}, +}; + +static bool g_had_aether_app = false; + +static void PreConstructCleanup() { + if (!g_had_aether_app) { + return; + } +# if !AE_WIFI_USE_FULL_DEINIT + esp_netif_deinit(); + esp_event_loop_delete_default(); +# endif +} + +static std::int64_t NowUs() { return esp_timer_get_time(); } +#else +static std::int64_t NowUs() { return 0; } +#endif + +enum class Phase : std::uint8_t { + kRegister, + kFullCycle, + kPrepared, + kFinal, + kDone, +}; + +static std::shared_ptr g_app; +static ae::Client::ptr g_client; +static std::unique_ptr g_stream; +static ae::Subscription g_select_sub; +static ae::Subscription g_stream_sub; +static ae::Subscription g_write_sub; + +static Phase g_phase = Phase::kRegister; +static bool g_registration_pending = false; +static bool g_write_armed = false; +static bool g_done = false; + +static int g_outer = 0; +static int g_prepared_index = 0; +static bool g_prepared_waiting_gap = false; +#if defined(ESP_PLATFORM) +static TickType_t g_prepared_gap_until = 0; +#endif + +static std::uint16_t g_seq = 0; +static std::uint32_t g_registration_us = 0; +static std::uint32_t g_last_full_us = 0; +static std::uint32_t g_last_prepared_us = 0; +static std::uint32_t g_pending_full_us = 0; +static bool g_have_pending_full = false; +static std::uint8_t g_sticky_cache_flags = 0; + +static std::int64_t g_t0 = 0; + +static void ReleaseApp() { + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + g_app.reset(); +} + +static std::uint16_t NextSeq() { return ++g_seq; } + +static ae::DataBuffer MakeFullPayload(int outer) { + bench::Payload p{}; + p.type = static_cast(bench::MsgType::kFull); + p.outer_cycle = static_cast(outer); + p.prepared_index = 0; + p.sequence_global = NextSeq(); + p.registration_us = (outer == 1) ? g_registration_us : 0; + p.previous_full_us = g_have_pending_full ? g_pending_full_us : 0; + p.previous_prepared_us = g_last_prepared_us; + p.cache_flags = g_sticky_cache_flags; + g_have_pending_full = false; + return bench::Encode(p); +} + +static ae::DataBuffer MakePreparedPayload(int outer, int index) { + bench::Payload p{}; + p.type = static_cast(bench::MsgType::kPrepared); + p.outer_cycle = static_cast(outer); + p.prepared_index = static_cast(index); + p.sequence_global = NextSeq(); + p.previous_prepared_us = (index == 1) ? 0 : g_last_prepared_us; + // cache_flags describe the *previous* prepared Wi-Fi attempt (index-1). + p.cache_flags = (index == 1) ? 0 : g_sticky_cache_flags; + return bench::Encode(p); +} + +static ae::DataBuffer MakeFinalPayload() { + bench::Payload p{}; + p.type = static_cast(bench::MsgType::kFinal); + p.outer_cycle = kOuterCycles; + p.prepared_index = kPreparedPerCycle; + p.sequence_global = NextSeq(); + p.registration_us = g_registration_us; + p.previous_full_us = g_have_pending_full ? g_pending_full_us : g_last_full_us; + p.previous_prepared_us = g_last_prepared_us; + p.cache_flags = g_sticky_cache_flags; + return bench::Encode(p); +} + +static void ConstructAether() { +#if defined(ESP_PLATFORM) + PreConstructCleanup(); +#endif + g_had_aether_app = true; + g_app = ae::AetherApp::Construct( + ae::AetherAppContext{} +#if AE_DISTILLATION && defined(ESP_PLATFORM) + .AddAdapterFactory([&](ae::AetherAppContext const& ctx) { + return ae::WifiAdapter::ptr::Create( + ae::CreateWith{ctx.domain()}.with_id( + ae::GlobalId::kWiFiAdapter), + ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit); + }) +#endif + ); +} + +static void OnRegisterReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + g_app->aether().Save(); + g_app->Exit(0); +} + +static void DoFullWrite() { + if (g_write_armed) { + return; + } + g_write_armed = true; + auto payload = MakeFullPayload(g_outer); + auto& wa = g_stream->Write(std::move(payload)); + g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) { + if (st != ae::WriteAction::Status::kSuccess) { + g_app->Exit(1); + return; + } + if (!prepared_send::ExportPreparedSendBlock(g_client, kServiceUid, + kPreparedPerCycle)) { + g_app->Exit(1); + return; + } + if (!prepared_send::HasPreparedSendBlock() || + prepared_send::PreparedMessageLeft() != + static_cast(kPreparedPerCycle)) { + g_app->Exit(1); + return; + } + g_app->aether().Save(); + g_app->Exit(0); + }); +} + +static void MaybeFullWrite() { + if (!g_stream || g_write_armed) { + return; + } + if (!g_stream->stream_info().is_writable) { + return; + } + DoFullWrite(); +} + +static void OnFullClientReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + auto client = g_client.Load(); + g_stream = std::make_unique(*g_app, client, kServiceUid, + ae::P2pPortHandle{}); + g_stream_sub = + g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); }); + MaybeFullWrite(); +} + +static void StartRegister() { + g_phase = Phase::kRegister; + g_write_armed = false; + g_t0 = NowUs(); + ConstructAether(); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + g_app->Exit(1); + return; + } + OnRegisterReady(std::move(res).value()); + }); +} + +static void StartFullCycle(int outer) { + g_phase = Phase::kFullCycle; + g_outer = outer; + g_write_armed = false; + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + g_t0 = NowUs(); + ConstructAether(); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + g_app->Exit(1); + return; + } + OnFullClientReady(std::move(res).value()); + }); +} + +static void StartPreparedPhase() { + g_phase = Phase::kPrepared; + g_prepared_index = 1; + g_prepared_waiting_gap = false; + g_sticky_cache_flags = 0; +#if defined(ESP_PLATFORM) + prepared_send::ReleaseFullAetherWifiForHotPath(); +#endif +} + +static void DoFinalWrite() { + if (g_write_armed) { + return; + } + g_write_armed = true; + auto payload = MakeFinalPayload(); + auto& wa = g_stream->Write(std::move(payload)); + g_write_sub = + wa.status_event().Subscribe([](ae::WriteAction::Status) { g_app->Exit(0); }); +} + +static void MaybeFinalWrite() { + if (!g_stream || g_write_armed) { + return; + } + if (!g_stream->stream_info().is_writable) { + return; + } + DoFinalWrite(); +} + +static void OnFinalClientReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + auto client = g_client.Load(); + g_stream = std::make_unique(*g_app, client, kServiceUid, + ae::P2pPortHandle{}); + g_stream_sub = + g_stream->stream_update_event().Subscribe([]() { MaybeFinalWrite(); }); + MaybeFinalWrite(); +} + +static void StartFinal() { + g_phase = Phase::kFinal; + g_write_armed = false; + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + ConstructAether(); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + g_app->Exit(1); + return; + } + OnFinalClientReady(std::move(res).value()); + }); +} + +void BeginAppMainTiming() {} +void FinalizeCycleBeforeSleep() {} +#if defined(ESP_PLATFORM) +void EnterDeepSleep() {} +#endif + +void setup() { +#if defined(ESP_PLATFORM) + nvs_flash_init(); + prepared_send::InvalidatePreparedWifiCache(); +#endif + g_done = false; + g_seq = 0; + g_registration_pending = true; + g_last_prepared_us = 0; + g_have_pending_full = false; + g_sticky_cache_flags = 0; +} + +void loop() { + if (g_done) { + return; + } + + if (g_registration_pending) { + g_registration_pending = false; + StartRegister(); + return; + } + + if (g_phase == Phase::kPrepared) { +#if defined(ESP_PLATFORM) + if (g_prepared_waiting_gap) { + if (xTaskGetTickCount() < g_prepared_gap_until) { + vTaskDelay(pdMS_TO_TICKS(20)); + return; + } + g_prepared_waiting_gap = false; + } +#endif + + if (g_prepared_index > kPreparedPerCycle) { + if (g_outer < kOuterCycles) { + StartFullCycle(g_outer + 1); + } else { + StartFinal(); + } + return; + } + + int const i = g_prepared_index; + auto payload = MakePreparedPayload(g_outer, i); + auto const t0 = NowUs(); + auto const status = prepared_send::SendPreparedOnce(payload); + auto const us = static_cast(NowUs() - t0); + (void)status; + g_last_prepared_us = us; + g_sticky_cache_flags = prepared_send::LastSendCacheFlags(); + + ++g_prepared_index; + if (g_prepared_index <= kPreparedPerCycle) { +#if defined(ESP_PLATFORM) + g_prepared_waiting_gap = true; + g_prepared_gap_until = + xTaskGetTickCount() + pdMS_TO_TICKS(kPreparedGapMs); +#endif + } + return; + } + + if (!g_app) { + return; + } + + if (!g_app->IsExited()) { + auto t = g_app->Update(ae::Now()); + g_app->WaitUntil(t); + return; + } + + if (g_phase == Phase::kRegister) { + ReleaseApp(); + g_registration_us = static_cast(NowUs() - g_t0); + StartFullCycle(1); + return; + } + + if (g_phase == Phase::kFullCycle) { + ReleaseApp(); + auto const full_us = static_cast(NowUs() - g_t0); + g_last_full_us = full_us; + g_pending_full_us = full_us; + g_have_pending_full = true; + StartPreparedPhase(); + return; + } + + if (g_phase == Phase::kFinal) { + ReleaseApp(); + g_phase = Phase::kDone; + g_done = true; + } +} + +} // namespace +} // namespace temp_sensor + +void setup() { temp_sensor::setup(); } +void loop() { temp_sensor::loop(); } diff --git a/sdkconfig.defaults.silent b/sdkconfig.defaults.silent new file mode 100644 index 0000000..a1cf56b --- /dev/null +++ b/sdkconfig.defaults.silent @@ -0,0 +1,13 @@ +# Silent measurement build: no console / no logging. + +CONFIG_ESP_CONSOLE_NONE=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y + +CONFIG_LOG_DEFAULT_LEVEL_NONE=y +CONFIG_LOG_DEFAULT_LEVEL=0 +CONFIG_LOG_MAXIMUM_LEVEL=0 + +CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y +CONFIG_BOOTLOADER_LOG_LEVEL=0 + +CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp index d898fc1..00a6ab9 100644 --- a/temperature_receiver/main.cpp +++ b/temperature_receiver/main.cpp @@ -1,10 +1,11 @@ /* * Copyright 2026 Aethernet Inc. * - * Desktop Æther console receiver for prepared-message E2E bench. - * Expects UTF-8 payloads: FULL:0 and PREPARED:1..10 + * Desktop Æther receiver for silent prepared Wi-Fi cache 5x20 experiment. + * Decodes binary bench payloads and prints aggregate statistics. */ +#include #include #include #include @@ -13,7 +14,6 @@ #include #include #include -#include #include #if defined(_WIN32) @@ -21,6 +21,7 @@ #endif #include "aether/all.h" +#include "bench_payload.h" using namespace std::chrono_literals; @@ -28,17 +29,33 @@ namespace { static constexpr auto kParentUid = ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); -static constexpr char const* kClientName = "prepared_message_bench_rx_v1"; -static constexpr int kExpectedPrepared = 10; +static constexpr char const* kClientName = "prepared_wifi_cache_rx_v1"; +static constexpr int kOuter = 5; +static constexpr int kPreparedPer = 20; +static constexpr int kExpectedFull = kOuter; +static constexpr int kExpectedPrepared = kOuter * kPreparedPer; +static constexpr int kExpectedApp = kExpectedFull + kExpectedPrepared; // 105 +static constexpr int kExpectedTotal = kExpectedApp + 1; // +FINAL std::mutex g_mu; std::vector> g_streams; -bool g_full_seen = false; -std::array g_prepared_hits{}; // 1..10 -int g_prepared_unique = 0; +std::uint32_t g_registration_us = 0; +std::array g_full_us{}; +std::array g_full_have{}; +std::array, kOuter> g_prep_us{}; +std::array, kOuter> g_prep_have{}; +std::array, kOuter> g_prep_flags{}; + +int g_full_recv = 0; +int g_prep_recv = 0; +int g_final_recv = 0; int g_duplicates = 0; -std::vector g_order; +int g_out_of_order = 0; +int g_last_seq = 0; +bool g_done = false; + +std::vector g_seen_seq; std::int64_t NowMs() { return std::chrono::duration_cast( @@ -46,85 +63,214 @@ std::int64_t NowMs() { .count(); } +std::uint32_t Percentile(std::vector v, int pct) { + if (v.empty()) { + return 0; + } + std::sort(v.begin(), v.end()); + auto const idx = (pct * (static_cast(v.size()) - 1) + 99) / 100; + return v[static_cast(idx)]; +} + void PrintSummary() { - int missing = 0; - std::string missing_list; - for (int i = 1; i <= kExpectedPrepared; ++i) { - if (g_prepared_hits[static_cast(i)] == 0) { - ++missing; - if (!missing_list.empty()) { - missing_list += ","; + std::vector fulls; + std::vector firsts; + std::vector warms; + std::vector all; + int bssid_hits = 0; + int ip_hits = 0; + int dhcp_skip = 0; + int fallbacks = 0; + + for (int o = 0; o < kOuter; ++o) { + if (g_full_have[static_cast(o)]) { + fulls.push_back(g_full_us[static_cast(o)]); + } + for (int i = 0; i < kPreparedPer; ++i) { + if (!g_prep_have[static_cast(o)][static_cast(i)]) { + continue; + } + auto const us = g_prep_us[static_cast(o)][static_cast(i)]; + auto const fl = g_prep_flags[static_cast(o)][static_cast(i)]; + all.push_back(us); + if (i == 0) { + firsts.push_back(us); + } else { + warms.push_back(us); + } + if (fl & static_cast(temp_sensor::bench::CacheFlags::kUsedBssid)) { + ++bssid_hits; + } + if (fl & static_cast(temp_sensor::bench::CacheFlags::kUsedStaticIp)) { + ++ip_hits; + } + if (fl & static_cast(temp_sensor::bench::CacheFlags::kDhcpSkipped)) { + ++dhcp_skip; + } + if (fl & static_cast(temp_sensor::bench::CacheFlags::kFallback)) { + ++fallbacks; } - missing_list += std::to_string(i); } } - std::cout << "RECEIVER\n"; - std::cout << " full=" << (g_full_seen ? 1 : 0) << "/1\n"; - std::cout << " prepared=" << g_prepared_unique << "/" << kExpectedPrepared - << "\n"; - std::cout << " missing=" << missing; - if (missing > 0) { - std::cout << " [" << missing_list << "]"; - } - std::cout << "\n"; - std::cout << " duplicates=" << g_duplicates << "\n"; - std::cout << " order="; - for (size_t i = 0; i < g_order.size(); ++i) { - if (i > 0) { - std::cout << ","; + + auto print_vec = [](char const* name, std::vector const& v) { + std::cout << name << " raw=["; + for (size_t i = 0; i < v.size(); ++i) { + if (i) { + std::cout << ", "; + } + std::cout << v[i]; + } + std::cout << "]\n"; + if (v.empty()) { + return; + } + auto sorted = v; + std::sort(sorted.begin(), sorted.end()); + std::cout << " min=" << sorted.front() << "\n"; + std::cout << " median=" << Percentile(v, 50) << "\n"; + if (v.size() >= 10) { + std::cout << " p90=" << Percentile(v, 90) << "\n"; + std::cout << " p99=" << Percentile(v, 99) << "\n"; + } + std::cout << " max=" << sorted.back() << "\n"; + std::cout << " n=" << v.size() << "\n"; + }; + + int missing = 0; + for (int s = 1; s <= kExpectedApp; ++s) { + if (std::find(g_seen_seq.begin(), g_seen_seq.end(), + static_cast(s)) == g_seen_seq.end()) { + ++missing; } - std::cout << g_order[i]; } - std::cout << "\n"; + + std::cout << "REGISTRATION\n"; + std::cout << " time_us=" << g_registration_us << "\n"; + std::cout << "FULL\n"; + print_vec(" ", fulls); + std::cout << "FIRST PREPARED\n"; + print_vec(" ", firsts); + std::cout << "WARM PREPARED\n"; + print_vec(" ", warms); + std::cout << "ALL PREPARED\n"; + print_vec(" ", all); + std::cout << "DELIVERY\n"; + std::cout << " full=" << g_full_recv << "/" << kExpectedFull << "\n"; + std::cout << " prepared=" << g_prep_recv << "/" << kExpectedPrepared << "\n"; + std::cout << " final=" << g_final_recv << "/1\n"; + std::cout << " missing=" << missing << "\n"; + std::cout << " duplicates=" << g_duplicates << "\n"; + std::cout << " out_of_order=" << g_out_of_order << "\n"; + std::cout << "CACHE\n"; + std::cout << " BSSID reuse confirmed=" + << (bssid_hits > 0 ? "yes" : "no") << " (hits=" << bssid_hits + << ")\n"; + std::cout << " channel reuse yes/no via BSSID flag hits=" << bssid_hits + << "\n"; + std::cout << " static IP reuse confirmed=" + << (ip_hits > 0 ? "yes" : "no") << " (hits=" << ip_hits << ")\n"; + std::cout << " DHCP skipped confirmed=" + << (dhcp_skip > 0 ? "yes" : "no") << " (hits=" << dhcp_skip + << ")\n"; + std::cout << " fallbacks=" << fallbacks << "\n"; + std::cout << "NOTE prepared timing includes AETHER_PREPARED_POST_SEND_HOLD_MS=300\n"; + std::cout << "BENCH_DONE\n"; std::cout.flush(); } void OnMessage(ae::Uid sender, ae::DataBuffer const& data) { - auto text = std::string_view{reinterpret_cast(data.data()), - data.size()}; - auto const ts = NowMs(); - auto const sender_text = ae::Format("{}", sender); + temp_sensor::bench::Payload p{}; + if (!temp_sensor::bench::Decode(data, p)) { + std::cout << "RECV unknown sender=" << ae::Format("{}", sender) + << " size=" << data.size() << "\n"; + return; + } + auto const ts = NowMs(); std::lock_guard lock{g_mu}; - if (text == "FULL:0") { - if (g_full_seen) { - ++g_duplicates; + + if (std::find(g_seen_seq.begin(), g_seen_seq.end(), p.sequence_global) != + g_seen_seq.end()) { + ++g_duplicates; + } else { + g_seen_seq.push_back(p.sequence_global); + } + if (p.sequence_global != 0 && g_last_seq != 0 && + p.sequence_global < static_cast(g_last_seq)) { + ++g_out_of_order; + } + g_last_seq = p.sequence_global; + + auto type = static_cast(p.type); + if (type == temp_sensor::bench::MsgType::kFull) { + ++g_full_recv; + if (p.outer_cycle == 1 && p.registration_us != 0) { + g_registration_us = p.registration_us; } - g_full_seen = true; - g_order.emplace_back("FULL:0"); - std::cout << ae::Format( - "RECV sender={} sequence=0 type=FULL receive_ts_ms={}\n", sender_text, - ts); - } else if (text.rfind("PREPARED:", 0) == 0) { - auto const seq_sv = text.substr(std::string_view{"PREPARED:"}.size()); - int seq = 0; - try { - seq = std::stoi(std::string{seq_sv}); - } catch (...) { - seq = -1; + // previous_full_us is timing of outer_cycle-1 + if (p.outer_cycle >= 2 && p.outer_cycle <= kOuter + 1) { + int const idx = static_cast(p.outer_cycle) - 2; + if (idx >= 0 && idx < kOuter && p.previous_full_us != 0) { + g_full_us[static_cast(idx)] = p.previous_full_us; + g_full_have[static_cast(idx)] = true; + } } - if (seq >= 1 && seq <= kExpectedPrepared) { - if (g_prepared_hits[static_cast(seq)] > 0) { - ++g_duplicates; - } else { - ++g_prepared_unique; + // previous_prepared_us is last prepared of previous outer + if (p.outer_cycle >= 2 && p.previous_prepared_us != 0) { + int const o = static_cast(p.outer_cycle) - 2; + if (o >= 0 && o < kOuter) { + g_prep_us[static_cast(o)][kPreparedPer - 1] = + p.previous_prepared_us; + g_prep_have[static_cast(o)][kPreparedPer - 1] = true; + g_prep_flags[static_cast(o)][kPreparedPer - 1] = p.cache_flags; } - ++g_prepared_hits[static_cast(seq)]; - g_order.emplace_back(std::string{text}); } std::cout << ae::Format( - "RECV sender={} sequence={} type=PREPARED receive_ts_ms={}\n", - sender_text, seq, ts); - } else { + "RECV FULL outer={} seq={} reg_us={} prev_full_us={} prev_prep_us={} " + "ts={}\n", + p.outer_cycle, p.sequence_global, p.registration_us, p.previous_full_us, + p.previous_prepared_us, ts); + } else if (type == temp_sensor::bench::MsgType::kPrepared) { + ++g_prep_recv; + int const o = static_cast(p.outer_cycle) - 1; + int const i = static_cast(p.prepared_index) - 1; + // previous_prepared_us is timing of prepared_index-1 + if (o >= 0 && o < kOuter && p.prepared_index >= 2) { + int const pi = static_cast(p.prepared_index) - 2; + if (pi >= 0 && pi < kPreparedPer) { + g_prep_us[static_cast(o)][static_cast(pi)] = + p.previous_prepared_us; + g_prep_have[static_cast(o)][static_cast(pi)] = true; + g_prep_flags[static_cast(o)][static_cast(pi)] = + p.cache_flags; + } + } std::cout << ae::Format( - "RECV sender={} sequence=? type=UNKNOWN receive_ts_ms={} text={}\n", - sender_text, ts, text); - } - std::cout.flush(); - - if (g_full_seen && g_prepared_unique == kExpectedPrepared) { + "RECV PREPARED outer={} idx={} seq={} prev_us={} flags={} ts={}\n", + p.outer_cycle, p.prepared_index, p.sequence_global, + p.previous_prepared_us, p.cache_flags, ts); + } else if (type == temp_sensor::bench::MsgType::kFinal) { + ++g_final_recv; + if (p.previous_full_us != 0) { + g_full_us[kOuter - 1] = p.previous_full_us; + g_full_have[kOuter - 1] = true; + } + if (p.previous_prepared_us != 0) { + g_prep_us[kOuter - 1][kPreparedPer - 1] = p.previous_prepared_us; + g_prep_have[kOuter - 1][kPreparedPer - 1] = true; + g_prep_flags[kOuter - 1][kPreparedPer - 1] = p.cache_flags; + } + if (p.registration_us != 0) { + g_registration_us = p.registration_us; + } + std::cout << ae::Format( + "RECV FINAL prev_full_us={} prev_prep_us={} flags={} ts={}\n", + p.previous_full_us, p.previous_prepared_us, p.cache_flags, ts); PrintSummary(); + g_done = true; } + std::cout.flush(); } std::filesystem::path ResolveSessionRoot() { @@ -147,7 +293,6 @@ int main() { std::filesystem::create_directories(session_root / "state"); std::filesystem::current_path(session_root); std::cerr << ae::Format("receiver_session_dir={}\n", session_root.string()); - std::cerr.flush(); auto aether_app = ae::AetherApp::Construct(ae::AetherAppContext{}); ae::Client::ptr client; @@ -182,10 +327,18 @@ int main() { while (!aether_app->IsExited()) { auto next = aether_app->Update(ae::Now()); aether_app->WaitUntil(next); + { + std::lock_guard lock{g_mu}; + if (g_done) { + aether_app->Exit(0); + } + } } { std::lock_guard lock{g_mu}; - PrintSummary(); + if (!g_done) { + PrintSummary(); + } } return aether_app->ExitCode(); } From f2045c8c84d778211409d11b55eb5e75acba10bf Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Fri, 28 Aug 2026 15:59:16 -0700 Subject: [PATCH 21/32] Add gateway MAC cache and static ARP on prepared UDP path. Export Wi-Fi/IP/gateway-MAC from the live FULL connection so first prepared sends stay fast; install static ARP before sendto. Hardware 5x20 still shows poor delivery (29/100); hold=600 control did not help. Co-authored-by: Cursor --- .../PREPARED_STATIC_ARP_5X20_REPORT.md | 133 +++++++++++++ experiments/prepared_static_arp_5x20.tsv | 18 ++ main/bench_payload.h | 4 +- main/prepared_send/prepared_send.cpp | 181 +++++++++++++++++- main/prepared_send/prepared_send.h | 6 +- main/prepared_wifi_cache_5x20_bench.cpp | 3 + sdkconfig.defaults.silent | 4 + temperature_receiver/main.cpp | 18 +- 8 files changed, 358 insertions(+), 9 deletions(-) create mode 100644 experiments/PREPARED_STATIC_ARP_5X20_REPORT.md create mode 100644 experiments/prepared_static_arp_5x20.tsv diff --git a/experiments/PREPARED_STATIC_ARP_5X20_REPORT.md b/experiments/PREPARED_STATIC_ARP_5X20_REPORT.md new file mode 100644 index 0000000..123a9c5 --- /dev/null +++ b/experiments/PREPARED_STATIC_ARP_5X20_REPORT.md @@ -0,0 +1,133 @@ +# Prepared static ARP 5×20 (ESP32-C6, no sleep, silent) + +Hardware: ESP32-C6, ESP-IDF v6.0.2, COM7 +Aether: `exp/esp32c6-wifi-lifecycle-diag` @ `157aadbec8e7b852d0f89274307ff7cb8103e5f7` (**unchanged**) +Firmware: `AE_EXP_PREPARED_WIFI_CACHE_5X20=1`, silent console/log NONE +Client: `prepared_wifi_cache_5x20_v1` +Receiver UID: `5aade50f-00d9-4624-b097-e203cdcf1e38` +Post-send hold: **300 ms** (included in prepared timings) + +## Changes vs `669ccdc3…` + +- Local prepared Wi-Fi cache extended with `gateway_mac[6]` + `gateway_mac_valid` +- Cold/DHCP path resolves gateway MAC via lwIP ARP (`etharp_request` / `etharp_find_addr` on tcpip thread) +- `CapturePreparedWifiCacheFromActiveConnection()` exports BSSID/channel/IP/gw/MAC from live FULL Æther Wi-Fi before release +- Fast path installs static ARP (`etharp_add_static_entry` via `esp_netif_tcpip_exec`) before `EncodePacket` / `sendto` +- Payload flags: `used_static_arp`, `arp_fallback`, `wifi_fallback` (plus existing BSSID/IP/DHCP bits) + +## Comparison to previous (`669ccdc3…`) + +| Metric | BEFORE (`669ccdc`) | AFTER (static ARP + FULL cache export) | +|---|---|---| +| prepared delivery | **44/100** | **29/100** | +| warm median | ~640 ms | ~650 ms | +| FIRST PREPARED | ~15.1 s cold (1/5 recovered) | **~660–760 ms** (3/5 recovered) | +| used_static_arp | n/a | **32 hits** | +| arp_fallback | n/a | 0 | +| wifi_fallback | 0 | 0 | + +**Verdict:** FULL→prepared cache export fixed FIRST PREPARED (no longer ~15 s cold). Static ARP installed and used (`flags=15`). Delivery did **not** improve (still far from 100/100); slightly worse in this run. + +## REGISTRATION + +``` +time_us=251137 +``` + +(~0.25 s) — NVS already had registration after earlier erase+boot; not a cold cloud register. + +## FULL + +``` +raw=[4738679, 3471545, 2911524, 3591534, 2871515] +min=2871515 +median=3471545 +max=4738679 +n=5 +``` + +## FIRST PREPARED (after FULL cache export) + +``` +raw=[760476, 730476, 660476] +min=660476 +median=730476 +max=760476 +n=3 +``` + +(~0.66–0.76 s including 300 ms hold) — **fast**, matches warm path. + +### FIRST PREPARED BEFORE vs AFTER + +| | BEFORE | AFTER | +|---|---|---| +| FIRST PREPARED | ~cold / ~15 s | ~730 ms median | + +## WARM PREPARED + +``` +n=29 +min=610482 +median=650477 +p90=720478 +p99=3020485 +max=3020485 +``` + +(~0.65 s median including 300 ms hold). Two ~3.0 s outliers. + +## ALL PREPARED (recovered timings) + +``` +n=32 +min=610482 +median=650482 +p90=730476 +p99=3020485 +max=3020485 +``` + +## DELIVERY + +``` +full=6/5 +prepared=29/100 +final=1/1 +missing=71 +duplicates=1 +out_of_order=0 +``` + +`full=6/5` / `duplicates=1`: one duplicated FULL outer=1 after mid-run hard reset. + +## CACHE + +``` +BSSID reuse confirmed=yes (hits=32) +static IP reuse confirmed=yes (hits=32) +DHCP skipped confirmed=yes (hits=32) +used_static_arp hits=32 +arp_fallback hits=0 +wifi_fallback hits=0 +``` + +`cache_flags=15` = `UsedBssid | UsedStaticIp | DhcpSkipped | UsedStaticArp`. + +## Control: hold=600 ms × 20 prepared + +Same fast path + static ARP; only hold increased to 600 ms; one outer × 20. + +``` +prepared delivery = 6/20 (~30%) +timings ~930–970 ms (includes 600 ms hold) +used_static_arp on recovered samples +``` + +**600 ms did not improve delivery** (same ~30% rate as 29/100). Unlikely that post-`sendto` async TX flush alone explains losses under a 300 ms hold. + +## Notes / next hypotheses + +- Static ARP + cache export are working; reliability bottleneck is elsewhere (Wi-Fi teardown vs UDP path, AP/router drop, packet size/path, or association instability — see ~3 s outliers). +- Do not increase hold further without a new hypothesis. +- Prefer targeted diagnostics (e.g. keep Wi-Fi up across prepared burst, or confirm UDP arrives at gateway) over broad refactors. diff --git a/experiments/prepared_static_arp_5x20.tsv b/experiments/prepared_static_arp_5x20.tsv new file mode 100644 index 0000000..2326770 --- /dev/null +++ b/experiments/prepared_static_arp_5x20.tsv @@ -0,0 +1,18 @@ +metric before_669ccdc after_static_arp hold600_control +prepared_delivery 44/100 29/100 6/20 +full_delivery 5/5 6/5 (dup) 1/1 +warm_median_us 640476 650477 ~ +first_prepared_median_us 15100453 730476 ~ +used_static_arp_hits 0 32 6 +arp_fallback_hits 0 0 0 +wifi_fallback_hits 0 0 0 +bssid_hits 46 32 6 +static_ip_hits 46 32 6 +dhcp_skipped_hits 46 32 6 +missing 56 71 14 +duplicates 0 1 0 +out_of_order 0 0 0 +post_send_hold_ms 300 300 600 +registration_us 13962906 251137 253771 +full_median_us 4124003 3471545 3437457 +aether_sha 157aadbec8e7b852d0f89274307ff7cb8103e5f7 157aadbec8e7b852d0f89274307ff7cb8103e5f7 157aadbec8e7b852d0f89274307ff7cb8103e5f7 diff --git a/main/bench_payload.h b/main/bench_payload.h index f30c71a..5327497 100644 --- a/main/bench_payload.h +++ b/main/bench_payload.h @@ -27,7 +27,9 @@ enum class CacheFlags : std::uint8_t { kUsedBssid = 1 << 0, kUsedStaticIp = 1 << 1, kDhcpSkipped = 1 << 2, - kFallback = 1 << 3, + kUsedStaticArp = 1 << 3, + kArpFallback = 1 << 4, + kWifiFallback = 1 << 5, }; #pragma pack(push, 1) diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index ddd63a6..9e40ef9 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -42,6 +42,11 @@ # include # include # include +# include +# include +# include +# include +# include # include # if !defined(AE_EXP_SILENT) # include @@ -83,6 +88,10 @@ static constexpr char const* kTag = "prepared-send"; # define AETHER_PREPARED_HOT_WIFI_MAX_RETRY 10 #endif +#ifndef AETHER_PREPARED_ARP_TIMEOUT_MS +# define AETHER_PREPARED_ARP_TIMEOUT_MS 500 +#endif + static std::uint8_t g_last_send_cache_flags = 0; #if defined(ESP_PLATFORM) @@ -91,8 +100,10 @@ static RTC_NOINIT_ATTR ae::prepared_packet::PreparedSendMessageBlock static RTC_DATA_ATTR esp_netif_ip_info_t rtc_ip_info = {}; static RTC_DATA_ATTR WiFiBaseStation base_station{}; +static RTC_DATA_ATTR std::uint8_t gateway_mac[6] = {}; static RTC_NOINIT_ATTR bool address_is_valid; static RTC_NOINIT_ATTR bool bs_is_valid; +static RTC_NOINIT_ATTR bool gateway_mac_valid; static_assert(sizeof(ae::prepared_packet::PreparedSendMessageBlock) <= 8 * 1024, "PreparedSendMessageBlock must fit in ESP32 RTC slow memory"); @@ -160,6 +171,129 @@ void CaptureApIntoCache() { bs_is_valid = true; } +struct GatewayMacLookupCtx { + struct netif* lwip_netif{nullptr}; + ip4_addr_t gw{}; + std::uint8_t mac[6]{}; + bool found{false}; +}; + +esp_err_t LookupGatewayMacTcpip(void* arg) { + auto* ctx = static_cast(arg); + struct eth_addr* eth_ret = nullptr; + ip4_addr_t const* ip_ret = nullptr; + auto const idx = + etharp_find_addr(ctx->lwip_netif, &ctx->gw, ð_ret, &ip_ret); + if (idx >= 0 && eth_ret != nullptr) { + std::memcpy(ctx->mac, eth_ret->addr, sizeof(ctx->mac)); + ctx->found = true; + } + return ESP_OK; +} + +esp_err_t RequestGatewayArpTcpip(void* arg) { + auto* ctx = static_cast(arg); + (void)etharp_request(ctx->lwip_netif, &ctx->gw); + return ESP_OK; +} + +struct StaticArpInstallCtx { + ip4_addr_t gw{}; + struct eth_addr eth{}; + err_t err{ERR_VAL}; +}; + +esp_err_t InstallStaticGatewayArpTcpip(void* arg) { + auto* ctx = static_cast(arg); + ctx->err = etharp_add_static_entry(&ctx->gw, &ctx->eth); + return ESP_OK; +} + +bool LookupGatewayMac(esp_netif_t* esp_netif, std::uint8_t out_mac[6]) { + if (esp_netif == nullptr || rtc_ip_info.gw.addr == 0) { + return false; + } + auto* lwip_netif = + static_cast(esp_netif_get_netif_impl(esp_netif)); + if (lwip_netif == nullptr) { + return false; + } + + GatewayMacLookupCtx ctx{}; + ctx.lwip_netif = lwip_netif; + ctx.gw.addr = rtc_ip_info.gw.addr; + if (esp_netif_tcpip_exec(&LookupGatewayMacTcpip, &ctx) != ESP_OK || + !ctx.found) { + return false; + } + std::memcpy(out_mac, ctx.mac, 6); + return true; +} + +bool ResolveAndCacheGatewayMac(esp_netif_t* esp_netif) { + std::uint8_t mac[6]{}; + if (LookupGatewayMac(esp_netif, mac)) { + std::memcpy(gateway_mac, mac, sizeof(gateway_mac)); + gateway_mac_valid = true; + return true; + } + + auto* lwip_netif = + static_cast(esp_netif_get_netif_impl(esp_netif)); + if (lwip_netif == nullptr || rtc_ip_info.gw.addr == 0) { + return false; + } + + GatewayMacLookupCtx req{}; + req.lwip_netif = lwip_netif; + req.gw.addr = rtc_ip_info.gw.addr; + (void)esp_netif_tcpip_exec(&RequestGatewayArpTcpip, &req); + + auto const deadline = + std::chrono::steady_clock::now() + + std::chrono::milliseconds(AETHER_PREPARED_ARP_TIMEOUT_MS); + while (std::chrono::steady_clock::now() < deadline) { + vTaskDelay(pdMS_TO_TICKS(20)); + if (LookupGatewayMac(esp_netif, mac)) { + std::memcpy(gateway_mac, mac, sizeof(gateway_mac)); + gateway_mac_valid = true; + return true; + } + } + return false; +} + +bool InstallStaticGatewayArp() { + if (!gateway_mac_valid || !address_is_valid || rtc_ip_info.gw.addr == 0) { + return false; + } + StaticArpInstallCtx ctx{}; + ctx.gw.addr = rtc_ip_info.gw.addr; + std::memcpy(ctx.eth.addr, gateway_mac, sizeof(gateway_mac)); + if (esp_netif_tcpip_exec(&InstallStaticGatewayArpTcpip, &ctx) != ESP_OK) { + return false; + } + return ctx.err == ERR_OK; +} + +// Ensure gateway L2 destination is known before UDP sendto. +// Prefer cached MAC + static ARP; otherwise resolve via ARP (do not drop). +bool EnsureGatewayArpReady(esp_netif_t* esp_netif) { + if (gateway_mac_valid && InstallStaticGatewayArp()) { + g_last_send_cache_flags |= + static_cast(bench::CacheFlags::kUsedStaticArp); + return true; + } + + g_last_send_cache_flags |= + static_cast(bench::CacheFlags::kArpFallback); + if (ResolveAndCacheGatewayMac(esp_netif)) { + (void)InstallStaticGatewayArp(); + } + // Do not discard the message if ARP is still unresolved; sendto may queue. + return true; +} + void WifiEventHandler(void*, esp_event_base_t event_base, std::int32_t event_id, void* event_data) { if (g_wifi_event_group == nullptr) { @@ -385,7 +519,17 @@ bool StartWifiAttempt(bool use_bssid_cache, bool use_static_ip) { esp_wifi_internal_set_fix_rate(WIFI_IF_STA, true, (wifi_phy_rate_t)0x0); - return (bits & kWifiReadyBit) != 0; + if ((bits & kWifiReadyBit) == 0) { + return false; + } + + // After DHCP (cold) or whenever IP is known, refresh gateway MAC cache. + if (address_is_valid && g_wifi_netif != nullptr) { + if (!gateway_mac_valid || !use_static_ip) { + (void)ResolveAndCacheGatewayMac(g_wifi_netif); + } + } + return true; } bool EnsureWifiConnectedForHotPath() { @@ -405,6 +549,7 @@ bool EnsureWifiConnectedForHotPath() { static_cast(bench::CacheFlags::kUsedStaticIp) | static_cast(bench::CacheFlags::kDhcpSkipped); } + (void)EnsureGatewayArpReady(g_wifi_netif); return true; } @@ -412,13 +557,14 @@ bool EnsureWifiConnectedForHotPath() { CleanupHotPathWifiRuntime(); InvalidatePreparedWifiCache(); g_last_send_cache_flags = - static_cast(bench::CacheFlags::kFallback); + static_cast(bench::CacheFlags::kWifiFallback); } if (!StartWifiAttempt(/*use_bssid_cache=*/false, /*use_static_ip=*/false)) { CleanupHotPathWifiRuntime(); return false; } + (void)EnsureGatewayArpReady(g_wifi_netif); return true; } @@ -513,8 +659,37 @@ ae::DataBuffer MakeBenchPayload(std::string_view kind, int sequence) { void InvalidatePreparedWifiCache() { address_is_valid = false; bs_is_valid = false; + gateway_mac_valid = false; std::memset(&rtc_ip_info, 0, sizeof(rtc_ip_info)); std::memset(&base_station, 0, sizeof(base_station)); + std::memset(gateway_mac, 0, sizeof(gateway_mac)); +} + +bool CapturePreparedWifiCacheFromActiveConnection() { + esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (netif == nullptr) { + return false; + } + + esp_netif_ip_info_t ip_info{}; + if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK || + ip_info.ip.addr == 0) { + return false; + } + + rtc_ip_info = ip_info; + address_is_valid = true; + CaptureApIntoCache(); + if (!bs_is_valid) { + return false; + } + + // Prefer existing ARP entry from the live FULL session; request if needed. + if (!ResolveAndCacheGatewayMac(netif)) { + // Still keep BSSID/IP cache; prepared path can fall back to ARP wait. + gateway_mac_valid = false; + } + return true; } void ReleaseFullAetherWifiForHotPath() { @@ -585,7 +760,7 @@ HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload) { } # ifndef AETHER_PREPARED_POST_SEND_HOLD_MS -# define AETHER_PREPARED_POST_SEND_HOLD_MS 450 +# define AETHER_PREPARED_POST_SEND_HOLD_MS 300 # endif std::this_thread::sleep_for( std::chrono::milliseconds(AETHER_PREPARED_POST_SEND_HOLD_MS)); diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h index 4ef0af4..6d238ce 100644 --- a/main/prepared_send/prepared_send.h +++ b/main/prepared_send/prepared_send.h @@ -51,8 +51,12 @@ std::uint8_t LastSendCacheFlags(); // No-sleep bench: AetherApp release may leave ESP-IDF Wi-Fi/netif up. void ReleaseFullAetherWifiForHotPath(); -// Invalidate retained BSSID/channel/IP cache (cold boot / failed cache path). +// Invalidate retained BSSID/channel/IP/gateway-MAC cache. void InvalidatePreparedWifiCache(); + +// Export Wi-Fi association + IP + gateway MAC from the still-active FULL path. +// Call before destroying Aether Wi-Fi so prepared #1 can use the fast path. +bool CapturePreparedWifiCacheFromActiveConnection(); #endif HotSendStatus TryHotWakePreparedSend(std::string const& temperature); diff --git a/main/prepared_wifi_cache_5x20_bench.cpp b/main/prepared_wifi_cache_5x20_bench.cpp index e871025..8ab31d4 100644 --- a/main/prepared_wifi_cache_5x20_bench.cpp +++ b/main/prepared_wifi_cache_5x20_bench.cpp @@ -206,6 +206,9 @@ static void DoFullWrite() { g_app->Exit(1); return; } + // Export association/IP/gateway-MAC into local prepared cache while FULL + // Wi-Fi is still up so prepared #1 can take the fast path. + (void)prepared_send::CapturePreparedWifiCacheFromActiveConnection(); g_app->aether().Save(); g_app->Exit(0); }); diff --git a/sdkconfig.defaults.silent b/sdkconfig.defaults.silent index a1cf56b..c274f60 100644 --- a/sdkconfig.defaults.silent +++ b/sdkconfig.defaults.silent @@ -11,3 +11,7 @@ CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y CONFIG_BOOTLOADER_LOG_LEVEL=0 CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 + +# Needed so etharp_add_static_entry is compiled (via DHCPS_STATIC_ENTRIES mapping +# in ESP-IDF lwipopts). Already typically enabled; keep explicit for silent builds. +CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp index 00a6ab9..9ecb25b 100644 --- a/temperature_receiver/main.cpp +++ b/temperature_receiver/main.cpp @@ -80,7 +80,9 @@ void PrintSummary() { int bssid_hits = 0; int ip_hits = 0; int dhcp_skip = 0; - int fallbacks = 0; + int static_arp_hits = 0; + int arp_fallback_hits = 0; + int wifi_fallback_hits = 0; for (int o = 0; o < kOuter; ++o) { if (g_full_have[static_cast(o)]) { @@ -107,8 +109,14 @@ void PrintSummary() { if (fl & static_cast(temp_sensor::bench::CacheFlags::kDhcpSkipped)) { ++dhcp_skip; } - if (fl & static_cast(temp_sensor::bench::CacheFlags::kFallback)) { - ++fallbacks; + if (fl & static_cast(temp_sensor::bench::CacheFlags::kUsedStaticArp)) { + ++static_arp_hits; + } + if (fl & static_cast(temp_sensor::bench::CacheFlags::kArpFallback)) { + ++arp_fallback_hits; + } + if (fl & static_cast(temp_sensor::bench::CacheFlags::kWifiFallback)) { + ++wifi_fallback_hits; } } } @@ -173,7 +181,9 @@ void PrintSummary() { std::cout << " DHCP skipped confirmed=" << (dhcp_skip > 0 ? "yes" : "no") << " (hits=" << dhcp_skip << ")\n"; - std::cout << " fallbacks=" << fallbacks << "\n"; + std::cout << " used_static_arp hits=" << static_arp_hits << "\n"; + std::cout << " arp_fallback hits=" << arp_fallback_hits << "\n"; + std::cout << " wifi_fallback hits=" << wifi_fallback_hits << "\n"; std::cout << "NOTE prepared timing includes AETHER_PREPARED_POST_SEND_HOLD_MS=300\n"; std::cout << "BENCH_DONE\n"; std::cout.flush(); From 631bc2f64b18d0bb791a5df499fc603b18394541 Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Fri, 28 Aug 2026 16:32:03 -0700 Subject: [PATCH 22/32] Add keep-Wi-Fi-up prepared 5x20 A/B and report 96/100 delivery. Prove losses track per-message Wi-Fi teardown/reassociation, not prepared packet path. Co-authored-by: Cursor --- CMakeLists.txt | 5 +- experiments/PREPARED_KEEP_WIFI_UP_REPORT.md | 103 +++++ main/CMakeLists.txt | 10 +- main/prepared_keep_wifi_up_5x20_bench.cpp | 473 ++++++++++++++++++++ main/prepared_send/prepared_send.cpp | 129 ++++-- main/prepared_send/prepared_send.h | 11 +- temperature_receiver/main.cpp | 31 +- 7 files changed, 721 insertions(+), 41 deletions(-) create mode 100644 experiments/PREPARED_KEEP_WIFI_UP_REPORT.md create mode 100644 main/prepared_keep_wifi_up_5x20_bench.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 94694c6..a624f10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,10 @@ set(AE_EXP_PREPARED_MESSAGE_E2E "" CACHE STRING "No-sleep prepared-message E2E bench (set to 1)") set(AE_EXP_PREPARED_WIFI_CACHE_5X20 "" CACHE STRING "Silent 5x20 prepared Wi-Fi cache bench (set to 1)") -if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1") +set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 "" CACHE STRING + "Silent 5x20 keep-Wi-Fi-up prepared bench (set to 1)") +if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR + AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1") list(APPEND SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent") elseif(AE_EXP_PREPARED_MESSAGE_E2E STREQUAL "1") diff --git a/experiments/PREPARED_KEEP_WIFI_UP_REPORT.md b/experiments/PREPARED_KEEP_WIFI_UP_REPORT.md new file mode 100644 index 0000000..b18c3ff --- /dev/null +++ b/experiments/PREPARED_KEEP_WIFI_UP_REPORT.md @@ -0,0 +1,103 @@ +# Prepared KEEP-WIFI-UP 5×20 (ESP32-C6, silent) + +Hardware: ESP32-C6, ESP-IDF v6.0.2, COM7 +Aether: `exp/esp32c6-wifi-lifecycle-diag` @ `157aadbec8e7b852d0f89274307ff7cb8103e5f7` (**unchanged**) +Firmware: `AE_EXP_PREPARED_KEEP_WIFI_UP_5X20=1`, silent console/log NONE +Client: `prepared_keep_wifi_up_5x20_v1` +Receiver UID: `5aade50f-00d9-4624-b097-e203cdcf1e38` +Block reserve: **20** (unchanged prepared packet path) + +## Hypothesis + +Losses on prepared sends may be caused by **full Wi-Fi teardown + reassociation after every prepared message**. + +## A/B change (bench-only) + +Production `SendPreparedOnce()` unchanged. + +New session API: + +- `BeginPreparedWifiSession()` — init, associate, static IP/ARP once; leave Wi-Fi up +- `SendPreparedPacketOnActiveWifi()` — EncodePacket → socket → sendto → close (no stop/deinit/reconnect) +- `EndPreparedWifiSession()` — stop/deinit/cleanup after message #20 + +Per outer cycle: FULL → PrepareSendMessageBlock(20) → capture Wi-Fi cache → release FULL Æther → Begin session → 20 prepared sends (1 s gap, **no** post-send hold) → End session. + +Socket is still create/close **per message** (extra socket A/B not required). + +## previous + +| Experiment | Delivery | +|---|---| +| static ARP + teardown each send | **29/100** | +| hold 300→600 ms (control) | **6/20 ≈ 30%** | + +## new + +| Experiment | Delivery | +|---|---| +| keep Wi-Fi up (5×20) | **96/100** | + +### DELIVERY detail + +``` +full=6/5 +prepared=96/100 +final=1/1 +missing=4 +duplicates=1 +out_of_order=0 +``` + +Cache flags on recovered prepared: BSSID/IP/DHCP-skip/static-ARP hits=99 (payload flag path active). + +## wifi_session_start + +Measured once per outer cycle (init → association → static IP/ARP ready). Carried on PREPARED#1 as `previous_full_us`. Only 2/5 first-of-burst messages recovered, so n=2: + +``` +raw=[254249, 279549] +min=254249 +median=279549 +max=279549 +n=2 +``` + +(~0.25–0.28 s) + +## prepared send (encode + socket + sendto + close only) + +No 300 ms post-send hold in this mode. + +### FIRST PREPARED (send-only) + +``` +raw=[2467, 2415, 2418, 3785, 2413] +min=2413 +median=2418 +max=3785 +n=5 +``` + +### ALL PREPARED (send-only) + +``` +n=99 +min=2180 +median=2600 +p90=2710 +p99=3785 +max=3785 +``` + +(~2.6 ms median) + +## Verdict + +``` +LOSS LOCATION = WIFI RECONNECT/TEARDOWN +``` + +KEEP-WIFI-UP reaches **96/100** (criterion ~95–100/100). Packet encoding / server / nonce / UDP path is adequate when Wi-Fi stays associated; prior ~30% delivery tracks full teardown/reassociation between each prepared send. + +Do **not** chase further Wi-Fi micro-optimizations (ARP, hold) for this loss class — next product work is avoiding per-message Wi-Fi lifecycle, not changing prepared packets. diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 8b0a222..bb73fbf 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -23,6 +23,11 @@ if(AE_EXP_PREPARED_WIFI_CACHE_5X20) "prepared_wifi_cache_5x20_bench.cpp" "prepared_send/prepared_send.cpp" ) +elseif(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20) + list(APPEND src_list + "prepared_keep_wifi_up_5x20_bench.cpp" + "prepared_send/prepared_send.cpp" + ) elseif(AE_EXP_PREPARED_MESSAGE_E2E) list(APPEND src_list "prepared_message_e2e_bench.cpp" @@ -154,6 +159,7 @@ set(AE_EXP_WIFI_LIFECYCLE_CYCLES "" CACHE STRING "Wi-Fi lifecycle cycles (defaul set(AE_EXP_WIFI_COOLDOWN_MS "" CACHE STRING "Cooldown between cycles, outside timer (ms)") set(AE_EXP_PREPARED_MESSAGE_E2E "" CACHE STRING "No-sleep prepared-message E2E bench (set to 1)") set(AE_EXP_PREPARED_WIFI_CACHE_5X20 "" CACHE STRING "Silent 5x20 prepared Wi-Fi cache bench (set to 1)") +set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 "" CACHE STRING "Silent 5x20 keep-Wi-Fi-up prepared bench (set to 1)") set(BENCH_CLIENT_ID "" CACHE STRING "Bench SelectClient id (default prepared_message_bench_v1)") set(AE_EXP_WIFI_CANONICAL "" CACHE STRING "Canonical ESP-IDF Wi-Fi driver (set to 1)") set(AE_EXP_WIFI_FEAT_AMPDU_OFF "" CACHE STRING "Canonical+bisect: AMPDU off") @@ -191,7 +197,9 @@ ae_exp_define_if_set(AE_EXP_WIFI_LIFECYCLE_CYCLES) ae_exp_define_if_set(AE_EXP_WIFI_COOLDOWN_MS) ae_exp_define_if_set(AE_EXP_PREPARED_MESSAGE_E2E) ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_CACHE_5X20) -if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1") +ae_exp_define_if_set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20) +if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR + AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1") target_compile_definitions(aether PUBLIC "AE_EXP_SILENT=1") target_compile_definitions(${TARGET_NAME} PRIVATE "AE_EXP_SILENT=1") endif() diff --git a/main/prepared_keep_wifi_up_5x20_bench.cpp b/main/prepared_keep_wifi_up_5x20_bench.cpp new file mode 100644 index 0000000..bdacd0d --- /dev/null +++ b/main/prepared_keep_wifi_up_5x20_bench.cpp @@ -0,0 +1,473 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Silent 5x20 prepared KEEP-WIFI-UP experiment (ESP32-C6): + * 1 registration, 5 FULL cycles each preparing 20 messages. + * Wi-Fi associates once per outer cycle; 20 UDP sends reuse the association. + * Per-message timing is encode+sendto only (no post-send hold). + * All timings travel in binary application payloads (no UART results). + */ + +#include +#include + +#include "aether/all.h" +#include "aether/ae_exp_wifi.h" +#include "aether/config.h" +#include "aether/env.h" +#include "bench_payload.h" +#include "prepared_send/prepared_send.h" + +#if defined(ESP_PLATFORM) +# include +# include +# include +# include +# include +# include +#endif + +using namespace std::chrono_literals; + +namespace temp_sensor { +namespace { + +static constexpr auto kParentUid = + ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); + +#ifndef BENCH_CLIENT_ID +# define BENCH_CLIENT_ID "prepared_keep_wifi_up_5x20_v1" +#endif +static constexpr char const* kBenchClientId = BENCH_CLIENT_ID; + +#if defined(SERVICE_UID) +static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID); +#else +static constexpr auto kServiceUid = + ae::Uid::FromString("3d284a4f-ebb4-451e-a2c5-aecb0d647a45"); +#endif + +static constexpr int kOuterCycles = 5; +static constexpr int kPreparedPerCycle = 20; +static constexpr int kPreparedGapMs = 1000; + +#if defined(ESP_PLATFORM) +static const auto kWifiInit = ae::WiFiInit{ + std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}}, + {}, +}; + +static bool g_had_aether_app = false; + +static void PreConstructCleanup() { + if (!g_had_aether_app) { + return; + } +# if !AE_WIFI_USE_FULL_DEINIT + esp_netif_deinit(); + esp_event_loop_delete_default(); +# endif +} + +static std::int64_t NowUs() { return esp_timer_get_time(); } +#else +static std::int64_t NowUs() { return 0; } +#endif + +enum class Phase : std::uint8_t { + kRegister, + kFullCycle, + kPrepared, + kFinal, + kDone, +}; + +static std::shared_ptr g_app; +static ae::Client::ptr g_client; +static std::unique_ptr g_stream; +static ae::Subscription g_select_sub; +static ae::Subscription g_stream_sub; +static ae::Subscription g_write_sub; + +static Phase g_phase = Phase::kRegister; +static bool g_registration_pending = false; +static bool g_write_armed = false; +static bool g_done = false; + +static int g_outer = 0; +static int g_prepared_index = 0; +static bool g_prepared_waiting_gap = false; +#if defined(ESP_PLATFORM) +static TickType_t g_prepared_gap_until = 0; +#endif + +static std::uint16_t g_seq = 0; +static std::uint32_t g_registration_us = 0; +static std::uint32_t g_last_full_us = 0; +static std::uint32_t g_last_prepared_us = 0; +static std::uint32_t g_last_session_start_us = 0; +static std::uint32_t g_pending_full_us = 0; +static bool g_have_pending_full = false; +static std::uint8_t g_sticky_cache_flags = 0; +static bool g_wifi_session_open = false; + +static std::int64_t g_t0 = 0; + +static void ReleaseApp() { + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + g_app.reset(); +} + +static std::uint16_t NextSeq() { return ++g_seq; } + +static ae::DataBuffer MakeFullPayload(int outer) { + bench::Payload p{}; + p.type = static_cast(bench::MsgType::kFull); + p.outer_cycle = static_cast(outer); + p.prepared_index = 0; + p.sequence_global = NextSeq(); + p.registration_us = (outer == 1) ? g_registration_us : 0; + p.previous_full_us = g_have_pending_full ? g_pending_full_us : 0; + p.previous_prepared_us = g_last_prepared_us; + p.cache_flags = g_sticky_cache_flags; + g_have_pending_full = false; + return bench::Encode(p); +} + +static ae::DataBuffer MakePreparedPayload(int outer, int index) { + bench::Payload p{}; + p.type = static_cast(bench::MsgType::kPrepared); + p.outer_cycle = static_cast(outer); + p.prepared_index = static_cast(index); + p.sequence_global = NextSeq(); + // For index==1: previous_full_us carries wifi session start µs. + p.previous_full_us = (index == 1) ? g_last_session_start_us : 0; + p.previous_prepared_us = (index == 1) ? 0 : g_last_prepared_us; + // cache_flags describe the Wi-Fi session (set at Begin). + p.cache_flags = g_sticky_cache_flags; + return bench::Encode(p); +} + +static ae::DataBuffer MakeFinalPayload() { + bench::Payload p{}; + p.type = static_cast(bench::MsgType::kFinal); + p.outer_cycle = kOuterCycles; + p.prepared_index = kPreparedPerCycle; + p.sequence_global = NextSeq(); + p.registration_us = g_registration_us; + p.previous_full_us = g_have_pending_full ? g_pending_full_us : g_last_full_us; + p.previous_prepared_us = g_last_prepared_us; + p.cache_flags = g_sticky_cache_flags; + return bench::Encode(p); +} + +static void ConstructAether() { +#if defined(ESP_PLATFORM) + PreConstructCleanup(); +#endif + g_had_aether_app = true; + g_app = ae::AetherApp::Construct( + ae::AetherAppContext{} +#if AE_DISTILLATION && defined(ESP_PLATFORM) + .AddAdapterFactory([&](ae::AetherAppContext const& ctx) { + return ae::WifiAdapter::ptr::Create( + ae::CreateWith{ctx.domain()}.with_id( + ae::GlobalId::kWiFiAdapter), + ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit); + }) +#endif + ); +} + +static void OnRegisterReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + g_app->aether().Save(); + g_app->Exit(0); +} + +static void DoFullWrite() { + if (g_write_armed) { + return; + } + g_write_armed = true; + auto payload = MakeFullPayload(g_outer); + auto& wa = g_stream->Write(std::move(payload)); + g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) { + if (st != ae::WriteAction::Status::kSuccess) { + g_app->Exit(1); + return; + } + if (!prepared_send::ExportPreparedSendBlock(g_client, kServiceUid, + kPreparedPerCycle)) { + g_app->Exit(1); + return; + } + if (!prepared_send::HasPreparedSendBlock() || + prepared_send::PreparedMessageLeft() != + static_cast(kPreparedPerCycle)) { + g_app->Exit(1); + return; + } + // Export association/IP/gateway-MAC into local prepared cache while FULL + // Wi-Fi is still up so prepared #1 can take the fast path. + (void)prepared_send::CapturePreparedWifiCacheFromActiveConnection(); + g_app->aether().Save(); + g_app->Exit(0); + }); +} + +static void MaybeFullWrite() { + if (!g_stream || g_write_armed) { + return; + } + if (!g_stream->stream_info().is_writable) { + return; + } + DoFullWrite(); +} + +static void OnFullClientReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + auto client = g_client.Load(); + g_stream = std::make_unique(*g_app, client, kServiceUid, + ae::P2pPortHandle{}); + g_stream_sub = + g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); }); + MaybeFullWrite(); +} + +static void StartRegister() { + g_phase = Phase::kRegister; + g_write_armed = false; + g_t0 = NowUs(); + ConstructAether(); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + g_app->Exit(1); + return; + } + OnRegisterReady(std::move(res).value()); + }); +} + +static void StartFullCycle(int outer) { + g_phase = Phase::kFullCycle; + g_outer = outer; + g_write_armed = false; + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + g_t0 = NowUs(); + ConstructAether(); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + g_app->Exit(1); + return; + } + OnFullClientReady(std::move(res).value()); + }); +} + +static void StartPreparedPhase() { + g_phase = Phase::kPrepared; + g_prepared_index = 1; + g_prepared_waiting_gap = false; + g_sticky_cache_flags = 0; + g_wifi_session_open = false; + g_last_session_start_us = 0; +#if defined(ESP_PLATFORM) + prepared_send::ReleaseFullAetherWifiForHotPath(); + if (!prepared_send::BeginPreparedWifiSession()) { + // Session failed — leave session closed; sends will report wifi-failed. + g_wifi_session_open = false; + } else { + g_wifi_session_open = true; + g_last_session_start_us = prepared_send::LastWifiSessionStartUs(); + g_sticky_cache_flags = prepared_send::LastSendCacheFlags(); + } +#endif +} + +static void DoFinalWrite() { + if (g_write_armed) { + return; + } + g_write_armed = true; + auto payload = MakeFinalPayload(); + auto& wa = g_stream->Write(std::move(payload)); + g_write_sub = + wa.status_event().Subscribe([](ae::WriteAction::Status) { g_app->Exit(0); }); +} + +static void MaybeFinalWrite() { + if (!g_stream || g_write_armed) { + return; + } + if (!g_stream->stream_info().is_writable) { + return; + } + DoFinalWrite(); +} + +static void OnFinalClientReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + auto client = g_client.Load(); + g_stream = std::make_unique(*g_app, client, kServiceUid, + ae::P2pPortHandle{}); + g_stream_sub = + g_stream->stream_update_event().Subscribe([]() { MaybeFinalWrite(); }); + MaybeFinalWrite(); +} + +static void StartFinal() { + g_phase = Phase::kFinal; + g_write_armed = false; + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + ConstructAether(); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + g_app->Exit(1); + return; + } + OnFinalClientReady(std::move(res).value()); + }); +} + +void BeginAppMainTiming() {} +void FinalizeCycleBeforeSleep() {} +#if defined(ESP_PLATFORM) +void EnterDeepSleep() {} +#endif + +void setup() { +#if defined(ESP_PLATFORM) + nvs_flash_init(); + prepared_send::InvalidatePreparedWifiCache(); +#endif + g_done = false; + g_seq = 0; + g_registration_pending = true; + g_last_prepared_us = 0; + g_have_pending_full = false; + g_sticky_cache_flags = 0; +} + +void loop() { + if (g_done) { + return; + } + + if (g_registration_pending) { + g_registration_pending = false; + StartRegister(); + return; + } + + if (g_phase == Phase::kPrepared) { +#if defined(ESP_PLATFORM) + if (g_prepared_waiting_gap) { + if (xTaskGetTickCount() < g_prepared_gap_until) { + vTaskDelay(pdMS_TO_TICKS(20)); + return; + } + g_prepared_waiting_gap = false; + } +#endif + + if (g_prepared_index > kPreparedPerCycle) { +#if defined(ESP_PLATFORM) + if (g_wifi_session_open) { + prepared_send::EndPreparedWifiSession(); + g_wifi_session_open = false; + } +#endif + if (g_outer < kOuterCycles) { + StartFullCycle(g_outer + 1); + } else { + StartFinal(); + } + return; + } + + int const i = g_prepared_index; + auto payload = MakePreparedPayload(g_outer, i); + auto const t0 = NowUs(); +#if defined(ESP_PLATFORM) + auto const status = + prepared_send::SendPreparedPacketOnActiveWifi(payload); +#else + auto const status = prepared_send::HotSendStatus::kUnsupported; +#endif + auto const us = static_cast(NowUs() - t0); + (void)status; + g_last_prepared_us = us; + + ++g_prepared_index; + if (g_prepared_index <= kPreparedPerCycle) { +#if defined(ESP_PLATFORM) + g_prepared_waiting_gap = true; + g_prepared_gap_until = + xTaskGetTickCount() + pdMS_TO_TICKS(kPreparedGapMs); +#endif + } + return; + } + + if (!g_app) { + return; + } + + if (!g_app->IsExited()) { + auto t = g_app->Update(ae::Now()); + g_app->WaitUntil(t); + return; + } + + if (g_phase == Phase::kRegister) { + ReleaseApp(); + g_registration_us = static_cast(NowUs() - g_t0); + StartFullCycle(1); + return; + } + + if (g_phase == Phase::kFullCycle) { + ReleaseApp(); + auto const full_us = static_cast(NowUs() - g_t0); + g_last_full_us = full_us; + g_pending_full_us = full_us; + g_have_pending_full = true; + StartPreparedPhase(); + return; + } + + if (g_phase == Phase::kFinal) { + ReleaseApp(); + g_phase = Phase::kDone; + g_done = true; + } +} + +} // namespace +} // namespace temp_sensor + +void setup() { temp_sensor::setup(); } +void loop() { temp_sensor::loop(); } diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index 9e40ef9..4e711fa 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -35,6 +35,7 @@ # include # include # include +# include # include # include # include @@ -55,6 +56,8 @@ namespace temp_sensor::prepared_send { +void ClearPreparedSendBlock(); + #if defined(ESP_PLATFORM) && !defined(AE_EXP_SILENT) static constexpr char const* kTag = "prepared-send"; # define PS_LOGI(...) ESP_LOGI(kTag, __VA_ARGS__) @@ -93,6 +96,8 @@ static constexpr char const* kTag = "prepared-send"; #endif static std::uint8_t g_last_send_cache_flags = 0; +static bool g_prepared_wifi_session_active = false; +static std::uint32_t g_last_wifi_session_start_us = 0; #if defined(ESP_PLATFORM) static RTC_NOINIT_ATTR ae::prepared_packet::PreparedSendMessageBlock @@ -568,12 +573,62 @@ bool EnsureWifiConnectedForHotPath() { return true; } +// Encode + socket/sendto/close only. Does not touch Wi-Fi lifetime. +HotSendStatus EncodeAndUdpSend(ae::DataBuffer const& payload) { + if (!g_prepared_send_message_block.is_valid()) { + return HotSendStatus::kNoPreparedBlock; + } + + if (g_prepared_send_message_block.Resolve()->message_left == 0) { + return HotSendStatus::kNonceExhausted; + } + + ae::DataBuffer packet; + auto encode_result = ae::prepared_packet::EncodePacket( + g_prepared_send_message_block, payload, packet); + + if (!encode_result) { + ClearPreparedSendBlock(); + return HotSendStatus::kEncodeFailed; + } + + auto const resolved_block = g_prepared_send_message_block.Resolve(); + auto endpoint = resolved_block->endpoint; + + sockaddr_storage dest_storage{}; + socklen_t dest_len = 0; + if (!FillUdpDestination(endpoint, reinterpret_cast(&dest_storage), + &dest_len)) { + return HotSendStatus::kSendFailed; + } + + int sock = socket( + endpoint.address.Index() == ae::AddrVersion::kIpV6 ? AF_INET6 : AF_INET, + SOCK_DGRAM, IPPROTO_IP); + if (sock < 0) { + return HotSendStatus::kSendFailed; + } + + auto sent = sendto(sock, packet.data(), packet.size(), 0, + reinterpret_cast(&dest_storage), dest_len); + close(sock); + + if (sent != static_cast(packet.size())) { + return HotSendStatus::kSendFailed; + } + return HotSendStatus::kSent; +} + #else bool EnsureWifiConnectedForHotPath() { return true; } void CleanupHotPathWifiRuntime() {} +HotSendStatus EncodeAndUdpSend(ae::DataBuffer const&) { + return HotSendStatus::kUnsupported; +} + #endif } // namespace @@ -725,38 +780,9 @@ HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload) { auto fail_after_wifi = ae_defer_at[] { CleanupHotPathWifiRuntime(); }; - ae::DataBuffer packet; - auto encode_result = ae::prepared_packet::EncodePacket( - g_prepared_send_message_block, payload, packet); - - if (!encode_result) { - ClearPreparedSendBlock(); - return HotSendStatus::kEncodeFailed; - } - - auto const resolved_block = g_prepared_send_message_block.Resolve(); - auto endpoint = resolved_block->endpoint; - - sockaddr_storage dest_storage{}; - socklen_t dest_len = 0; - if (!FillUdpDestination(endpoint, reinterpret_cast(&dest_storage), - &dest_len)) { - return HotSendStatus::kSendFailed; - } - - int sock = socket( - endpoint.address.Index() == ae::AddrVersion::kIpV6 ? AF_INET6 : AF_INET, - SOCK_DGRAM, IPPROTO_IP); - if (sock < 0) { - return HotSendStatus::kSendFailed; - } - - auto sent = sendto(sock, packet.data(), packet.size(), 0, - reinterpret_cast(&dest_storage), dest_len); - close(sock); - - if (sent != static_cast(packet.size())) { - return HotSendStatus::kSendFailed; + auto const status = EncodeAndUdpSend(payload); + if (status != HotSendStatus::kSent) { + return status; } # ifndef AETHER_PREPARED_POST_SEND_HOLD_MS @@ -772,6 +798,45 @@ HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload) { #endif } +#if defined(ESP_PLATFORM) +void EndPreparedWifiSession(); + +bool BeginPreparedWifiSession() { + if (g_prepared_wifi_session_active) { + EndPreparedWifiSession(); + } + g_last_wifi_session_start_us = 0; + auto const t0 = esp_timer_get_time(); + if (!EnsureWifiConnectedForHotPath()) { + return false; + } + auto const elapsed = esp_timer_get_time() - t0; + g_last_wifi_session_start_us = + elapsed < 0 ? 0 + : static_cast( + elapsed > static_cast( + std::numeric_limits::max()) + ? std::numeric_limits::max() + : elapsed); + g_prepared_wifi_session_active = true; + return true; +} + +HotSendStatus SendPreparedPacketOnActiveWifi(ae::DataBuffer const& payload) { + if (!g_prepared_wifi_session_active) { + return HotSendStatus::kWifiFailed; + } + return EncodeAndUdpSend(payload); +} + +void EndPreparedWifiSession() { + CleanupHotPathWifiRuntime(); + g_prepared_wifi_session_active = false; +} + +std::uint32_t LastWifiSessionStartUs() { return g_last_wifi_session_start_us; } +#endif + HotSendStatus TryHotWakePreparedSend( [[maybe_unused]] std::string const& temperature) { #if defined(ESP_PLATFORM) diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h index 6d238ce..b284e96 100644 --- a/main/prepared_send/prepared_send.h +++ b/main/prepared_send/prepared_send.h @@ -44,10 +44,19 @@ ae::DataBuffer MakeBenchPayload(std::string_view kind, int sequence); // Shared encode + Wi-Fi + UDP + post-send hold + Wi-Fi runtime cleanup. HotSendStatus SendPreparedOnce(ae::DataBuffer const& payload); -// Last send cache usage bits (bench CacheFlags). Valid after SendPreparedOnce. +// Last send cache usage bits (bench CacheFlags). Valid after SendPreparedOnce / +// BeginPreparedWifiSession / SendPreparedPacketOnActiveWifi. std::uint8_t LastSendCacheFlags(); #if defined(ESP_PLATFORM) +// Bench-only: keep one Wi-Fi association across many prepared UDP sends. +// Does not replace production SendPreparedOnce(). +bool BeginPreparedWifiSession(); +HotSendStatus SendPreparedPacketOnActiveWifi(ae::DataBuffer const& payload); +void EndPreparedWifiSession(); +// Duration of the last successful BeginPreparedWifiSession() (µs). +std::uint32_t LastWifiSessionStartUs(); + // No-sleep bench: AetherApp release may leave ESP-IDF Wi-Fi/netif up. void ReleaseFullAetherWifiForHotPath(); diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp index 9ecb25b..609da52 100644 --- a/temperature_receiver/main.cpp +++ b/temperature_receiver/main.cpp @@ -43,6 +43,8 @@ std::vector> g_streams; std::uint32_t g_registration_us = 0; std::array g_full_us{}; std::array g_full_have{}; +std::array g_session_us{}; +std::array g_session_have{}; std::array, kOuter> g_prep_us{}; std::array, kOuter> g_prep_have{}; std::array, kOuter> g_prep_flags{}; @@ -74,6 +76,7 @@ std::uint32_t Percentile(std::vector v, int pct) { void PrintSummary() { std::vector fulls; + std::vector sessions; std::vector firsts; std::vector warms; std::vector all; @@ -88,6 +91,9 @@ void PrintSummary() { if (g_full_have[static_cast(o)]) { fulls.push_back(g_full_us[static_cast(o)]); } + if (g_session_have[static_cast(o)]) { + sessions.push_back(g_session_us[static_cast(o)]); + } for (int i = 0; i < kPreparedPer; ++i) { if (!g_prep_have[static_cast(o)][static_cast(i)]) { continue; @@ -157,11 +163,13 @@ void PrintSummary() { std::cout << " time_us=" << g_registration_us << "\n"; std::cout << "FULL\n"; print_vec(" ", fulls); - std::cout << "FIRST PREPARED\n"; + std::cout << "WIFI_SESSION_START\n"; + print_vec(" ", sessions); + std::cout << "FIRST PREPARED (send-only)\n"; print_vec(" ", firsts); - std::cout << "WARM PREPARED\n"; + std::cout << "WARM PREPARED (send-only)\n"; print_vec(" ", warms); - std::cout << "ALL PREPARED\n"; + std::cout << "ALL PREPARED (send-only)\n"; print_vec(" ", all); std::cout << "DELIVERY\n"; std::cout << " full=" << g_full_recv << "/" << kExpectedFull << "\n"; @@ -184,7 +192,8 @@ void PrintSummary() { std::cout << " used_static_arp hits=" << static_arp_hits << "\n"; std::cout << " arp_fallback hits=" << arp_fallback_hits << "\n"; std::cout << " wifi_fallback hits=" << wifi_fallback_hits << "\n"; - std::cout << "NOTE prepared timing includes AETHER_PREPARED_POST_SEND_HOLD_MS=300\n"; + std::cout << "NOTE prepared_send_us is encode+sendto only (no post-send hold in keep-wifi-up)\n"; + std::cout << "NOTE previous_full_us on PREPARED#1 carries wifi_session_start_us\n"; std::cout << "BENCH_DONE\n"; std::cout.flush(); } @@ -245,6 +254,11 @@ void OnMessage(ae::Uid sender, ae::DataBuffer const& data) { ++g_prep_recv; int const o = static_cast(p.outer_cycle) - 1; int const i = static_cast(p.prepared_index) - 1; + if (o >= 0 && o < kOuter && p.prepared_index == 1 && + p.previous_full_us != 0) { + g_session_us[static_cast(o)] = p.previous_full_us; + g_session_have[static_cast(o)] = true; + } // previous_prepared_us is timing of prepared_index-1 if (o >= 0 && o < kOuter && p.prepared_index >= 2) { int const pi = static_cast(p.prepared_index) - 2; @@ -256,10 +270,15 @@ void OnMessage(ae::Uid sender, ae::DataBuffer const& data) { p.cache_flags; } } + if (o >= 0 && o < kOuter && i >= 0 && i < kPreparedPer) { + g_prep_flags[static_cast(o)][static_cast(i)] = + p.cache_flags; + } std::cout << ae::Format( - "RECV PREPARED outer={} idx={} seq={} prev_us={} flags={} ts={}\n", + "RECV PREPARED outer={} idx={} seq={} prev_us={} session_us={} flags={} " + "ts={}\n", p.outer_cycle, p.prepared_index, p.sequence_global, - p.previous_prepared_us, p.cache_flags, ts); + p.previous_prepared_us, p.previous_full_us, p.cache_flags, ts); } else if (type == temp_sensor::bench::MsgType::kFinal) { ++g_final_recv; if (p.previous_full_us != 0) { From 14d1bb785e77ababfb2c6b0273e35b7fff461b4a Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Fri, 28 Aug 2026 23:07:24 -0700 Subject: [PATCH 23/32] Add silent Wi-Fi single-factor prepared bisect and 13x20 results. Fix harness hang (4MB flash, deferred work after Update, no nested Write) so B1 smoke and the full silent one-factor table can complete. Co-authored-by: Cursor --- CMakeLists.txt | 15 +- .../PREPARED_WIFI_SINGLE_FACTOR_BISECT.md | 68 ++ .../PREPARED_WIFI_SINGLE_FACTOR_BISECT.tsv | 14 + main/CMakeLists.txt | 15 +- main/bench_payload.h | 151 ++++- main/prepared_send/prepared_send.cpp | 382 +++++++++++ main/prepared_send/prepared_send.h | 48 ++ ...epared_wifi_single_factor_bisect_bench.cpp | 640 ++++++++++++++++++ temperature_receiver/main.cpp | 451 ++++++------ 9 files changed, 1552 insertions(+), 232 deletions(-) create mode 100644 experiments/PREPARED_WIFI_SINGLE_FACTOR_BISECT.md create mode 100644 experiments/PREPARED_WIFI_SINGLE_FACTOR_BISECT.tsv create mode 100644 main/prepared_wifi_single_factor_bisect_bench.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a624f10..4e79a84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,8 +31,19 @@ set(AE_EXP_PREPARED_WIFI_CACHE_5X20 "" CACHE STRING "Silent 5x20 prepared Wi-Fi cache bench (set to 1)") set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 "" CACHE STRING "Silent 5x20 keep-Wi-Fi-up prepared bench (set to 1)") -if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR - AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1") +set(AE_EXP_PREPARED_WIFI_BISECT "" CACHE STRING + "Silent single-factor prepared Wi-Fi bisect (set to 1)") +set(AE_EXP_BISECT_CONSOLE "" CACHE STRING + "Bisect USB stage markers / console (set to 1; disables silent)") +set(AE_EXP_BISECT_SMOKE "" CACHE STRING + "Bisect smoke: B1 only, 2 prepared (set to 1)") +if(AE_EXP_BISECT_CONSOLE STREQUAL "1" AND + AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1") + list(APPEND SDKCONFIG_DEFAULTS + "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.bench") +elseif(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR + AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1" OR + AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1") list(APPEND SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent") elseif(AE_EXP_PREPARED_MESSAGE_E2E STREQUAL "1") diff --git a/experiments/PREPARED_WIFI_SINGLE_FACTOR_BISECT.md b/experiments/PREPARED_WIFI_SINGLE_FACTOR_BISECT.md new file mode 100644 index 0000000..e56011d --- /dev/null +++ b/experiments/PREPARED_WIFI_SINGLE_FACTOR_BISECT.md @@ -0,0 +1,68 @@ +# Prepared Wi-Fi single-factor bisect (ESP32-C6, silent 13×20) + +Hardware: ESP32-C6, ESP-IDF v6.0.2, COM7, flash size **4MB** +Aether: `exp/esp32c6-wifi-lifecycle-diag` @ `157aadbec8e7b852d0f89274307ff7cb8103e5f7` (**unchanged**) +Firmware: `AE_EXP_PREPARED_WIFI_BISECT=1`, silent console (`CONFIG_ESP_CONSOLE_NONE`, log level 0) +Receiver: `prepared_wifi_cache_rx_v1` / UID `5aade50f-00d9-4624-b097-e203cdcf1e38` +Policy: Wi-Fi 4 b/g/n, `WIFI_PS_NONE`, DHCP+GOT_IP baseline; 200 ms pre-delay (except B0); 300 ms post-hold; 1 s prepared gap. + +## Hang diagnosis (pre-full-run) + +Silent flash previously showed **0 FULL / 0 PREPARED** for ~22 minutes. + +Root causes fixed (harness / flash only; bisect factors unchanged): + +1. **Flash size mismatch**: flashing with `--flash-size 2MB` while the app partition is `0x300000` caused a boot loop (`partition … exceeds flash chip size`). Chip is 4MB → use `--flash-size 4MB`. +2. **Deferred SelectClient/Write work after `WaitUntil`**: callbacks armed deferred stages that only ran on the next loop wake, stalling after `SELECT_BEGIN`. Fixed by running deferred work immediately after `Update()`, before `WaitUntil`. +3. **No nested `Write()` from Write/Update callbacks**: META/cache freeze and Prepare/Save/Release stay deferred into the main loop/state machine. +4. **200 ms settle** after `ReleaseFullAetherWifiForHotPath()` before the first bisect STA init. + +Smoke (B1 × 2 prepared, temporary USB stage markers) reached `BISECT_VARIANT_DONE B1` with **2 PREPARED** on the receiver, then console/logging were turned back off for the silent full run. + +## Results (one silent 13×20) + +All variants: `WifiReady=Encode=Sendto=Nonce=20` (device completed every reconnect/encode/sendto; losses are delivery). + +| Variant | Single change | Delivered/20 | Missing | Median_ms | Verdict | +|---|---|---:|---:|---:|---| +| B0 | no cache, 0 ms pre-delay | 15/20 | 5 | 2170 | DEGRADES | +| B1 | no cache, 200 ms pre-delay | 18/20 | 2 | 2390 | OK | +| C1 | BSSID only | 20/20 | 0 | 2390 | OK | +| C2 | CHANNEL only | 20/20 | 0 | 2390 | OK | +| C3 | BSSID+CHANNEL | 19/20 | 1 | 2390 | OK | +| C4 | FAST_SCAN only | 18/20 | 2 | 2390 | OK | +| C5 | STATIC_IP only | 17/20 | 3 | 850 | DEGRADES | +| C6 | STATIC_IP+ARP (dep C5) | 20/20 | 0 | 850 | OK | +| C7 | BSSID+STATIC_IP | 19/20 | 1 | 850 | OK | +| C8 | CHANNEL+STATIC_IP | 20/20 | 0 | 850 | OK | +| P1 | PS_MAX_MODEM | 20/20 | 0 | 2400 | OK | +| P2 | AMPDU_OFF | 18/20 | 2 | 2390 | OK | +| P3 | FIXED_1M | 20/20 | 0 | 2430 | OK | + +Totals: prepared delivered **244/260**; final **1/1**; out_of_order **0**. +(Receiver also counted 14 FULL vs 13 variants — one extra FULL from registration/start sequencing; not used for factor verdicts.) + +## CHANNEL HYPOTHESIS + +``` +B1 no cache = 18/20 +C1 BSSID only = 20/20 +C2 channel only = 20/20 +C3 BSSID+channel = 19/20 +C7 BSSID+static IP = 19/20 +C8 channel+static IP = 20/20 +Does cached channel independently correlate with loss? NO +``` + +Channel match counts (requested vs actual when channel factor set): C2 19/0, C3 19/0, C8 19/0 (mismatches 0). + +## Notes + +- Static-IP variants (C5–C8) show ~850 ms median vs ~2.4 s for DHCP reconnects — expected speedup; C5 alone still loses more packets than C6 (ARP) / C8. +- B0 (0 ms pre-delay) is worse than B1 (200 ms) → keep 200 ms pre-delay in main tests. +- No production Wi-Fi combo invented from this table; report only. + +## Artifacts + +- Receiver log: `experiments/prepared_wifi_bisect_full_rx.log` +- TSV: `experiments/PREPARED_WIFI_SINGLE_FACTOR_BISECT.tsv` diff --git a/experiments/PREPARED_WIFI_SINGLE_FACTOR_BISECT.tsv b/experiments/PREPARED_WIFI_SINGLE_FACTOR_BISECT.tsv new file mode 100644 index 0000000..3497f7c --- /dev/null +++ b/experiments/PREPARED_WIFI_SINGLE_FACTOR_BISECT.tsv @@ -0,0 +1,14 @@ +Variant Single change Delivered/20 Missing Median_ms WifiReady Encode Sendto Nonce Verdict +B0 no cache, 0ms pre-delay 15 5 2170 20 20 20 20 DEGRADES +B1 no cache, 200ms pre-delay 18 2 2390 20 20 20 20 OK +C1 BSSID only 20 0 2390 20 20 20 20 OK +C2 CHANNEL only 20 0 2390 20 20 20 20 OK +C3 BSSID+CHANNEL 19 1 2390 20 20 20 20 OK +C4 FAST_SCAN only 18 2 2390 20 20 20 20 OK +C5 STATIC_IP only 17 3 850 20 20 20 20 DEGRADES +C6 STATIC_IP+ARP (dep C5) 20 0 850 20 20 20 20 OK +C7 BSSID+STATIC_IP 19 1 850 20 20 20 20 OK +C8 CHANNEL+STATIC_IP 20 0 850 20 20 20 20 OK +P1 PS_MAX_MODEM 20 0 2400 20 20 20 20 OK +P2 AMPDU_OFF 18 2 2390 20 20 20 20 OK +P3 FIXED_1M 20 0 2430 20 20 20 20 OK diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index bb73fbf..f0e6ea6 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -28,6 +28,11 @@ elseif(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20) "prepared_keep_wifi_up_5x20_bench.cpp" "prepared_send/prepared_send.cpp" ) +elseif(AE_EXP_PREPARED_WIFI_BISECT) + list(APPEND src_list + "prepared_wifi_single_factor_bisect_bench.cpp" + "prepared_send/prepared_send.cpp" + ) elseif(AE_EXP_PREPARED_MESSAGE_E2E) list(APPEND src_list "prepared_message_e2e_bench.cpp" @@ -160,6 +165,9 @@ set(AE_EXP_WIFI_COOLDOWN_MS "" CACHE STRING "Cooldown between cycles, outside ti set(AE_EXP_PREPARED_MESSAGE_E2E "" CACHE STRING "No-sleep prepared-message E2E bench (set to 1)") set(AE_EXP_PREPARED_WIFI_CACHE_5X20 "" CACHE STRING "Silent 5x20 prepared Wi-Fi cache bench (set to 1)") set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 "" CACHE STRING "Silent 5x20 keep-Wi-Fi-up prepared bench (set to 1)") +set(AE_EXP_PREPARED_WIFI_BISECT "" CACHE STRING "Silent single-factor prepared Wi-Fi bisect (set to 1)") +set(AE_EXP_BISECT_CONSOLE "" CACHE STRING "Bisect USB stage markers (set to 1)") +set(AE_EXP_BISECT_SMOKE "" CACHE STRING "Bisect smoke B1/2 prepared (set to 1)") set(BENCH_CLIENT_ID "" CACHE STRING "Bench SelectClient id (default prepared_message_bench_v1)") set(AE_EXP_WIFI_CANONICAL "" CACHE STRING "Canonical ESP-IDF Wi-Fi driver (set to 1)") set(AE_EXP_WIFI_FEAT_AMPDU_OFF "" CACHE STRING "Canonical+bisect: AMPDU off") @@ -198,8 +206,13 @@ ae_exp_define_if_set(AE_EXP_WIFI_COOLDOWN_MS) ae_exp_define_if_set(AE_EXP_PREPARED_MESSAGE_E2E) ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_CACHE_5X20) ae_exp_define_if_set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20) +ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_BISECT) +ae_exp_define_if_set(AE_EXP_BISECT_CONSOLE) +ae_exp_define_if_set(AE_EXP_BISECT_SMOKE) if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR - AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1") + AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1" OR + (AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1" AND + NOT AE_EXP_BISECT_CONSOLE STREQUAL "1")) target_compile_definitions(aether PUBLIC "AE_EXP_SILENT=1") target_compile_definitions(${TARGET_NAME} PRIVATE "AE_EXP_SILENT=1") endif() diff --git a/main/bench_payload.h b/main/bench_payload.h index 5327497..4049e32 100644 --- a/main/bench_payload.h +++ b/main/bench_payload.h @@ -1,7 +1,7 @@ /* * Copyright 2026 Aethernet Inc. * - * Compact binary benchmark payload for prepared Wi-Fi cache 5x20 experiment. + * Compact binary benchmark payloads for prepared Wi-Fi experiments. * All multi-byte fields are little-endian. */ @@ -15,6 +15,7 @@ namespace temp_sensor::bench { static constexpr std::uint8_t kMagic = 0xAE; +static constexpr std::uint8_t kBisectMagic = 0xAF; enum class MsgType : std::uint8_t { kFull = 1, @@ -32,6 +33,49 @@ enum class CacheFlags : std::uint8_t { kWifiFallback = 1 << 5, }; +enum class BisectMsgType : std::uint8_t { + kFull = 1, + kPrepared = 2, + kFinal = 3, + kMeta = 4, + kVariantSummary = 5, +}; + +enum class BisectVariant : std::uint8_t { + kB0 = 0, + kB1, + kC1, + kC2, + kC3, + kC4, + kC5, + kC6, + kC7, + kC8, + kP1, + kP2, + kP3, + kCount, +}; + +enum class BisectFactorBits : std::uint8_t { + kBssid = 1 << 0, + kChannel = 1 << 1, + kFastScan = 1 << 2, + kStaticIp = 1 << 3, + kStaticArp = 1 << 4, + kPsMaxModem = 1 << 5, + kAmpduOff = 1 << 6, + kFixed1M = 1 << 7, +}; + +enum class BisectStatusBits : std::uint8_t { + kWifiReady = 1 << 0, + kEncodeOk = 1 << 1, + kSendtoOk = 1 << 2, + kChannelMatch = 1 << 3, +}; + #pragma pack(push, 1) struct Payload { std::uint8_t magic{kMagic}; @@ -44,9 +88,98 @@ struct Payload { std::uint32_t previous_prepared_us{0}; std::uint8_t cache_flags{0}; }; + +struct BisectPayload { + std::uint8_t magic{kBisectMagic}; + std::uint8_t type{0}; + std::uint8_t variant_id{0}; + std::uint8_t prepared_index{0}; + std::uint16_t sequence_global{0}; + std::uint32_t time_us{0}; + std::uint32_t aux_us{0}; + std::uint8_t requested_channel{0}; + std::uint8_t actual_channel{0}; + std::uint8_t status_flags{0}; + std::uint8_t factor_bits{0}; + std::uint8_t wifi_ready_count{0}; + std::uint8_t encode_count{0}; + std::uint8_t sendto_count{0}; + std::uint8_t nonce_consumed{0}; + std::uint32_t cached_ip{0}; + std::uint8_t cached_bssid[6]{}; + std::uint8_t cached_channel{0}; + std::uint8_t pre_delay_ms{0}; +}; #pragma pack(pop) static_assert(sizeof(Payload) == 19, "bench payload size"); +static_assert(sizeof(BisectPayload) == 34, "bisect payload size"); + +inline char const* BisectVariantName(std::uint8_t id) { + switch (static_cast(id)) { + case BisectVariant::kB0: + return "B0"; + case BisectVariant::kB1: + return "B1"; + case BisectVariant::kC1: + return "C1"; + case BisectVariant::kC2: + return "C2"; + case BisectVariant::kC3: + return "C3"; + case BisectVariant::kC4: + return "C4"; + case BisectVariant::kC5: + return "C5"; + case BisectVariant::kC6: + return "C6"; + case BisectVariant::kC7: + return "C7"; + case BisectVariant::kC8: + return "C8"; + case BisectVariant::kP1: + return "P1"; + case BisectVariant::kP2: + return "P2"; + case BisectVariant::kP3: + return "P3"; + default: + return "?"; + } +} + +inline char const* BisectVariantChange(std::uint8_t id) { + switch (static_cast(id)) { + case BisectVariant::kB0: + return "no cache, 0ms pre-delay"; + case BisectVariant::kB1: + return "no cache, 200ms pre-delay"; + case BisectVariant::kC1: + return "BSSID only"; + case BisectVariant::kC2: + return "CHANNEL only"; + case BisectVariant::kC3: + return "BSSID+CHANNEL"; + case BisectVariant::kC4: + return "FAST_SCAN only"; + case BisectVariant::kC5: + return "STATIC_IP only"; + case BisectVariant::kC6: + return "STATIC_IP+ARP (dep C5)"; + case BisectVariant::kC7: + return "BSSID+STATIC_IP"; + case BisectVariant::kC8: + return "CHANNEL+STATIC_IP"; + case BisectVariant::kP1: + return "PS_MAX_MODEM"; + case BisectVariant::kP2: + return "AMPDU_OFF"; + case BisectVariant::kP3: + return "FIXED_1M"; + default: + return "?"; + } +} inline std::vector EncodeVec(Payload const& p) { std::vector out(sizeof(Payload)); @@ -70,6 +203,22 @@ inline bool Decode(Buffer const& data, Payload& out) { return out.magic == kMagic; } +template +inline Buffer EncodeBisect(BisectPayload const& p) { + Buffer out(sizeof(BisectPayload)); + std::memcpy(out.data(), &p, sizeof(BisectPayload)); + return out; +} + +template +inline bool DecodeBisect(Buffer const& data, BisectPayload& out) { + if (data.size() < sizeof(BisectPayload)) { + return false; + } + std::memcpy(&out, data.data(), sizeof(BisectPayload)); + return out.magic == kBisectMagic; +} + } // namespace temp_sensor::bench #endif // TEMP_SENSOR_BENCH_PAYLOAD_H_ diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp index 4e711fa..658e7eb 100644 --- a/main/prepared_send/prepared_send.cpp +++ b/main/prepared_send/prepared_send.cpp @@ -835,6 +835,388 @@ void EndPreparedWifiSession() { } std::uint32_t LastWifiSessionStartUs() { return g_last_wifi_session_start_us; } + +namespace { + +struct BisectFactorConfig { + bool use_bssid{false}; + bool use_channel{false}; + bool use_fast_scan{false}; + bool use_static_ip{false}; + bool use_static_arp{false}; + bool ps_max_modem{false}; + bool ampdu_off{false}; + bool fixed_1m{false}; + std::uint8_t pre_delay_ms{200}; +}; + +BisectWifiCacheSnapshot g_bisect_cache{}; +std::uint8_t g_bisect_actual_channel = 0; + +BisectFactorConfig MakeBisectConfig(WifiBisectVariant variant) { + BisectFactorConfig c{}; + switch (variant) { + case WifiBisectVariant::kB0: + c.pre_delay_ms = 0; + break; + case WifiBisectVariant::kB1: + break; + case WifiBisectVariant::kC1: + c.use_bssid = true; + break; + case WifiBisectVariant::kC2: + c.use_channel = true; + break; + case WifiBisectVariant::kC3: + c.use_bssid = true; + c.use_channel = true; + break; + case WifiBisectVariant::kC4: + c.use_fast_scan = true; + break; + case WifiBisectVariant::kC5: + c.use_static_ip = true; + break; + case WifiBisectVariant::kC6: + c.use_static_ip = true; + c.use_static_arp = true; + break; + case WifiBisectVariant::kC7: + c.use_bssid = true; + c.use_static_ip = true; + break; + case WifiBisectVariant::kC8: + c.use_channel = true; + c.use_static_ip = true; + break; + case WifiBisectVariant::kP1: + c.ps_max_modem = true; + break; + case WifiBisectVariant::kP2: + c.ampdu_off = true; + break; + case WifiBisectVariant::kP3: + c.fixed_1m = true; + break; + case WifiBisectVariant::kCount: + break; + } + return c; +} + +std::uint8_t BisectFactorBitsOf(BisectFactorConfig const& c) { + using F = bench::BisectFactorBits; + std::uint8_t bits = 0; + if (c.use_bssid) { + bits |= static_cast(F::kBssid); + } + if (c.use_channel) { + bits |= static_cast(F::kChannel); + } + if (c.use_fast_scan) { + bits |= static_cast(F::kFastScan); + } + if (c.use_static_ip) { + bits |= static_cast(F::kStaticIp); + } + if (c.use_static_arp) { + bits |= static_cast(F::kStaticArp); + } + if (c.ps_max_modem) { + bits |= static_cast(F::kPsMaxModem); + } + if (c.ampdu_off) { + bits |= static_cast(F::kAmpduOff); + } + if (c.fixed_1m) { + bits |= static_cast(F::kFixed1M); + } + return bits; +} + +std::uint8_t ReadActualChannel() { + wifi_ap_record_t ap_info{}; + if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK) { + return 0; + } + return ap_info.primary; +} + +bool StartBisectWifi(BisectFactorConfig const& cfg) { +# ifndef WIFI_SSID + return false; +# endif +# ifndef WIFI_PASSWORD + return false; +# endif + + CleanupHotPathWifiRuntime(); + g_bisect_actual_channel = 0; + + bool const need_static_ip = cfg.use_static_ip && g_bisect_cache.valid_ip; + g_wait_got_ip = !need_static_ip; + g_using_bssid_cache = cfg.use_bssid && g_bisect_cache.valid_bssid; + g_max_wifi_retry = AETHER_PREPARED_HOT_WIFI_MAX_RETRY; + + wifi_init_config_t wifi_init_cfg = WIFI_INIT_CONFIG_DEFAULT(); + if (cfg.ampdu_off) { + wifi_init_cfg.ampdu_rx_enable = 0; + wifi_init_cfg.ampdu_tx_enable = 0; + } + + auto err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || + err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + if (err != ESP_OK && err != ESP_ERR_NVS_NO_FREE_PAGES) { + return false; + } + + (void)esp_netif_init(); + err = esp_event_loop_create_default(); + if (err == ESP_OK) { + g_default_event_loop_created = true; + } else if (err != ESP_ERR_INVALID_STATE) { + return false; + } + + g_wifi_event_group = xEventGroupCreate(); + if (g_wifi_event_group == nullptr) { + CleanupHotPathWifiRuntime(); + return false; + } + + g_wifi_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (g_wifi_netif == nullptr) { + g_wifi_netif = esp_netif_create_default_wifi_sta(); + } + if (g_wifi_netif == nullptr) { + CleanupHotPathWifiRuntime(); + return false; + } + + if (need_static_ip) { + esp_netif_dhcpc_stop(g_wifi_netif); + esp_netif_ip_info_t ip_info = { + .ip = {.addr = g_bisect_cache.ip}, + .netmask = {.addr = g_bisect_cache.netmask}, + .gw = {.addr = g_bisect_cache.gateway}}; + esp_netif_set_ip_info(g_wifi_netif, &ip_info); + rtc_ip_info = ip_info; + address_is_valid = true; + } + + err = esp_wifi_init(&wifi_init_cfg); + if (err == ESP_ERR_WIFI_INIT_STATE) { + g_wifi_initialized = true; + } else if (err != ESP_OK) { + CleanupHotPathWifiRuntime(); + return false; + } else { + g_wifi_initialized = true; + } + + err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + &WifiEventHandler, nullptr, + &g_wifi_any_id_handler); + if (err != ESP_OK) { + CleanupHotPathWifiRuntime(); + return false; + } + + err = esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, + &WifiEventHandler, nullptr, + &g_wifi_got_ip_handler); + if (err != ESP_OK) { + CleanupHotPathWifiRuntime(); + return false; + } + + wifi_config_t wifi_config{}; + std::strncpy(reinterpret_cast(wifi_config.sta.ssid), WIFI_SSID, + sizeof(wifi_config.sta.ssid)); + std::strncpy(reinterpret_cast(wifi_config.sta.password), WIFI_PASSWORD, + sizeof(wifi_config.sta.password)); + wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK; + + if (cfg.use_fast_scan) { + wifi_config.sta.scan_method = WIFI_FAST_SCAN; + } + + if (cfg.use_bssid && g_bisect_cache.valid_bssid) { + wifi_config.sta.bssid_set = true; + std::memcpy(wifi_config.sta.bssid, g_bisect_cache.bssid, + sizeof(wifi_config.sta.bssid)); + } + + if (cfg.use_channel && g_bisect_cache.valid_bssid) { + wifi_config.sta.channel = g_bisect_cache.channel; + } + + err = esp_wifi_set_mode(WIFI_MODE_STA); + if (err != ESP_OK) { + CleanupHotPathWifiRuntime(); + return false; + } + + err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); + if (err != ESP_OK) { + CleanupHotPathWifiRuntime(); + return false; + } + + (void)esp_wifi_set_protocol( + WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N); + + err = esp_wifi_start(); + if (err != ESP_OK) { + CleanupHotPathWifiRuntime(); + return false; + } + g_wifi_started = true; + + (void)esp_wifi_set_max_tx_power(80); + (void)esp_wifi_set_ps(cfg.ps_max_modem ? WIFI_PS_MAX_MODEM : WIFI_PS_NONE); + + EventBits_t bits = xEventGroupWaitBits( + g_wifi_event_group, kWifiReadyBit | kWifiFailBit, pdFALSE, pdFALSE, + pdMS_TO_TICKS(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS)); + + if (cfg.fixed_1m) { + (void)esp_wifi_internal_set_fix_rate(WIFI_IF_STA, true, + WIFI_PHY_RATE_1M_L); + } + + if ((bits & kWifiReadyBit) == 0) { + return false; + } + + g_bisect_actual_channel = ReadActualChannel(); + + if (cfg.use_static_arp && g_bisect_cache.valid_gw_mac && + g_bisect_cache.valid_ip) { + std::memcpy(gateway_mac, g_bisect_cache.gw_mac, sizeof(gateway_mac)); + gateway_mac_valid = true; + (void)InstallStaticGatewayArp(); + } + + return true; +} + +} // namespace + +bool FreezeBisectWifiCacheFromActiveConnection() { + g_bisect_cache = {}; + esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (netif == nullptr) { + return false; + } + + esp_netif_ip_info_t ip_info{}; + if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK || + ip_info.ip.addr == 0) { + return false; + } + + wifi_ap_record_t ap_info{}; + if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK) { + return false; + } + + g_bisect_cache.valid_ip = true; + g_bisect_cache.ip = ip_info.ip.addr; + g_bisect_cache.netmask = ip_info.netmask.addr; + g_bisect_cache.gateway = ip_info.gw.addr; + + g_bisect_cache.valid_bssid = true; + g_bisect_cache.channel = ap_info.primary; + std::memcpy(g_bisect_cache.bssid, ap_info.bssid, + sizeof(g_bisect_cache.bssid)); + + // Also refresh production RTC cache helpers used by ARP install. + rtc_ip_info = ip_info; + address_is_valid = true; + CaptureApIntoCache(); + if (ResolveAndCacheGatewayMac(netif)) { + g_bisect_cache.valid_gw_mac = true; + std::memcpy(g_bisect_cache.gw_mac, gateway_mac, + sizeof(g_bisect_cache.gw_mac)); + } + return true; +} + +BisectWifiCacheSnapshot GetBisectWifiCacheSnapshot() { return g_bisect_cache; } + +BisectSendResult SendPreparedOnceWithBisectFactor( + WifiBisectVariant variant, ae::DataBuffer const& payload) { + BisectSendResult out{}; + auto const cfg = MakeBisectConfig(variant); + out.pre_delay_ms = cfg.pre_delay_ms; + out.factor_bits = BisectFactorBitsOf(cfg); + out.requested_channel = + (cfg.use_channel && g_bisect_cache.valid_bssid) ? g_bisect_cache.channel + : 0; + + if (!g_prepared_send_message_block.is_valid()) { + out.status = HotSendStatus::kNoPreparedBlock; + return out; + } + if (g_prepared_send_message_block.Resolve()->message_left == 0) { + out.status = HotSendStatus::kNonceExhausted; + return out; + } + + auto const t0 = esp_timer_get_time(); + if (!StartBisectWifi(cfg)) { + CleanupHotPathWifiRuntime(); + out.status = HotSendStatus::kWifiFailed; + out.actual_channel = g_bisect_actual_channel; + auto const elapsed = esp_timer_get_time() - t0; + out.total_us = elapsed < 0 ? 0 : static_cast(elapsed); + return out; + } + + out.status_flags |= + static_cast(bench::BisectStatusBits::kWifiReady); + out.actual_channel = g_bisect_actual_channel; + if (out.requested_channel != 0 && + out.requested_channel == out.actual_channel) { + out.status_flags |= + static_cast(bench::BisectStatusBits::kChannelMatch); + } + + if (cfg.pre_delay_ms > 0) { + vTaskDelay(pdMS_TO_TICKS(cfg.pre_delay_ms)); + } + + auto const encode_status = EncodeAndUdpSend(payload); + if (encode_status == HotSendStatus::kSent) { + out.status_flags |= + static_cast(bench::BisectStatusBits::kEncodeOk) | + static_cast(bench::BisectStatusBits::kSendtoOk); + } else if (encode_status == HotSendStatus::kEncodeFailed) { + // encode failed after wifi ready + } else if (encode_status == HotSendStatus::kSendFailed) { + out.status_flags |= + static_cast(bench::BisectStatusBits::kEncodeOk); + } + +# ifndef AETHER_PREPARED_POST_SEND_HOLD_MS +# define AETHER_PREPARED_POST_SEND_HOLD_MS 300 +# endif + if (encode_status == HotSendStatus::kSent) { + vTaskDelay(pdMS_TO_TICKS(AETHER_PREPARED_POST_SEND_HOLD_MS)); + } + + CleanupHotPathWifiRuntime(); + + auto const elapsed = esp_timer_get_time() - t0; + out.total_us = elapsed < 0 ? 0 : static_cast(elapsed); + out.status = encode_status; + return out; +} #endif HotSendStatus TryHotWakePreparedSend( diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h index b284e96..6566b3f 100644 --- a/main/prepared_send/prepared_send.h +++ b/main/prepared_send/prepared_send.h @@ -66,6 +66,54 @@ void InvalidatePreparedWifiCache(); // Export Wi-Fi association + IP + gateway MAC from the still-active FULL path. // Call before destroying Aether Wi-Fi so prepared #1 can use the fast path. bool CapturePreparedWifiCacheFromActiveConnection(); + +// Bench-only single-factor Wi-Fi bisect (does not alter SendPreparedOnce). +enum class WifiBisectVariant : std::uint8_t { + kB0 = 0, + kB1, + kC1, + kC2, + kC3, + kC4, + kC5, + kC6, + kC7, + kC8, + kP1, + kP2, + kP3, + kCount, +}; + +struct BisectWifiCacheSnapshot { + bool valid_bssid{false}; + bool valid_ip{false}; + bool valid_gw_mac{false}; + std::uint8_t bssid[6]{}; + std::uint8_t channel{0}; + std::uint32_t ip{0}; + std::uint32_t netmask{0}; + std::uint32_t gateway{0}; + std::uint8_t gw_mac[6]{}; +}; + +struct BisectSendResult { + HotSendStatus status{HotSendStatus::kWifiFailed}; + std::uint32_t total_us{0}; + std::uint8_t requested_channel{0}; + std::uint8_t actual_channel{0}; + std::uint8_t status_flags{0}; + std::uint8_t factor_bits{0}; + std::uint8_t pre_delay_ms{0}; +}; + +bool FreezeBisectWifiCacheFromActiveConnection(); +BisectWifiCacheSnapshot GetBisectWifiCacheSnapshot(); + +// One reconnect prepared send under a single-factor Wi-Fi config. +// Wi-Fi failure before EncodePacket does not consume a prepared nonce. +BisectSendResult SendPreparedOnceWithBisectFactor( + WifiBisectVariant variant, ae::DataBuffer const& payload); #endif HotSendStatus TryHotWakePreparedSend(std::string const& temperature); diff --git a/main/prepared_wifi_single_factor_bisect_bench.cpp b/main/prepared_wifi_single_factor_bisect_bench.cpp new file mode 100644 index 0000000..34c1127 --- /dev/null +++ b/main/prepared_wifi_single_factor_bisect_bench.cpp @@ -0,0 +1,640 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Silent single-factor Wi-Fi bisect for prepared reconnect sends (ESP32-C6). + * Each variant differs from the same canonical baseline by exactly one factor + * (except documented dependent combinations C3/C6/C7/C8). + * + * AE_EXP_BISECT_SMOKE=1 → B1 only, 2 prepared sends. + * AE_EXP_BISECT_CONSOLE=1 → USB stage markers (no AE_EXP_SILENT). + */ + +#include +#include +#include + +#include "aether/all.h" +#include "aether/ae_exp_wifi.h" +#include "aether/config.h" +#include "aether/env.h" +#include "bench_payload.h" +#include "prepared_send/prepared_send.h" + +#if defined(ESP_PLATFORM) +# include +# include +# include +# include +# include +# include +# if defined(AE_EXP_BISECT_CONSOLE) +# include +# endif +#endif + +using namespace std::chrono_literals; + +namespace temp_sensor { +namespace { + +static constexpr auto kParentUid = + ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); + +#ifndef BENCH_CLIENT_ID +# define BENCH_CLIENT_ID "prepared_wifi_bisect_v1" +#endif +static constexpr char const* kBenchClientId = BENCH_CLIENT_ID; + +#if defined(SERVICE_UID) +static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID); +#else +static constexpr auto kServiceUid = + ae::Uid::FromString("3d284a4f-ebb4-451e-a2c5-aecb0d647a45"); +#endif + +static constexpr int kAllVariantCount = + static_cast(prepared_send::WifiBisectVariant::kCount); + +#if defined(AE_EXP_BISECT_SMOKE) && AE_EXP_BISECT_SMOKE +// Smoke: B1 only, 2 prepared reconnect sends. +static constexpr int kFirstVariant = + static_cast(prepared_send::WifiBisectVariant::kB1); +static constexpr int kLastVariantExclusive = kFirstVariant + 1; +static constexpr int kPreparedPerVariant = 2; +#else +static constexpr int kFirstVariant = 0; +static constexpr int kLastVariantExclusive = kAllVariantCount; +static constexpr int kPreparedPerVariant = 20; +#endif + +static constexpr int kPreparedGapMs = 1000; + +#if defined(ESP_PLATFORM) && defined(AE_EXP_BISECT_CONSOLE) +static void Stage(char const* msg) { + std::printf("%s\n", msg); + std::fflush(stdout); +} +static void Stage2(char const* a, char const* b) { + std::printf("%s %s\n", a, b); + std::fflush(stdout); +} +#else +static void Stage(char const*) {} +static void Stage2(char const*, char const*) {} +#endif + +#if defined(ESP_PLATFORM) +static const auto kWifiInit = ae::WiFiInit{ + std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}}, + {}, +}; + +static bool g_had_aether_app = false; + +static void PreConstructCleanup() { + if (!g_had_aether_app) { + return; + } +# if !AE_WIFI_USE_FULL_DEINIT + esp_netif_deinit(); + esp_event_loop_delete_default(); +# endif +} + +static std::int64_t NowUs() { return esp_timer_get_time(); } +#else +static std::int64_t NowUs() { return 0; } +#endif + +enum class Phase : std::uint8_t { + kRegister, + kFullCycle, + kPrepared, + kFinal, + kDone, +}; + +static std::shared_ptr g_app; +static ae::Client::ptr g_client; +static std::unique_ptr g_stream; +static ae::Subscription g_select_sub; +static ae::Subscription g_stream_sub; +static ae::Subscription g_write_sub; + +static Phase g_phase = Phase::kRegister; +static bool g_registration_pending = false; +static bool g_write_armed = false; +static bool g_done = false; + +// Deferred work: never Prepare/Freeze/Save/Exit inside Write callbacks. +static bool g_pending_register_finish = false; +static bool g_pending_full_post_write = false; +static bool g_full_write_ok = false; +static bool g_pending_final_exit = false; + +static int g_variant = kFirstVariant; +static int g_prepared_index = 0; +static bool g_prepared_waiting_gap = false; +#if defined(ESP_PLATFORM) +static TickType_t g_prepared_gap_until = 0; +#endif + +static std::uint16_t g_seq = 0; +static std::uint32_t g_registration_us = 0; +static std::uint32_t g_last_full_us = 0; +static std::uint32_t g_last_prepared_us = 0; +static std::uint32_t g_pending_full_us = 0; +static bool g_have_pending_full = false; + +static std::uint8_t g_wifi_ready_count = 0; +static std::uint8_t g_encode_count = 0; +static std::uint8_t g_sendto_count = 0; +static std::uint8_t g_nonce_start = 0; + +static prepared_send::BisectSendResult g_last_result{}; +static prepared_send::BisectWifiCacheSnapshot g_cache{}; + +static std::uint8_t g_prev_wifi_ready = 0; +static std::uint8_t g_prev_encode = 0; +static std::uint8_t g_prev_sendto = 0; +static std::uint8_t g_prev_nonce = 0; +static bool g_have_prev_summary = false; + +static std::int64_t g_t0 = 0; + +static void ReleaseApp() { + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + g_app.reset(); +} + +static std::uint16_t NextSeq() { return ++g_seq; } + +static prepared_send::WifiBisectVariant CurrentVariant() { + return static_cast(g_variant); +} + +static char const* CurrentVariantName() { + return bench::BisectVariantName(static_cast(g_variant)); +} + +static ae::DataBuffer MakeFullPayload() { + bench::BisectPayload p{}; + p.type = static_cast(bench::BisectMsgType::kFull); + p.variant_id = static_cast(g_variant); + p.sequence_global = NextSeq(); + p.time_us = g_have_pending_full ? g_pending_full_us : 0; + p.aux_us = (g_variant == kFirstVariant) ? g_registration_us : 0; + if (g_have_prev_summary) { + p.wifi_ready_count = g_prev_wifi_ready; + p.encode_count = g_prev_encode; + p.sendto_count = g_prev_sendto; + p.nonce_consumed = g_prev_nonce; + } + p.cached_ip = g_cache.ip; + p.cached_channel = g_cache.channel; + p.requested_channel = g_cache.channel; + p.actual_channel = g_cache.channel; + std::memcpy(p.cached_bssid, g_cache.bssid, sizeof(p.cached_bssid)); + p.pre_delay_ms = + (CurrentVariant() == prepared_send::WifiBisectVariant::kB0) ? 0 : 200; + g_have_pending_full = false; + return bench::EncodeBisect(p); +} + +static ae::DataBuffer MakePreparedPayload(int index) { + bench::BisectPayload p{}; + p.type = static_cast(bench::BisectMsgType::kPrepared); + p.variant_id = static_cast(g_variant); + p.prepared_index = static_cast(index); + p.sequence_global = NextSeq(); + if (index == 1) { + p.cached_ip = g_cache.ip; + p.cached_channel = g_cache.channel; + std::memcpy(p.cached_bssid, g_cache.bssid, sizeof(p.cached_bssid)); + p.pre_delay_ms = + (CurrentVariant() == prepared_send::WifiBisectVariant::kB0) ? 0 : 200; + } else { + p.time_us = g_last_prepared_us; + p.requested_channel = g_last_result.requested_channel; + p.actual_channel = g_last_result.actual_channel; + p.status_flags = g_last_result.status_flags; + p.factor_bits = g_last_result.factor_bits; + p.pre_delay_ms = g_last_result.pre_delay_ms; + } + return bench::EncodeBisect(p); +} + +static ae::DataBuffer MakeFinalPayload() { + bench::BisectPayload p{}; + p.type = static_cast(bench::BisectMsgType::kFinal); + p.variant_id = static_cast(g_variant > 0 ? g_variant - 1 + : kFirstVariant); + p.prepared_index = kPreparedPerVariant; + p.sequence_global = NextSeq(); + p.time_us = g_last_prepared_us; + p.aux_us = g_registration_us; + p.requested_channel = g_last_result.requested_channel; + p.actual_channel = g_last_result.actual_channel; + p.status_flags = g_last_result.status_flags; + p.factor_bits = g_last_result.factor_bits; + p.pre_delay_ms = g_last_result.pre_delay_ms; + p.wifi_ready_count = g_wifi_ready_count; + p.encode_count = g_encode_count; + p.sendto_count = g_sendto_count; + auto const left = prepared_send::PreparedMessageLeft(); + p.nonce_consumed = g_nonce_start >= left + ? static_cast(g_nonce_start - left) + : g_encode_count; + return bench::EncodeBisect(p); +} + +static void ConstructAether() { + Stage("CONSTRUCT_BEGIN"); +#if defined(ESP_PLATFORM) + PreConstructCleanup(); +#endif + g_had_aether_app = true; + g_app = ae::AetherApp::Construct( + ae::AetherAppContext{} +#if AE_DISTILLATION && defined(ESP_PLATFORM) + .AddAdapterFactory([&](ae::AetherAppContext const& ctx) { + return ae::WifiAdapter::ptr::Create( + ae::CreateWith{ctx.domain()}.with_id( + ae::GlobalId::kWiFiAdapter), + ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit); + }) +#endif + ); + Stage("CONSTRUCT_DONE"); +} + +static void DoFullWrite() { + if (g_write_armed) { + return; + } + g_write_armed = true; + Stage("FULL_WRITE_BEGIN"); + auto payload = MakeFullPayload(); + auto& wa = g_stream->Write(std::move(payload)); + g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) { + // Defer Prepare/Freeze/Save/Exit into the main loop — never nest them + // inside the Write completion path. + g_full_write_ok = (st == ae::WriteAction::Status::kSuccess); + g_pending_full_post_write = true; + }); +} + +static void MaybeFullWrite() { + if (!g_stream || g_write_armed) { + return; + } + if (!g_stream->stream_info().is_writable) { + return; + } + Stage("STREAM_WRITABLE"); + DoFullWrite(); +} + +static void OnFullClientReady(ae::Client::ptr client_ptr) { + Stage("SELECT_DONE"); + g_client = std::move(client_ptr); + Stage("STREAM_BEGIN"); + auto client = g_client.Load(); + g_stream = std::make_unique(*g_app, client, kServiceUid, + ae::P2pPortHandle{}); + g_stream_sub = + g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); }); + MaybeFullWrite(); +} + +static void StartRegister() { + g_phase = Phase::kRegister; + g_write_armed = false; + g_pending_register_finish = false; + g_t0 = NowUs(); + ConstructAether(); + Stage("SELECT_BEGIN"); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + Stage("SELECT_FAIL"); + g_app->Exit(1); + return; + } + Stage("SELECT_RESULT"); + g_client = std::move(res).value(); + g_pending_register_finish = true; + }); +} + +static void StartFullCycle() { + g_phase = Phase::kFullCycle; + g_write_armed = false; + g_pending_full_post_write = false; + g_full_write_ok = false; + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + g_t0 = NowUs(); + Stage2("BISECT_VARIANT_BEGIN", CurrentVariantName()); + ConstructAether(); + Stage("SELECT_BEGIN"); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + Stage("SELECT_FAIL"); + g_app->Exit(1); + return; + } + Stage("SELECT_RESULT"); + OnFullClientReady(std::move(res).value()); + }); +} + +static void StartPreparedPhase() { + g_phase = Phase::kPrepared; + g_prepared_index = 1; + g_prepared_waiting_gap = false; + g_wifi_ready_count = 0; + g_encode_count = 0; + g_sendto_count = 0; + g_last_prepared_us = 0; + g_last_result = {}; + g_nonce_start = + static_cast(prepared_send::PreparedMessageLeft()); + Stage("RELEASE_BEGIN"); +#if defined(ESP_PLATFORM) + prepared_send::ReleaseFullAetherWifiForHotPath(); + // Let IDF finish tearing down before the first bisect STA init. + vTaskDelay(pdMS_TO_TICKS(200)); +#endif + Stage("RELEASE_DONE"); +} + +static void DoFinalWrite() { + if (g_write_armed) { + return; + } + g_write_armed = true; + auto& wa = g_stream->Write(MakeFinalPayload()); + g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status) { + g_pending_final_exit = true; + }); +} + +static void MaybeFinalWrite() { + if (!g_stream || g_write_armed) { + return; + } + if (!g_stream->stream_info().is_writable) { + return; + } + DoFinalWrite(); +} + +static void OnFinalClientReady(ae::Client::ptr client_ptr) { + g_client = std::move(client_ptr); + auto client = g_client.Load(); + g_stream = std::make_unique(*g_app, client, kServiceUid, + ae::P2pPortHandle{}); + g_stream_sub = + g_stream->stream_update_event().Subscribe([]() { MaybeFinalWrite(); }); + MaybeFinalWrite(); +} + +static void StartFinal() { + g_phase = Phase::kFinal; + g_write_armed = false; + g_pending_final_exit = false; + g_select_sub.Reset(); + g_stream_sub.Reset(); + g_write_sub.Reset(); + g_stream.reset(); + g_client = {}; + ConstructAether(); + g_select_sub = g_app->aether() + ->SelectClient(kParentUid, kBenchClientId) + .result_event() + .Subscribe([](ae::Result res) { + if (!res) { + g_app->Exit(1); + return; + } + OnFinalClientReady(std::move(res).value()); + }); +} + +static void StorePrevSummaryAndAdvance() { + Stage2("BISECT_VARIANT_DONE", CurrentVariantName()); + g_prev_wifi_ready = g_wifi_ready_count; + g_prev_encode = g_encode_count; + g_prev_sendto = g_sendto_count; + auto const left = prepared_send::PreparedMessageLeft(); + g_prev_nonce = g_nonce_start >= left + ? static_cast(g_nonce_start - left) + : g_encode_count; + g_have_prev_summary = true; + ++g_variant; + if (g_variant >= kLastVariantExclusive) { + StartFinal(); + } else { + StartFullCycle(); + } +} + +static void FinishRegisterInLoop() { + Stage("SELECT_DONE"); + Stage("SAVE_BEGIN"); + g_app->aether().Save(); + Stage("SAVE_DONE"); + g_app->Exit(0); +} + +static void FinishFullPostWriteInLoop() { + Stage("FULL_WRITE_DONE"); + if (!g_full_write_ok) { + g_app->Exit(1); + return; + } + Stage("PREPARE_BLOCK_BEGIN"); + if (!prepared_send::ExportPreparedSendBlock(g_client, kServiceUid, + kPreparedPerVariant)) { + g_app->Exit(1); + return; + } + if (!prepared_send::HasPreparedSendBlock() || + prepared_send::PreparedMessageLeft() != + static_cast(kPreparedPerVariant)) { + g_app->Exit(1); + return; + } + Stage("PREPARE_BLOCK_DONE"); + if (!prepared_send::FreezeBisectWifiCacheFromActiveConnection()) { + g_app->Exit(1); + return; + } + g_cache = prepared_send::GetBisectWifiCacheSnapshot(); + Stage("SAVE_BEGIN"); + g_app->aether().Save(); + Stage("SAVE_DONE"); + g_app->Exit(0); +} + +void setup() { + Stage("BOOT"); +#if defined(ESP_PLATFORM) + nvs_flash_init(); + prepared_send::InvalidatePreparedWifiCache(); +#endif + g_done = false; + g_seq = 0; + g_registration_pending = true; + g_variant = kFirstVariant; + g_have_prev_summary = false; + g_pending_register_finish = false; + g_pending_full_post_write = false; + g_pending_final_exit = false; +} + +void loop() { + if (g_done) { + return; + } + + if (g_registration_pending) { + g_registration_pending = false; + StartRegister(); + return; + } + + auto process_deferred = []() { + if (g_app && g_pending_register_finish) { + g_pending_register_finish = false; + FinishRegisterInLoop(); + return true; + } + if (g_app && g_pending_full_post_write) { + g_pending_full_post_write = false; + FinishFullPostWriteInLoop(); + return true; + } + if (g_app && g_pending_final_exit) { + g_pending_final_exit = false; + g_app->Exit(0); + return true; + } + return false; + }; + + // Catch deferred work that was armed before this tick. + if (process_deferred()) { + return; + } + + if (g_phase == Phase::kPrepared) { +#if defined(ESP_PLATFORM) + if (g_prepared_waiting_gap) { + if (xTaskGetTickCount() < g_prepared_gap_until) { + vTaskDelay(pdMS_TO_TICKS(20)); + return; + } + g_prepared_waiting_gap = false; + } +#endif + + if (g_prepared_index > kPreparedPerVariant) { + StorePrevSummaryAndAdvance(); + return; + } + + int const i = g_prepared_index; + auto payload = MakePreparedPayload(i); +#if defined(ESP_PLATFORM) + auto const result = prepared_send::SendPreparedOnceWithBisectFactor( + CurrentVariant(), payload); +#else + prepared_send::BisectSendResult result{}; + result.status = prepared_send::HotSendStatus::kUnsupported; +#endif + g_last_result = result; + g_last_prepared_us = result.total_us; + if (result.status_flags & + static_cast(bench::BisectStatusBits::kWifiReady)) { + ++g_wifi_ready_count; + } + if (result.status_flags & + static_cast(bench::BisectStatusBits::kEncodeOk)) { + ++g_encode_count; + } + if (result.status_flags & + static_cast(bench::BisectStatusBits::kSendtoOk)) { + ++g_sendto_count; + } + + ++g_prepared_index; + if (g_prepared_index <= kPreparedPerVariant) { +#if defined(ESP_PLATFORM) + g_prepared_waiting_gap = true; + g_prepared_gap_until = + xTaskGetTickCount() + pdMS_TO_TICKS(kPreparedGapMs); +#endif + } + return; + } + + if (!g_app) { + return; + } + + if (!g_app->IsExited()) { + auto t = g_app->Update(ae::Now()); + // SelectClient / Write may arm deferred work during Update — run it before + // WaitUntil so we do not stall on the next wake delay. + if (process_deferred()) { + return; + } + if (!g_app->IsExited()) { + g_app->WaitUntil(t); + } + return; + } + + if (g_phase == Phase::kRegister) { + ReleaseApp(); + g_registration_us = static_cast(NowUs() - g_t0); + StartFullCycle(); + return; + } + + if (g_phase == Phase::kFullCycle) { + ReleaseApp(); + auto const full_us = static_cast(NowUs() - g_t0); + g_last_full_us = full_us; + g_pending_full_us = full_us; + g_have_pending_full = true; + StartPreparedPhase(); + return; + } + + if (g_phase == Phase::kFinal) { + ReleaseApp(); + g_phase = Phase::kDone; + g_done = true; + } +} + +} // namespace +} // namespace temp_sensor + +void setup() { temp_sensor::setup(); } +void loop() { temp_sensor::loop(); } diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp index 609da52..e31eed7 100644 --- a/temperature_receiver/main.cpp +++ b/temperature_receiver/main.cpp @@ -1,8 +1,7 @@ /* * Copyright 2026 Aethernet Inc. * - * Desktop Æther receiver for silent prepared Wi-Fi cache 5x20 experiment. - * Decodes binary bench payloads and prints aggregate statistics. + * Desktop Æther receiver for prepared Wi-Fi single-factor bisect. */ #include @@ -29,279 +28,288 @@ namespace { static constexpr auto kParentUid = ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165"); +// Reuse the stable cache-bench receiver identity (UID 5aade50f-...). static constexpr char const* kClientName = "prepared_wifi_cache_rx_v1"; -static constexpr int kOuter = 5; +static constexpr int kVariants = + static_cast(temp_sensor::bench::BisectVariant::kCount); static constexpr int kPreparedPer = 20; -static constexpr int kExpectedFull = kOuter; -static constexpr int kExpectedPrepared = kOuter * kPreparedPer; -static constexpr int kExpectedApp = kExpectedFull + kExpectedPrepared; // 105 -static constexpr int kExpectedTotal = kExpectedApp + 1; // +FINAL + +struct VariantStats { + int delivered{0}; + int duplicates{0}; + std::array got{}; + std::array have_us{}; + std::array us{}; + std::array req_ch{}; + std::array act_ch{}; + std::uint8_t wifi_ready{0}; + std::uint8_t encode{0}; + std::uint8_t sendto{0}; + std::uint8_t nonce{0}; + bool have_summary{false}; + bool have_meta{false}; + std::uint8_t cached_channel{0}; + std::uint32_t cached_ip{0}; + std::uint8_t pre_delay_ms{0}; + int channel_match{0}; + int channel_mismatch{0}; +}; std::mutex g_mu; std::vector> g_streams; - -std::uint32_t g_registration_us = 0; -std::array g_full_us{}; -std::array g_full_have{}; -std::array g_session_us{}; -std::array g_session_have{}; -std::array, kOuter> g_prep_us{}; -std::array, kOuter> g_prep_have{}; -std::array, kOuter> g_prep_flags{}; - +std::array g_var{}; +std::vector g_seen_seq; +int g_last_seq = 0; +int g_out_of_order = 0; int g_full_recv = 0; +int g_meta_recv = 0; int g_prep_recv = 0; int g_final_recv = 0; -int g_duplicates = 0; -int g_out_of_order = 0; -int g_last_seq = 0; bool g_done = false; -std::vector g_seen_seq; - std::int64_t NowMs() { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); } -std::uint32_t Percentile(std::vector v, int pct) { +std::uint32_t MedianUs(std::vector v) { if (v.empty()) { return 0; } std::sort(v.begin(), v.end()); - auto const idx = (pct * (static_cast(v.size()) - 1) + 99) / 100; - return v[static_cast(idx)]; + return v[v.size() / 2]; } -void PrintSummary() { - std::vector fulls; - std::vector sessions; - std::vector firsts; - std::vector warms; - std::vector all; - int bssid_hits = 0; - int ip_hits = 0; - int dhcp_skip = 0; - int static_arp_hits = 0; - int arp_fallback_hits = 0; - int wifi_fallback_hits = 0; +char const* Verdict(int delivered, int wifi_ready) { + if (wifi_ready > 0 && wifi_ready < 10) { + return "INCONCLUSIVE"; + } + if (delivered >= 18) { + return "OK"; + } + if (delivered >= 10) { + return "DEGRADES"; + } + return "BREAKS"; +} - for (int o = 0; o < kOuter; ++o) { - if (g_full_have[static_cast(o)]) { - fulls.push_back(g_full_us[static_cast(o)]); - } - if (g_session_have[static_cast(o)]) { - sessions.push_back(g_session_us[static_cast(o)]); +void PrintSummary() { + std::cout << "BISECT_TABLE\n"; + std::cout << "Variant\tSingle change\tDelivered/20\tMissing\tMedian_ms\t" + "WifiReady\tEncode\tSendto\tNonce\tVerdict\n"; + for (int v = 0; v < kVariants; ++v) { + auto const& s = g_var[static_cast(v)]; + int missing = kPreparedPer - s.delivered; + if (missing < 0) { + missing = 0; } + std::vector times; for (int i = 0; i < kPreparedPer; ++i) { - if (!g_prep_have[static_cast(o)][static_cast(i)]) { - continue; - } - auto const us = g_prep_us[static_cast(o)][static_cast(i)]; - auto const fl = g_prep_flags[static_cast(o)][static_cast(i)]; - all.push_back(us); - if (i == 0) { - firsts.push_back(us); - } else { - warms.push_back(us); - } - if (fl & static_cast(temp_sensor::bench::CacheFlags::kUsedBssid)) { - ++bssid_hits; - } - if (fl & static_cast(temp_sensor::bench::CacheFlags::kUsedStaticIp)) { - ++ip_hits; - } - if (fl & static_cast(temp_sensor::bench::CacheFlags::kDhcpSkipped)) { - ++dhcp_skip; - } - if (fl & static_cast(temp_sensor::bench::CacheFlags::kUsedStaticArp)) { - ++static_arp_hits; - } - if (fl & static_cast(temp_sensor::bench::CacheFlags::kArpFallback)) { - ++arp_fallback_hits; - } - if (fl & static_cast(temp_sensor::bench::CacheFlags::kWifiFallback)) { - ++wifi_fallback_hits; + if (s.have_us[static_cast(i)]) { + times.push_back(s.us[static_cast(i)]); } } + auto const med_ms = MedianUs(times) / 1000; + std::cout << temp_sensor::bench::BisectVariantName( + static_cast(v)) + << '\t' + << temp_sensor::bench::BisectVariantChange( + static_cast(v)) + << '\t' << s.delivered << "/20\t" << missing << '\t' << med_ms + << '\t' << static_cast(s.wifi_ready) << '\t' + << static_cast(s.encode) << '\t' + << static_cast(s.sendto) << '\t' + << static_cast(s.nonce) << '\t' + << Verdict(s.delivered, s.wifi_ready) << '\n'; } - auto print_vec = [](char const* name, std::vector const& v) { - std::cout << name << " raw=["; - for (size_t i = 0; i < v.size(); ++i) { - if (i) { - std::cout << ", "; - } - std::cout << v[i]; - } - std::cout << "]\n"; - if (v.empty()) { - return; - } - auto sorted = v; - std::sort(sorted.begin(), sorted.end()); - std::cout << " min=" << sorted.front() << "\n"; - std::cout << " median=" << Percentile(v, 50) << "\n"; - if (v.size() >= 10) { - std::cout << " p90=" << Percentile(v, 90) << "\n"; - std::cout << " p99=" << Percentile(v, 99) << "\n"; - } - std::cout << " max=" << sorted.back() << "\n"; - std::cout << " n=" << v.size() << "\n"; + auto get = [](int id) { + return g_var[static_cast(id)].delivered; }; + std::cout << "CHANNEL_HYPOTHESIS\n"; + std::cout << "B1 no cache = " << get(1) << "/20\n"; + std::cout << "C1 BSSID only = " << get(2) << "/20\n"; + std::cout << "C2 channel only = " << get(3) << "/20\n"; + std::cout << "C3 BSSID+channel = " << get(4) << "/20\n"; + std::cout << "C7 BSSID+static IP = " << get(8) << "/20\n"; + std::cout << "C8 channel+static IP = " << get(9) << "/20\n"; - int missing = 0; - for (int s = 1; s <= kExpectedApp; ++s) { - if (std::find(g_seen_seq.begin(), g_seen_seq.end(), - static_cast(s)) == g_seen_seq.end()) { - ++missing; - } + bool channel_bad = false; + bool channel_ok = false; + // Correlate: variants that set channel (C2,C3,C8) vs without (B1,C1,C7) + auto const with_ch = get(3) + get(4) + get(9); + auto const without_ch = get(1) + get(2) + get(8); + if (without_ch - with_ch >= 15) { + channel_bad = true; + } + if (with_ch >= without_ch - 3) { + channel_ok = true; + } + std::cout << "Does cached channel independently correlate with loss? "; + if (channel_bad && !channel_ok) { + std::cout << "YES\n"; + } else if (!channel_bad && channel_ok) { + std::cout << "NO\n"; + } else { + std::cout << "INCONCLUSIVE\n"; + } + + std::cout << "CHANNEL_MATCH_COUNTS\n"; + for (int v : {3, 4, 9}) { + auto const& s = g_var[static_cast(v)]; + std::cout << temp_sensor::bench::BisectVariantName( + static_cast(v)) + << " match=" << s.channel_match + << " mismatch=" << s.channel_mismatch + << " cached_ch=" << static_cast(s.cached_channel) << '\n'; } - std::cout << "REGISTRATION\n"; - std::cout << " time_us=" << g_registration_us << "\n"; - std::cout << "FULL\n"; - print_vec(" ", fulls); - std::cout << "WIFI_SESSION_START\n"; - print_vec(" ", sessions); - std::cout << "FIRST PREPARED (send-only)\n"; - print_vec(" ", firsts); - std::cout << "WARM PREPARED (send-only)\n"; - print_vec(" ", warms); - std::cout << "ALL PREPARED (send-only)\n"; - print_vec(" ", all); - std::cout << "DELIVERY\n"; - std::cout << " full=" << g_full_recv << "/" << kExpectedFull << "\n"; - std::cout << " prepared=" << g_prep_recv << "/" << kExpectedPrepared << "\n"; + std::cout << "DELIVERY_TOTALS\n"; + std::cout << " full=" << g_full_recv << "/" << kVariants << "\n"; + std::cout << " meta=" << g_meta_recv << "/" << kVariants << "\n"; + std::cout << " prepared=" << g_prep_recv << "/" << (kVariants * kPreparedPer) + << "\n"; std::cout << " final=" << g_final_recv << "/1\n"; - std::cout << " missing=" << missing << "\n"; - std::cout << " duplicates=" << g_duplicates << "\n"; std::cout << " out_of_order=" << g_out_of_order << "\n"; - std::cout << "CACHE\n"; - std::cout << " BSSID reuse confirmed=" - << (bssid_hits > 0 ? "yes" : "no") << " (hits=" << bssid_hits - << ")\n"; - std::cout << " channel reuse yes/no via BSSID flag hits=" << bssid_hits - << "\n"; - std::cout << " static IP reuse confirmed=" - << (ip_hits > 0 ? "yes" : "no") << " (hits=" << ip_hits << ")\n"; - std::cout << " DHCP skipped confirmed=" - << (dhcp_skip > 0 ? "yes" : "no") << " (hits=" << dhcp_skip - << ")\n"; - std::cout << " used_static_arp hits=" << static_arp_hits << "\n"; - std::cout << " arp_fallback hits=" << arp_fallback_hits << "\n"; - std::cout << " wifi_fallback hits=" << wifi_fallback_hits << "\n"; - std::cout << "NOTE prepared_send_us is encode+sendto only (no post-send hold in keep-wifi-up)\n"; - std::cout << "NOTE previous_full_us on PREPARED#1 carries wifi_session_start_us\n"; std::cout << "BENCH_DONE\n"; std::cout.flush(); } -void OnMessage(ae::Uid sender, ae::DataBuffer const& data) { - temp_sensor::bench::Payload p{}; - if (!temp_sensor::bench::Decode(data, p)) { - std::cout << "RECV unknown sender=" << ae::Format("{}", sender) - << " size=" << data.size() << "\n"; +void NoteSeq(std::uint16_t seq) { + if (std::find(g_seen_seq.begin(), g_seen_seq.end(), seq) != + g_seen_seq.end()) { return; } + g_seen_seq.push_back(seq); + if (seq != 0 && g_last_seq != 0 && + seq < static_cast(g_last_seq)) { + ++g_out_of_order; + } + g_last_seq = seq; +} - auto const ts = NowMs(); - std::lock_guard lock{g_mu}; - - if (std::find(g_seen_seq.begin(), g_seen_seq.end(), p.sequence_global) != - g_seen_seq.end()) { - ++g_duplicates; - } else { - g_seen_seq.push_back(p.sequence_global); +void ApplyPreparedMetrics(int v, int slot, + temp_sensor::bench::BisectPayload const& p) { + if (v < 0 || v >= kVariants || slot < 0 || slot >= kPreparedPer) { + return; } - if (p.sequence_global != 0 && g_last_seq != 0 && - p.sequence_global < static_cast(g_last_seq)) { - ++g_out_of_order; + auto& s = g_var[static_cast(v)]; + if (p.time_us != 0) { + s.us[static_cast(slot)] = p.time_us; + s.have_us[static_cast(slot)] = true; } - g_last_seq = p.sequence_global; + s.req_ch[static_cast(slot)] = p.requested_channel; + s.act_ch[static_cast(slot)] = p.actual_channel; + if (p.requested_channel != 0) { + if (p.requested_channel == p.actual_channel) { + ++s.channel_match; + } else if (p.actual_channel != 0) { + ++s.channel_mismatch; + } + } +} + +void OnBisect(temp_sensor::bench::BisectPayload const& p) { + auto const ts = NowMs(); + NoteSeq(p.sequence_global); + int const v = static_cast(p.variant_id); + auto type = static_cast(p.type); - auto type = static_cast(p.type); - if (type == temp_sensor::bench::MsgType::kFull) { + if (type == temp_sensor::bench::BisectMsgType::kFull) { ++g_full_recv; - if (p.outer_cycle == 1 && p.registration_us != 0) { - g_registration_us = p.registration_us; + if (v >= 1 && v < kVariants) { + // Previous variant summary rides on this FULL. + auto& prev = g_var[static_cast(v - 1)]; + prev.wifi_ready = p.wifi_ready_count; + prev.encode = p.encode_count; + prev.sendto = p.sendto_count; + prev.nonce = p.nonce_consumed; + prev.have_summary = true; } - // previous_full_us is timing of outer_cycle-1 - if (p.outer_cycle >= 2 && p.outer_cycle <= kOuter + 1) { - int const idx = static_cast(p.outer_cycle) - 2; - if (idx >= 0 && idx < kOuter && p.previous_full_us != 0) { - g_full_us[static_cast(idx)] = p.previous_full_us; - g_full_have[static_cast(idx)] = true; - } - } - // previous_prepared_us is last prepared of previous outer - if (p.outer_cycle >= 2 && p.previous_prepared_us != 0) { - int const o = static_cast(p.outer_cycle) - 2; - if (o >= 0 && o < kOuter) { - g_prep_us[static_cast(o)][kPreparedPer - 1] = - p.previous_prepared_us; - g_prep_have[static_cast(o)][kPreparedPer - 1] = true; - g_prep_flags[static_cast(o)][kPreparedPer - 1] = p.cache_flags; - } + std::cout << ae::Format( + "RECV FULL variant={} seq={} time_us={} ts={}\n", + temp_sensor::bench::BisectVariantName(p.variant_id), p.sequence_global, + p.time_us, ts); + } else if (type == temp_sensor::bench::BisectMsgType::kMeta) { + ++g_meta_recv; + if (v >= 0 && v < kVariants) { + auto& s = g_var[static_cast(v)]; + s.have_meta = true; + s.cached_channel = p.cached_channel; + s.cached_ip = p.cached_ip; + s.pre_delay_ms = p.pre_delay_ms; } std::cout << ae::Format( - "RECV FULL outer={} seq={} reg_us={} prev_full_us={} prev_prep_us={} " + "RECV META variant={} seq={} cached_ch={} cached_ip={:08x} pre_ms={} " "ts={}\n", - p.outer_cycle, p.sequence_global, p.registration_us, p.previous_full_us, - p.previous_prepared_us, ts); - } else if (type == temp_sensor::bench::MsgType::kPrepared) { + temp_sensor::bench::BisectVariantName(p.variant_id), p.sequence_global, + p.cached_channel, p.cached_ip, p.pre_delay_ms, ts); + } else if (type == temp_sensor::bench::BisectMsgType::kPrepared) { ++g_prep_recv; - int const o = static_cast(p.outer_cycle) - 1; - int const i = static_cast(p.prepared_index) - 1; - if (o >= 0 && o < kOuter && p.prepared_index == 1 && - p.previous_full_us != 0) { - g_session_us[static_cast(o)] = p.previous_full_us; - g_session_have[static_cast(o)] = true; - } - // previous_prepared_us is timing of prepared_index-1 - if (o >= 0 && o < kOuter && p.prepared_index >= 2) { - int const pi = static_cast(p.prepared_index) - 2; - if (pi >= 0 && pi < kPreparedPer) { - g_prep_us[static_cast(o)][static_cast(pi)] = - p.previous_prepared_us; - g_prep_have[static_cast(o)][static_cast(pi)] = true; - g_prep_flags[static_cast(o)][static_cast(pi)] = - p.cache_flags; + if (v >= 0 && v < kVariants) { + auto& s = g_var[static_cast(v)]; + int const idx = static_cast(p.prepared_index); + if (idx == 1 && p.cached_channel != 0) { + s.have_meta = true; + s.cached_channel = p.cached_channel; + s.cached_ip = p.cached_ip; + s.pre_delay_ms = p.pre_delay_ms; + } + if (idx >= 1 && idx <= kPreparedPer) { + auto& seen = s.got[static_cast(idx - 1)]; + if (!seen) { + seen = true; + ++s.delivered; + } else { + ++s.duplicates; + } + } + if (idx >= 2) { + ApplyPreparedMetrics(v, idx - 2, p); } - } - if (o >= 0 && o < kOuter && i >= 0 && i < kPreparedPer) { - g_prep_flags[static_cast(o)][static_cast(i)] = - p.cache_flags; } std::cout << ae::Format( - "RECV PREPARED outer={} idx={} seq={} prev_us={} session_us={} flags={} " - "ts={}\n", - p.outer_cycle, p.prepared_index, p.sequence_global, - p.previous_prepared_us, p.previous_full_us, p.cache_flags, ts); - } else if (type == temp_sensor::bench::MsgType::kFinal) { + "RECV PREPARED variant={} idx={} seq={} prev_us={} req_ch={} act_ch={} " + "flags={} ts={}\n", + temp_sensor::bench::BisectVariantName(p.variant_id), p.prepared_index, + p.sequence_global, p.time_us, p.requested_channel, p.actual_channel, + p.status_flags, ts); + } else if (type == temp_sensor::bench::BisectMsgType::kFinal) { ++g_final_recv; - if (p.previous_full_us != 0) { - g_full_us[kOuter - 1] = p.previous_full_us; - g_full_have[kOuter - 1] = true; - } - if (p.previous_prepared_us != 0) { - g_prep_us[kOuter - 1][kPreparedPer - 1] = p.previous_prepared_us; - g_prep_have[kOuter - 1][kPreparedPer - 1] = true; - g_prep_flags[kOuter - 1][kPreparedPer - 1] = p.cache_flags; - } - if (p.registration_us != 0) { - g_registration_us = p.registration_us; + if (v >= 0 && v < kVariants) { + auto& s = g_var[static_cast(v)]; + s.wifi_ready = p.wifi_ready_count; + s.encode = p.encode_count; + s.sendto = p.sendto_count; + s.nonce = p.nonce_consumed; + s.have_summary = true; + ApplyPreparedMetrics(v, kPreparedPer - 1, p); } std::cout << ae::Format( - "RECV FINAL prev_full_us={} prev_prep_us={} flags={} ts={}\n", - p.previous_full_us, p.previous_prepared_us, p.cache_flags, ts); + "RECV FINAL variant={} seq={} last_us={} wifi_ready={} encode={} " + "sendto={} nonce={} ts={}\n", + temp_sensor::bench::BisectVariantName(p.variant_id), p.sequence_global, + p.time_us, p.wifi_ready_count, p.encode_count, p.sendto_count, + p.nonce_consumed, ts); PrintSummary(); g_done = true; } std::cout.flush(); } +void OnMessage(ae::Uid sender, ae::DataBuffer const& data) { + std::lock_guard lock{g_mu}; + temp_sensor::bench::BisectPayload bp{}; + if (temp_sensor::bench::DecodeBisect(data, bp)) { + OnBisect(bp); + return; + } + std::cout << "RECV unknown sender=" << ae::Format("{}", sender) + << " size=" << data.size() << "\n"; + std::cout.flush(); +} + std::filesystem::path ResolveSessionRoot() { #if defined(_WIN32) if (char const* env = std::getenv("AE_RECEIVER_SESSION_DIR")) { @@ -348,26 +356,13 @@ int main() { *aether_app, client.Load(), sender, std::move(handle)); stream->out_data_event().Subscribe( [sender](auto const& d) { OnMessage(sender, d); }); - std::lock_guard lock{g_mu}; g_streams.push_back(std::move(stream)); }); }); - while (!aether_app->IsExited()) { - auto next = aether_app->Update(ae::Now()); - aether_app->WaitUntil(next); - { - std::lock_guard lock{g_mu}; - if (g_done) { - aether_app->Exit(0); - } - } - } - { - std::lock_guard lock{g_mu}; - if (!g_done) { - PrintSummary(); - } + while (!aether_app->IsExited() && !g_done) { + auto t = aether_app->Update(ae::Now()); + aether_app->WaitUntil(t); } return aether_app->ExitCode(); } From adf561992acaa479f549267b20b0df0c7293cb21 Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Sat, 29 Aug 2026 06:23:48 -0700 Subject: [PATCH 24/32] Add silent fastest-path prepared Wi-Fi campaign and 710ms winner report. Measures ESP32-C6 reconnect cycles (channel+static IP+ARP baseline), finds WPA2 SAE-off + PRE/POST 200/300 as the most reliable long-run path (198/200 at 710ms vs ~850ms), and documents callback/auth/delay results without changing production SendPreparedOnce. Co-authored-by: Cursor --- .gitignore | 3 + CMakeLists.txt | 12 + .../PREPARED_WIFI_FASTEST_PATH_REPORT.md | 104 +++ experiments/fastest_chat.txt | 570 +++++++++++++++ experiments/fastest_state.json | 27 + experiments/prepared_wifi_fastest_path.tsv | 38 + experiments/run_fastest_continue.py | 397 +++++++++++ experiments/run_fastest_continue2.py | 350 ++++++++++ experiments/run_fastest_path.py | 655 ++++++++++++++++++ experiments/run_fastest_safe_val.py | 322 +++++++++ experiments/run_fastest_validate.py | 290 ++++++++ main/CMakeLists.txt | 30 + main/bench_payload.h | 62 ++ main/prepared_send/prepared_send.cpp | 359 ++++++++++ main/prepared_send/prepared_send.h | 46 ++ main/prepared_wifi_fastest_path_bench.cpp | 621 +++++++++++++++++ sdkconfig.defaults.fastest | 6 + sdkconfig.defaults.wpa2only | 3 + temperature_receiver/main.cpp | 375 ++++------ 19 files changed, 4044 insertions(+), 226 deletions(-) create mode 100644 experiments/PREPARED_WIFI_FASTEST_PATH_REPORT.md create mode 100644 experiments/fastest_chat.txt create mode 100644 experiments/fastest_state.json create mode 100644 experiments/prepared_wifi_fastest_path.tsv create mode 100644 experiments/run_fastest_continue.py create mode 100644 experiments/run_fastest_continue2.py create mode 100644 experiments/run_fastest_path.py create mode 100644 experiments/run_fastest_safe_val.py create mode 100644 experiments/run_fastest_validate.py create mode 100644 main/prepared_wifi_fastest_path_bench.cpp create mode 100644 sdkconfig.defaults.fastest create mode 100644 sdkconfig.defaults.wpa2only diff --git a/.gitignore b/.gitignore index e4ce498..e6c1229 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ sdkconfig* !sdkconfig.defaults + +!sdkconfig.defaults.* + diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e79a84..6333f9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,10 @@ set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 "" CACHE STRING "Silent 5x20 keep-Wi-Fi-up prepared bench (set to 1)") set(AE_EXP_PREPARED_WIFI_BISECT "" CACHE STRING "Silent single-factor prepared Wi-Fi bisect (set to 1)") +set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING + "Silent fastest-path prepared Wi-Fi campaign (set to 1)") +set(AE_EXP_FAST_DISABLE_WPA3 "" CACHE STRING + "Benchmark-only: disable CONFIG_ESP_WIFI_ENABLE_WPA3_SAE (set to 1)") set(AE_EXP_BISECT_CONSOLE "" CACHE STRING "Bisect USB stage markers / console (set to 1; disables silent)") set(AE_EXP_BISECT_SMOKE "" CACHE STRING @@ -41,6 +45,14 @@ if(AE_EXP_BISECT_CONSOLE STREQUAL "1" AND AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1") list(APPEND SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.bench") +elseif(AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1") + list(APPEND SDKCONFIG_DEFAULTS + "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent" + "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest") + if(AE_EXP_FAST_DISABLE_WPA3 STREQUAL "1") + list(APPEND SDKCONFIG_DEFAULTS + "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only") + endif() elseif(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1" OR AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1") diff --git a/experiments/PREPARED_WIFI_FASTEST_PATH_REPORT.md b/experiments/PREPARED_WIFI_FASTEST_PATH_REPORT.md new file mode 100644 index 0000000..5676987 --- /dev/null +++ b/experiments/PREPARED_WIFI_FASTEST_PATH_REPORT.md @@ -0,0 +1,104 @@ +# Prepared Wi-Fi Fastest Path Report (ESP32-C6) + +## Pins +- **temperature-sensor** branch: `thermometer-prepared-send-v0` +- **aether-client-cpp**: `157aadbec8e7b852d0f89274307ff7cb8103e5f7` **unchanged=yes** +- ESP-IDF v6.0.2 · Silent Release / NDEBUG · `CONFIG_ESP_CONSOLE_NONE` · log level NONE +- CPU 160 MHz · `# CONFIG_PM_ENABLE is not set` · `WIFI_PS_NONE` · max TX power · Wi-Fi 4 only · auto PHY +- Pattern: full Wi-Fi init → associate → prepared UDP → full teardown → 1 s gap **outside** timer +- No deep/light sleep / reboot during benchmark +- Production `SendPreparedOnce` **not** switched to winner + +## OLD vs NEW +| | | +|---|---| +| OLD | ~850 ms (prior C6/C8 static-IP cycle) | +| NEW | **710 ms** (VAL200 median cycle) | +| Absolute saving | **140 ms** | +| Percent saving | **16.5%** | +| Speedup | **1.20×** | + +## BEST RELIABLE CONFIG (winner) +Effective measurement configuration: + +| Knob | Value | +|------|-------| +| Wi-Fi protocol | 802.11b/g/n only | +| Channel cache | yes | +| BSSID cache | **no** | +| Static IPv4 + netmask + gateway | yes | +| Static ARP (gateway MAC) | yes | +| Scan method | default (not `WIFI_FAST_SCAN`) | +| Auth | **WPA2-PSK** (negotiated `authmode=3`) | +| SAE / WPA3 | benchmark-only `CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=n` (restored after tests; **security decision deferred**) | +| Association retry max | **10** | +| PRE-send delay | **200 ms** (after network-ready) | +| POST-send | **300 ms** fixed hold | +| TX-done callback | **not used** | +| AMPDU TX | already off in sdkconfig; explicit D1 no cycle win | +| Wi-Fi storage | NVS default (`WIFI_STORAGE_RAM` not adopted) | +| IRAM | `CONFIG_ESP_WIFI_IRAM_OPT` / `RX_IRAM_OPT` already on; `CONFIG_LWIP_IRAM_OPTIMIZATION` off (not A/B'd — sendto not bottleneck) | + +### VAL200 (winner validation) +- **Delivered: 198/200** (not rounded) +- connect median **125 ms** · cycle median **710 ms** · p90 **750** · max **870** +- wifi_ready / encode / sendto / nonce = 200/200 + +## Callback verdict +- IDF 6.0.2 API: `esp_wifi_set_tx_done_cb` (`esp_private/wifi.h`) +- CB0 (POST=0): callback **fires** (`cb_any=18`) but **fingerprint match=0**; delivery **18/20** +- **CALLBACK_NOT_USABLE** for normal lwIP/BSD UDP — do not pursue further + +## Auth (WPA2 / WPA3 / H2E) +- AP advertises transition (`authmode=7` on WPA3-capable builds) +- Threshold-only WPA2 still negotiated as 7 while SAE enabled +- With `CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=n`: negotiated **authmode=3 (WPA2_PSK)**; connect ~128 ms; cycle ~710 ms @ PRE/POST 200/300 +- H2E-only: no clear win; delivery 18/20 on screen +- **Report only** — production security not auto-weakened; SAE restored in build tree after campaign + +## Association extras (from BASE WPA3 path) +- A1 BSSID / A2 FAST_SCAN / A3 both: no clear ≥20 ms improvement; some delivery loss + +## Retry +- R0/R1/R3 success-path medians similar; R0 unreliable on longer runs (e.g. PRE_200 + retry=0 → 13/20) +- Keep **retry=10** + +## Delay search +| Stage | Result | +|-------|--------| +| PRE | 200 reliable on screen; 150+POST300 → 14/20 (stop) | +| POST | 150 screen 20/20 @ ~550 ms but VAL100 only 94/100; **100 → 13/20** (stop) | +| 2D | `PRE=150,POST=200` screen 20/20 @ 540 ms; VAL100 97/100 | +| Reliability | Aggressive delays fail ≥99/100; **SAFE 200/300** best long-run delivery | + +### VAL100 candidates (actual delivery) +| Variant | Delivered | Cycle med | Connect med | p90 | +|---------|-----------|-----------|-------------|-----| +| PRE200/POST150 | 94/100 | 560 | 131 | 620 | +| PRE150/POST200 | 97/100 | 560 | 131 | 610 | +| SAFE PRE200/POST300 | 98/100 | 730 | 138 | 770 | +| SAFE repeat | 96/100 | 720 | 124 | 750 | +| MID PRE200/POST200 | 91/100 | 620 | 125 | 660 | +| FAST PRE200/POST150 | 79/100 | 560 | 122 | 600 | +| FAST PRE150/POST200 | 84/100 | 540 | 118 | 580 | + +Winner chosen by long-run delivery (then VAL200), not screen-only 550 ms. + +## AMPDU / A-MSDU +- sdkconfig already `# CONFIG_ESP_WIFI_AMPDU_TX_ENABLED is not set` (RX also off) +- D1 explicit AMPDU TX off (correct n=20): **17/20**, cycle **710** — no improvement vs winner +- A-MSDU TX: not enabled in sdkconfig — no separate test + +## STORAGE_RAM +- Screen: **19/20**, cycle **670** / connect **112** (−40 ms vs 710) +- Not adopted: delivery <20/20 on screen; no 100/200 validation; winner stays NVS storage + +## sdkconfig notes +- Useful already-on: Wi-Fi IRAM opts, CPU 160 MHz, PM disabled for this campaign +- Candidate left untested (latency unclear / not bottleneck): `CONFIG_LWIP_IRAM_OPTIMIZATION` +- `CONFIG_ESP_WIFI_NVS_ENABLED=n` not tried (avoid touching Aether NVS) + +## Artifacts +- TSV: `experiments/prepared_wifi_fastest_path.tsv` +- Orchestrators: `experiments/run_fastest_path.py`, `run_fastest_continue*.py`, `run_fastest_safe_val.py` +- Progress: `experiments/fastest_chat.txt`, `experiments/fastest_progress.log` diff --git a/experiments/fastest_chat.txt b/experiments/fastest_chat.txt new file mode 100644 index 0000000..14a6673 --- /dev/null +++ b/experiments/fastest_chat.txt @@ -0,0 +1,570 @@ +[TEST 1/22] BASE +delivery=20/20 +median_cycle=840 ms +median_connect=262 ms +p90=920 +result=PASS +remaining=21 +best=BASE cycle=840ms connect=262ms del=20/20 +change vs best=NEW BEST +BEST NOW: +config=BASE +pre=200 post/callback=300 mode=0 +median=840 +NEXT: A1 BASE+cached BSSID x20 + +[TEST 2/22] A1_BSSID +delivery=19/20 +median_cycle=840 ms +median_connect=278 ms +p90=870 +result=FAIL +remaining=20 +best=BASE cycle=840ms connect=262ms del=20/20 +change vs best=+0 ms +BEST NOW: +config=BASE +pre=200 post/callback=300 mode=0 +median=840 +NEXT: A2 BASE+WIFI_FAST_SCAN x20 + +[TEST 3/22] A2_FAST_SCAN +delivery=20/20 +median_cycle=850 ms +median_connect=270 ms +p90=880 +result=PASS +remaining=19 +best=BASE cycle=840ms connect=262ms del=20/20 +change vs best=+10 ms +BEST NOW: +config=BASE +pre=200 post/callback=300 mode=0 +median=840 +NEXT: A3 BASE+BSSID+FAST_SCAN x20 + +[TEST 4/22] A3_BSSID_FAST_SCAN +delivery=19/20 +median_cycle=860 ms +median_connect=276 ms +p90=880 +result=FAIL +remaining=18 +best=BASE cycle=840ms connect=262ms del=20/20 +change vs best=+20 ms +BEST NOW: +config=BASE +pre=200 post/callback=300 mode=0 +median=840 +NEXT: AUTH1 WPA3 on BASE x20 + +[TEST 5/22] AUTH2_H2E +delivery=18/20 +median_cycle=840 ms +median_connect=269 ms +p90=910 +result=FAIL +remaining=17 +best=BASE cycle=840ms connect=262ms del=20/20 +change vs best=+0 ms +BEST NOW: +config=BASE +pre=200 post/callback=300 mode=0 +median=840 +NEXT: AUTH3 WPA2-only on BASE x20 + +[TEST 6/22] AUTH3_WPA2 +delivery=20/20 +median_cycle=850 ms +median_connect=258 ms +p90=920 +result=PASS +remaining=16 +best=BASE cycle=840ms connect=262ms del=20/20 +change vs best=+10 ms +BEST NOW: +config=BASE +pre=200 post/callback=300 mode=0 +median=840 +NEXT: R0 no reconnect retry x20 + +[TEST 7/23] AUTH3B_WPA3SAE_OFF +delivery=20/20 +median_cycle=830 ms +median_connect=253 ms +p90=900 +result=PASS +remaining=16 +best=BASE cycle=840ms connect=262ms del=20/20 +change vs best=-10 ms +BEST NOW: +config=BASE +pre=200 post/callback=300 mode=0 +median=840 +NEXT: restore WPA3 SAE then R0 + +[TEST 8/23] AUTH3B_WPA3SAE_OFF_VERIFIED +delivery=20/20 +median_cycle=710 ms +median_connect=128 ms +p90=810 +result=PASS +remaining=15 +best=AUTH3B_WPA3SAE_OFF_VERIFIED cycle=710ms connect=128ms del=20/20 +change vs best=NEW BEST +BEST NOW: +config=AUTH3B_WPA3SAE_OFF_VERIFIED +pre=200 post/callback=300 mode=0 +median=710 +NEXT: restore WPA3 then R0 + +[TEST 9/22] R0 +delivery=20/20 +median_cycle=730 ms +median_connect=149 ms +p90=760 +result=PASS +remaining=13 +best=AUTH3B_WPA3SAE_OFF_VERIFIED cycle=710ms connect=128ms del=20/20 +change vs best=+20 ms +BEST NOW: +config=AUTH3B_WPA3SAE_OFF_VERIFIED +pre=200 post/callback=300 mode=0 +median=710 +NEXT: R1 retry=1 x20 + +[TEST 10/22] R1 +delivery=20/20 +median_cycle=720 ms +median_connect=149 ms +p90=750 +result=PASS +remaining=12 +best=AUTH3B_WPA3SAE_OFF_VERIFIED cycle=710ms connect=128ms del=20/20 +change vs best=+10 ms +BEST NOW: +config=AUTH3B_WPA3SAE_OFF_VERIFIED +pre=200 post/callback=300 mode=0 +median=710 +NEXT: R3 retry=3 x20 + +[TEST 11/22] R3 +delivery=20/20 +median_cycle=740 ms +median_connect=153 ms +p90=750 +result=PASS +remaining=11 +best=AUTH3B_WPA3SAE_OFF_VERIFIED cycle=710ms connect=128ms del=20/20 +change vs best=+30 ms +BEST NOW: +config=AUTH3B_WPA3SAE_OFF_VERIFIED +pre=200 post/callback=300 mode=0 +median=710 +NEXT: CB0 tx-done callback POST=0 x20 + +[TEST 12/22] CB0 +delivery=18/20 +median_cycle=530 ms +median_connect=154 ms +p90=550 +result=CALLBACK_NOT_USABLE +remaining=10 +best=AUTH3B_WPA3SAE_OFF_VERIFIED cycle=710ms connect=128ms del=20/20 +change vs best=-180 ms +BEST NOW: +config=AUTH3B_WPA3SAE_OFF_VERIFIED +pre=200 post/callback=300 mode=0 +median=710 +NEXT: PRE sweep from 200ms (fixed POST=300) + +[TEST 13/22] PRE_200 +delivery=13/20 +median_cycle=740 ms +median_connect=151 ms +p90=750 +result=FAIL +remaining=9 +best=AUTH3B_WPA3SAE_OFF_VERIFIED cycle=710ms connect=128ms del=20/20 +change vs best=+30 ms +BEST NOW: +config=AUTH3B_WPA3SAE_OFF_VERIFIED +pre=200 post/callback=300 mode=0 +median=710 +NEXT: PRE stop at 200; use best_pre=200 + +[TEST 13/25] PRE_200_R0FAIL +delivery=13/20 +median_cycle=740 ms +median_connect=151 ms +p90=750 +result=FAIL +remaining=12 +best=AUTH3B_WPA3SAE_OFF_VERIFIED cycle=710ms connect=128ms del=20/20 +change vs best=+30 ms +BEST NOW: +config=AUTH3B_WPA3SAE_OFF_VERIFIED +pre=200 post/callback=300 mode=0 +median=710 +NEXT: PRE_200 with retry=10 + +[TEST 14/25] PRE_200 +delivery=19/20 +median_cycle=730 ms +median_connect=144 ms +p90=750 +result=FAIL +remaining=11 +best=AUTH3B_WPA3SAE_OFF_VERIFIED cycle=710ms connect=128ms del=20/20 +change vs best=+20 ms +BEST NOW: +config=AUTH3B_WPA3SAE_OFF_VERIFIED +pre=200 post/callback=300 mode=0 +median=710 +NEXT: PRE_200 repeat + +[TEST 15/25] PRE_200_R +delivery=20/20 +median_cycle=700 ms +median_connect=123 ms +p90=750 +result=PASS +remaining=10 +best=PRE_200_R cycle=700ms connect=123ms del=20/20 +change vs best=NEW BEST +BEST NOW: +config=PRE_200_R +pre=200 post/callback=300 mode=0 +median=700 +NEXT: PRE_150 + +[TEST 16/25] PRE_150 +delivery=14/20 +median_cycle=640 ms +median_connect=124 ms +p90=670 +result=FAIL +remaining=9 +best=PRE_200_R cycle=700ms connect=123ms del=20/20 +change vs best=-60 ms +BEST NOW: +config=PRE_200_R +pre=200 post/callback=300 mode=0 +median=700 +NEXT: PRE stop at 150; best_pre=200 + +[TEST 17/25] POST_300 +delivery=20/20 +median_cycle=700 ms +median_connect=120 ms +p90=740 +result=PASS +remaining=8 +best=PRE_200_R cycle=700ms connect=123ms del=20/20 +change vs best=+0 ms +BEST NOW: +config=PRE_200_R +pre=200 post/callback=300 mode=0 +median=700 +NEXT: POST_250 + +[TEST 18/25] POST_250 +delivery=20/20 +median_cycle=640 ms +median_connect=121 ms +p90=690 +result=PASS +remaining=7 +best=POST_250 cycle=640ms connect=121ms del=20/20 +change vs best=NEW BEST +BEST NOW: +config=POST_250 +pre=200 post/callback=250 mode=0 +median=640 +NEXT: POST_200 + +[TEST 19/25] POST_200 +delivery=20/20 +median_cycle=620 ms +median_connect=132 ms +p90=650 +result=PASS +remaining=6 +best=POST_200 cycle=620ms connect=132ms del=20/20 +change vs best=NEW BEST +BEST NOW: +config=POST_200 +pre=200 post/callback=200 mode=0 +median=620 +NEXT: POST_150 + +[TEST 20/25] POST_150 +delivery=19/20 +median_cycle=540 ms +median_connect=124 ms +p90=550 +result=FAIL +remaining=5 +best=POST_200 cycle=620ms connect=132ms del=20/20 +change vs best=-80 ms +BEST NOW: +config=POST_200 +pre=200 post/callback=200 mode=0 +median=620 +NEXT: POST_150 repeat + +[TEST 21/25] POST_150_R +delivery=20/20 +median_cycle=550 ms +median_connect=125 ms +p90=580 +result=PASS +remaining=4 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=NEW BEST +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: POST_100 + +[TEST 22/25] POST_100 +delivery=13/20 +median_cycle=490 ms +median_connect=113 ms +p90=510 +result=FAIL +remaining=3 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=-60 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: POST stop; best_post=150 + +[TEST 23/25] 2D_p175_q175 +delivery=18/20 +median_cycle=530 ms +median_connect=127 ms +p90=550 +result=FAIL +remaining=2 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=-20 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: next 2D or VAL100 + +[TEST 24/25] 2D_p225_q125 +delivery=19/20 +median_cycle=530 ms +median_connect=127 ms +p90=550 +result=FAIL +remaining=1 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=-20 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: next 2D or VAL100 + +[TEST 25/25] 2D_p150_q200 +delivery=20/20 +median_cycle=540 ms +median_connect=125 ms +p90=580 +result=PASS +remaining=0 +best=2D_p150_q200 cycle=540ms connect=125ms del=20/20 +change vs best=NEW BEST +BEST NOW: +config=2D_p150_q200 +pre=150 post/callback=200 mode=0 +median=540 +NEXT: next 2D or VAL100 + +[TEST 26/33] VAL100_PRIMARY +delivery=94/100 +median_cycle=560 ms +median_connect=131 ms +p90=620 +result=FAIL +remaining=7 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=+10 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: VAL100 secondary + +[TEST 27/33] VAL100_SECONDARY +delivery=97/100 +median_cycle=560 ms +median_connect=131 ms +p90=610 +result=FAIL +remaining=6 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=+10 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: pick winner VAL200 + +[TEST 28/37] VAL100_SAFE_200_300 +delivery=98/100 +median_cycle=730 ms +median_connect=138 ms +p90=770 +result=FAIL +remaining=9 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=+180 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: next candidate or VAL200 + +[TEST 29/37] VAL100_SAFE_200_300_R +delivery=96/100 +median_cycle=720 ms +median_connect=124 ms +p90=750 +result=FAIL +remaining=8 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=+170 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: next + +[TEST 30/37] VAL100_MID_200_200 +delivery=91/100 +median_cycle=620 ms +median_connect=125 ms +p90=660 +result=FAIL +remaining=7 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=+70 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: next candidate or VAL200 + +[TEST 31/37] VAL100_FAST_200_150 +delivery=79/100 +median_cycle=560 ms +median_connect=122 ms +p90=600 +result=FAIL +remaining=6 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=+10 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: next candidate or VAL200 + +[TEST 32/37] VAL100_FAST_150_200 +delivery=84/100 +median_cycle=540 ms +median_connect=118 ms +p90=580 +result=FAIL +remaining=5 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=-10 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: next candidate or VAL200 + +[TEST 33/37] VAL200 +delivery=198/200 +median_cycle=710 ms +median_connect=125 ms +p90=750 +result=FAIL +remaining=4 +best=POST_150_R cycle=550ms connect=125ms del=20/20 +change vs best=+160 ms +BEST NOW: +config=POST_150_R +pre=200 post/callback=150 mode=0 +median=550 +NEXT: D1 AMPDU + +[TEST 34/37] D1_AMPDU_TX_OFF +delivery=198/200 +median_cycle=710 ms +median_connect=125 ms +p90=750 +result=FAIL +remaining=3 +best=VAL100_SAFE_200_300 cycle=710ms connect=125ms del=198/200 +change vs best=+0 ms +BEST NOW: +config=VAL100_SAFE_200_300 +pre=200 post/callback=300 mode=0 +median=710 +NEXT: STORAGE_RAM + +[TEST 35/37] STORAGE_RAM +delivery=198/200 +median_cycle=710 ms +median_connect=125 ms +p90=750 +result=FAIL +remaining=2 +best=VAL100_SAFE_200_300 cycle=710ms connect=125ms del=198/200 +change vs best=+0 ms +BEST NOW: +config=VAL100_SAFE_200_300 +pre=200 post/callback=300 mode=0 +median=710 +NEXT: write report + restore WPA3 + +[TEST 36/38] D1_AMPDU_TX_OFF_RERUN +delivery=17/20 +median_cycle=710 ms +median_connect=122 ms +p90=750 +result=FAIL +remaining=2 +best=VAL200 cycle=710ms connect=125ms del=198/200 +change vs best=+0 ms +BEST NOW: +config=VAL200 +pre=200 post/callback=300 mode=0 +median=710 +NEXT: STORAGE_RAM rerun + +[TEST 37/38] STORAGE_RAM_RERUN +delivery=19/20 +median_cycle=670 ms +median_connect=112 ms +p90=720 +result=FAIL +remaining=1 +best=VAL200 cycle=710ms connect=125ms del=198/200 +change vs best=-40 ms +BEST NOW: +config=VAL200 +pre=200 post/callback=300 mode=0 +median=710 +NEXT: finalize report + commit + diff --git a/experiments/fastest_state.json b/experiments/fastest_state.json new file mode 100644 index 0000000..9406351 --- /dev/null +++ b/experiments/fastest_state.json @@ -0,0 +1,27 @@ +{ + "last": "STORAGE_RAM_RERUN", + "result": { + "id": 95, + "n": 20, + "del": 19, + "plan": 20, + "conn": 112, + "cyc": 670, + "p90": 720, + "mx": 770, + "wr": 20, + "enc": 20, + "st": 20, + "nonce": 20, + "pre": 200, + "post": 300, + "assoc": 90, + "auth": 3, + "retry": 10, + "pm": 0, + "cba": 0, + "cbm": 0, + "samp": 19, + "name": "STORAGE_RAM_RERUN" + } +} \ No newline at end of file diff --git a/experiments/prepared_wifi_fastest_path.tsv b/experiments/prepared_wifi_fastest_path.tsv new file mode 100644 index 0000000..fa49715 --- /dev/null +++ b/experiments/prepared_wifi_fastest_path.tsv @@ -0,0 +1,38 @@ +Variant Association Auth PRE POST Delivered N ConnectMed CycleMed p90 max WifiReady Encode Sendto CbAny CbMatch PostMode +BASE 0x1a 7 200 300 20/20 20 262 840 920 960 20 20 20 0 0 0 +A1_BSSID 0x1b 7 200 300 19/20 20 278 840 870 920 20 20 20 0 0 0 +A2_FAST_SCAN 0x1e 7 200 300 20/20 20 270 850 880 1010 20 20 20 0 0 0 +A3_BSSID_FAST_SCAN 0x1f 7 200 300 19/20 20 276 860 880 890 20 20 20 0 0 0 +AUTH2_H2E 0x1a 7 200 300 18/20 20 269 840 910 950 20 20 20 0 0 0 +AUTH3_WPA2 0x1a 7 200 300 20/20 20 258 850 920 940 20 20 20 0 0 0 +AUTH3B_WPA3SAE_OFF 0x1a 7 200 300 20/20 20 253 830 900 940 20 20 20 0 0 0 +AUTH3B_WPA3SAE_OFF_VERIFIED 0x1a 3 200 300 20/20 20 128 710 810 910 20 20 20 0 0 0 +R0 0x1a 3 200 300 20/20 20 149 730 760 760 20 20 20 0 0 0 +R1 0x1a 3 200 300 20/20 20 149 720 750 780 20 20 20 0 0 0 +R3 0x1a 3 200 300 20/20 20 153 740 750 830 20 20 20 0 0 0 +CB0 0x9a 3 200 0 18/20 20 154 530 550 590 20 20 20 18 0 1 +PRE_200 0x1a 3 200 300 13/20 20 151 740 750 760 20 20 20 0 0 0 +PRE_200 0x1a 3 200 300 19/20 20 144 730 750 760 20 20 20 0 0 0 +PRE_200_R 0x1a 3 200 300 20/20 20 123 700 750 760 20 20 20 0 0 0 +PRE_150 0x1a 3 150 300 14/20 20 124 640 670 740 20 20 20 0 0 0 +POST_300 0x1a 3 200 300 20/20 20 120 700 740 780 20 20 20 0 0 0 +POST_250 0x1a 3 200 250 20/20 20 121 640 690 700 20 20 20 0 0 0 +POST_200 0x1a 3 200 200 20/20 20 132 620 650 790 20 20 20 0 0 0 +POST_150 0x1a 3 200 150 19/20 20 124 540 550 580 20 20 20 0 0 0 +POST_150_R 0x1a 3 200 150 20/20 20 125 550 580 590 20 20 20 0 0 0 +POST_100 0x1a 3 200 100 13/20 20 113 490 510 570 20 20 20 0 0 0 +2D_p175_q175 0x1a 3 175 175 18/20 20 127 530 550 560 20 20 20 0 0 0 +2D_p225_q125 0x1a 3 225 125 19/20 20 127 530 550 580 20 20 20 0 0 0 +2D_p150_q200 0x1a 3 150 200 20/20 20 125 540 580 600 20 20 20 0 0 0 +VAL100_PRIMARY 0x1a 3 200 150 94/100 100 131 560 620 900 100 100 100 0 0 0 +VAL100_SECONDARY 0x1a 3 150 200 97/100 100 131 560 610 650 100 100 100 0 0 0 +VAL100_SAFE_200_300 0x1a 3 200 300 98/100 100 138 730 770 930 100 100 100 0 0 0 +VAL100_SAFE_200_300_R 0x1a 3 200 300 96/100 100 124 720 750 840 100 100 100 0 0 0 +VAL100_MID_200_200 0x1a 3 200 200 91/100 100 125 620 660 790 100 100 100 0 0 0 +VAL100_FAST_200_150 0x1a 3 200 150 79/100 100 122 560 600 1210 100 100 100 0 0 0 +VAL100_FAST_150_200 0x1a 3 150 200 84/100 100 118 540 580 690 100 100 100 0 0 0 +VAL200 0x1a 3 200 300 198/200 200 125 710 750 870 200 200 200 0 0 0 +D1_AMPDU_TX_OFF 0x3a 3 200 300 198/200 200 125 710 750 870 20 20 20 0 0 0 +STORAGE_RAM 0x5a 3 200 300 198/200 200 125 710 750 870 20 20 20 0 0 0 +D1_AMPDU_TX_OFF_RERUN 0x3a 3 200 300 17/20 20 122 710 750 770 20 20 20 0 0 0 +STORAGE_RAM_RERUN 0x5a 3 200 300 19/20 20 112 670 720 770 20 20 20 0 0 0 diff --git a/experiments/run_fastest_continue.py b/experiments/run_fastest_continue.py new file mode 100644 index 0000000..becb231 --- /dev/null +++ b/experiments/run_fastest_continue.py @@ -0,0 +1,397 @@ +"""Resume fastest-path campaign after AUTH3B WPA2 SAE-off verified win.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from run_fastest_path import ( # noqa: E402 + CHAT, + RESULTS, + STATE, + cmake_configure, + ensure_receiver, + log, + ninja_build, + pass20, + run_test, + write_chat, +) + +# Winner association+auth from AUTH3B verified: BASE caches + real WPA2 +# (CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=n). Security decision later; report only. +CUR = { + "AE_EXP_FAST_PRE_MS": "200", + "AE_EXP_FAST_POST_MS": "300", + "AE_EXP_FAST_USE_BSSID": "0", + "AE_EXP_FAST_FAST_SCAN": "0", + "AE_EXP_FAST_AUTH": "2", + "AE_EXP_FAST_RETRY": "10", + "AE_EXP_FAST_POST_MODE": "0", + "AE_EXP_FAST_AMPDU_TX_OFF": "0", + "AE_EXP_FAST_STORAGE_RAM": "0", + "AE_EXP_FAST_DISABLE_WPA3": "1", +} + +BEST = { + "name": "AUTH3B_WPA3SAE_OFF_VERIFIED", + "cyc": 710, + "conn": 128, + "del": 20, + "plan": 20, + "pre": 200, + "post": 300, + "pm": 0, + "p90": 810, + "auth": 3, +} + +TEST_NO = 9 +REMAINING = 14 + + +def consider(name: str, r: dict) -> None: + global BEST + if not pass20(r) and r.get("plan", 20) <= 20 and r["del"] < r["plan"]: + return + if r["del"] < max(1, int(0.95 * r["plan"])): + return + clear = BEST is None or r["cyc"] + 20 < BEST["cyc"] + if clear or (BEST and r["cyc"] < BEST["cyc"] and r["del"] >= BEST["del"]): + BEST = dict(r) + BEST["name"] = name + + +def report(name: str, r: dict, nxt: str) -> None: + global TEST_NO, REMAINING + consider(name, r) + REMAINING = max(0, REMAINING - 1) + write_chat(TEST_NO, REMAINING, name, r, BEST, nxt) + TEST_NO += 1 + STATE.write_text( + json.dumps({"best": BEST, "cur": CUR, "last": name, "result": r}, indent=2), + encoding="utf-8", + ) + + +def force_wpa3_off() -> None: + """Ensure sdkconfig.defaults.wpa2only is applied via cmake flag.""" + sdk = Path( + r"C:\Users\nickc\Projects\temperature-sensor-prepared" + r"\build-esp32c6-save-bench-smoke\sdkconfig" + ) + text = sdk.read_text(encoding="utf-8") + text = text.replace( + "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", + "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set", + ) + text = text.replace( + "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", + "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set", + ) + sdk.write_text(text, encoding="utf-8") + + +def restore_wpa3() -> None: + sdk = Path( + r"C:\Users\nickc\Projects\temperature-sensor-prepared" + r"\build-esp32c6-save-bench-smoke\sdkconfig" + ) + text = sdk.read_text(encoding="utf-8") + text = text.replace( + "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set", + "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", + ) + text = text.replace( + "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set", + "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", + ) + if "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y" not in text: + text += "\nCONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y\n" + sdk.write_text(text, encoding="utf-8") + cmake_configure( + { + **CUR, + "AE_EXP_FAST_DISABLE_WPA3": "", + "AE_EXP_FAST_AUTH": "0", + "AE_EXP_FAST_TEST_ID": "99", + } + ) + # re-patch after cmake may re-enable from defaults without overlay + text = sdk.read_text(encoding="utf-8") + if "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y" not in text: + text = text.replace( + "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set", + "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", + ) + sdk.write_text(text, encoding="utf-8") + cmake_configure( + { + **CUR, + "AE_EXP_FAST_DISABLE_WPA3": "", + "AE_EXP_FAST_AUTH": "0", + "AE_EXP_FAST_TEST_ID": "99", + } + ) + log("WPA3 SAE restore attempted") + + +def main() -> int: + log("=== FASTEST PATH CONTINUE from R0 ===") + ensure_receiver() + force_wpa3_off() + + # 6. Retry + r0 = run_test("R0", {**CUR, "AE_EXP_FAST_TEST_ID": "8", "AE_EXP_FAST_RETRY": "0"}, 20) + report("R0", r0, "R1 retry=1 x20") + r1 = run_test("R1", {**CUR, "AE_EXP_FAST_TEST_ID": "9", "AE_EXP_FAST_RETRY": "1"}, 20) + report("R1", r1, "R3 retry=3 x20") + r3 = run_test("R3", {**CUR, "AE_EXP_FAST_TEST_ID": "10", "AE_EXP_FAST_RETRY": "3"}, 20) + report("R3", r3, "CB0 tx-done callback POST=0 x20") + + retry = 10 + if pass20(r0) and r0["wr"] >= 19: + retry = 0 + elif pass20(r1) and r1["wr"] >= 19: + retry = 1 + elif pass20(r3) and r3["wr"] >= 19: + retry = 3 + # Prefer smallest retry that keeps delivery; don't pick by median alone + CUR["AE_EXP_FAST_RETRY"] = str(retry) + log(f"retry selected {retry}") + + # 7. Callback + cb0 = run_test( + "CB0", + { + **CUR, + "AE_EXP_FAST_TEST_ID": "11", + "AE_EXP_FAST_POST_MODE": "1", + "AE_EXP_FAST_POST_MS": "0", + }, + 20, + ) + cb_usable = pass20(cb0) and cb0.get("cbm", 0) >= 15 + if not cb_usable: + log("CALLBACK_NOT_USABLE") + CUR["AE_EXP_FAST_POST_MODE"] = "0" + CUR["AE_EXP_FAST_POST_MS"] = "300" + report("CB0", cb0, "PRE sweep from 200ms (fixed POST=300)") + else: + CUR["AE_EXP_FAST_POST_MODE"] = "1" + CUR["AE_EXP_FAST_POST_MS"] = "0" + consider("CB0", cb0) + if cb0["del"] < 20: + report("CB0", cb0, "CB1 callback+10ms") + cb1 = run_test( + "CB1_10", + {**CUR, "AE_EXP_FAST_TEST_ID": "12", "AE_EXP_FAST_POST_MODE": "2"}, + 20, + ) + if pass20(cb1): + CUR["AE_EXP_FAST_POST_MODE"] = "2" + report("CB1_10", cb1, "PRE sweep") + else: + report("CB1_10", cb1, "CB2 callback+25ms") + cb2 = run_test( + "CB2_25", + {**CUR, "AE_EXP_FAST_TEST_ID": "13", "AE_EXP_FAST_POST_MODE": "3"}, + 20, + ) + if pass20(cb2): + CUR["AE_EXP_FAST_POST_MODE"] = "3" + report("CB2_25", cb2, "PRE sweep") + else: + CUR["AE_EXP_FAST_POST_MODE"] = "0" + CUR["AE_EXP_FAST_POST_MS"] = "300" + report("CB2_25", cb2, "callback weak — PRE with POST=300") + else: + report("CB0", cb0, "PRE sweep (callback POST)") + + # 8. PRE sweep + best_pre = int(CUR["AE_EXP_FAST_PRE_MS"]) + pre_vals = [200, 150, 100, 75, 50, 25, 0] + for i, pre in enumerate(pre_vals): + name = f"PRE_{pre}" + rr = run_test( + name, + {**CUR, "AE_EXP_FAST_TEST_ID": str(20 + i), "AE_EXP_FAST_PRE_MS": str(pre)}, + 20, + ) + nxt = f"PRE_{pre_vals[i+1]}" if i + 1 < len(pre_vals) else "POST sweep or 2D" + if rr["del"] >= 20: + report(name, rr, nxt) + best_pre = pre + continue + if rr["del"] == 19: + report(name, rr, name + " repeat") + rr2 = run_test( + name + "_R", + { + **CUR, + "AE_EXP_FAST_TEST_ID": str(40 + i), + "AE_EXP_FAST_PRE_MS": str(pre), + }, + 20, + ) + if rr2["del"] >= 19: + report(name + "_R", rr2, nxt) + best_pre = pre + continue + report(name, rr, f"PRE stop at {pre}; use best_pre={best_pre}") + log(f"PRE {pre} too aggressive — stop") + break + CUR["AE_EXP_FAST_PRE_MS"] = str(best_pre) + + # 9. POST sweep if fixed delay + best_post = int(CUR.get("AE_EXP_FAST_POST_MS", "300")) + if CUR.get("AE_EXP_FAST_POST_MODE", "0") == "0": + post_vals = [300, 250, 200, 150, 100, 75, 50, 25, 0] + for i, post in enumerate(post_vals): + name = f"POST_{post}" + rr = run_test( + name, + { + **CUR, + "AE_EXP_FAST_TEST_ID": str(50 + i), + "AE_EXP_FAST_POST_MS": str(post), + }, + 20, + ) + nxt = ( + f"POST_{post_vals[i+1]}" + if i + 1 < len(post_vals) + else "2D neighbors" + ) + if rr["del"] >= 20: + report(name, rr, nxt) + best_post = post + continue + if rr["del"] == 19: + report(name, rr, name + " repeat") + rr2 = run_test( + name + "_R", + { + **CUR, + "AE_EXP_FAST_TEST_ID": str(60 + i), + "AE_EXP_FAST_POST_MS": str(post), + }, + 20, + ) + if rr2["del"] >= 19: + report(name + "_R", rr2, nxt) + best_post = post + continue + report(name, rr, f"POST stop; best_post={best_post}") + break + CUR["AE_EXP_FAST_POST_MS"] = str(best_post) + + # 10. 2D neighbors + p = int(CUR["AE_EXP_FAST_PRE_MS"]) + q = int(CUR.get("AE_EXP_FAST_POST_MS", "0")) + neighbors = [] + if CUR.get("AE_EXP_FAST_POST_MODE", "0") == "0": + neighbors = [ + (max(0, p - 25), q + 25), + (p + 25, max(0, q - 25)), + (max(0, p - 50), q + 50), + (p + 50, max(0, q - 50)), + ] + for i, (pp, qq) in enumerate(neighbors): + name = f"2D_p{pp}_q{qq}" + rr = run_test( + name, + { + **CUR, + "AE_EXP_FAST_TEST_ID": str(70 + i), + "AE_EXP_FAST_PRE_MS": str(pp), + "AE_EXP_FAST_POST_MS": str(qq), + }, + 20, + ) + report(name, rr, "next 2D or VAL100") + if pass20(rr) and BEST and BEST.get("name") == name: + CUR["AE_EXP_FAST_PRE_MS"] = str(pp) + CUR["AE_EXP_FAST_POST_MS"] = str(qq) + + # Align CUR to BEST delays if best is a PRE/POST/2D variant + if BEST: + if "pre" in BEST: + CUR["AE_EXP_FAST_PRE_MS"] = str(BEST["pre"]) + if "post" in BEST and CUR.get("AE_EXP_FAST_POST_MODE", "0") == "0": + CUR["AE_EXP_FAST_POST_MS"] = str(BEST["post"]) + if "pm" in BEST: + CUR["AE_EXP_FAST_POST_MODE"] = str(BEST["pm"]) + + # 11. Validation + log(f"validation flags={CUR}") + v100 = run_test( + "VAL100", {**CUR, "AE_EXP_FAST_TEST_ID": "80"}, 100, timeout_s=45 * 100 + 300 + ) + report("VAL100", v100, "VAL200 or VAL100_SAFE") + if v100["del"] < 98: + cur2 = dict(CUR) + cur2["AE_EXP_FAST_PRE_MS"] = "200" + if cur2.get("AE_EXP_FAST_POST_MODE", "0") == "0": + cur2["AE_EXP_FAST_POST_MS"] = "300" + v100b = run_test( + "VAL100_SAFE", + {**cur2, "AE_EXP_FAST_TEST_ID": "81"}, + 100, + timeout_s=45 * 100 + 300, + ) + report("VAL100_SAFE", v100b, "VAL200") + if v100b["del"] > v100["del"]: + CUR.update(cur2) + v100 = v100b + if v100["del"] == 98: + v100r = run_test( + "VAL100_REPEAT", + {**CUR, "AE_EXP_FAST_TEST_ID": "82"}, + 100, + timeout_s=45 * 100 + 300, + ) + report("VAL100_REPEAT", v100r, "VAL200") + + v200 = run_test( + "VAL200", {**CUR, "AE_EXP_FAST_TEST_ID": "90"}, 200, timeout_s=45 * 200 + 300 + ) + report("VAL200", v200, "D1 AMPDU TX off") + + # 12. AMPDU + d1 = run_test( + "D1_AMPDU_TX_OFF", + {**CUR, "AE_EXP_FAST_TEST_ID": "91", "AE_EXP_FAST_AMPDU_TX_OFF": "1"}, + 20, + ) + report("D1_AMPDU_TX_OFF", d1, "STORAGE_RAM") + + # 13. storage RAM + ram = run_test( + "STORAGE_RAM", + {**CUR, "AE_EXP_FAST_TEST_ID": "92", "AE_EXP_FAST_STORAGE_RAM": "1"}, + 20, + ) + report("STORAGE_RAM", ram, "write report + restore WPA3") + + log("=== SCREENING COMPLETE ===") + log(f"BEST {BEST}") + log(f"VAL200 {v200}") + STATE.write_text( + json.dumps({"best": BEST, "cur": CUR, "val200": v200}, indent=2), + encoding="utf-8", + ) + + # Restore WPA3 SAE for build tree (do not leave production-weakened) + restore_wpa3() + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as ex: + log(f"FATAL {ex}") + raise diff --git a/experiments/run_fastest_continue2.py b/experiments/run_fastest_continue2.py new file mode 100644 index 0000000..5a29067 --- /dev/null +++ b/experiments/run_fastest_continue2.py @@ -0,0 +1,350 @@ +"""Resume from PRE sweep with retry=10 (R0 caused 13/20 on PRE_200).""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from run_fastest_path import ( # noqa: E402 + STATE, + append_tsv, + ensure_receiver, + log, + pass20, + run_test, + write_chat, +) + +# Record orphaned PRE_200 retry=0 failure if not already in TSV +RESULTS = Path(__file__).resolve().parent / "prepared_wifi_fastest_path.tsv" +tsv = RESULTS.read_text(encoding="utf-8") if RESULTS.exists() else "" +if "PRE_200\t" not in tsv and "PRE_200_R0FAIL" not in tsv: + append_tsv( + "PRE_200_R0FAIL", + { + "assoc": 0x1A, + "auth": 3, + "pre": 200, + "post": 300, + "del": 13, + "plan": 20, + "n": 20, + "conn": 151, + "cyc": 740, + "p90": 750, + "mx": 760, + "wr": 20, + "enc": 20, + "st": 20, + "cba": 0, + "cbm": 0, + "pm": 0, + }, + ) + +CUR = { + "AE_EXP_FAST_PRE_MS": "200", + "AE_EXP_FAST_POST_MS": "300", + "AE_EXP_FAST_USE_BSSID": "0", + "AE_EXP_FAST_FAST_SCAN": "0", + "AE_EXP_FAST_AUTH": "2", + "AE_EXP_FAST_RETRY": "10", # R0 unreliable (13/20) + "AE_EXP_FAST_POST_MODE": "0", + "AE_EXP_FAST_AMPDU_TX_OFF": "0", + "AE_EXP_FAST_STORAGE_RAM": "0", + "AE_EXP_FAST_DISABLE_WPA3": "1", +} + +BEST = { + "name": "AUTH3B_WPA3SAE_OFF_VERIFIED", + "cyc": 710, + "conn": 128, + "del": 20, + "plan": 20, + "pre": 200, + "post": 300, + "pm": 0, + "p90": 810, + "auth": 3, +} + +TEST_NO = 13 +REMAINING = 12 + + +def consider(name: str, r: dict) -> None: + global BEST + if r["del"] < r["plan"] and r["plan"] <= 20: + return + if r["plan"] > 20 and r["del"] < int(0.95 * r["plan"]): + return + if BEST is None or r["cyc"] + 20 < BEST["cyc"] or ( + r["cyc"] < BEST["cyc"] and r["del"] >= BEST.get("del", 0) + ): + BEST = dict(r) + BEST["name"] = name + + +def report(name: str, r: dict, nxt: str) -> None: + global TEST_NO, REMAINING + consider(name, r) + REMAINING = max(0, REMAINING - 1) + write_chat(TEST_NO, REMAINING, name, r, BEST, nxt) + TEST_NO += 1 + STATE.write_text( + json.dumps({"best": BEST, "cur": CUR, "last": name, "result": r}, indent=2), + encoding="utf-8", + ) + + +def force_wpa3_off() -> None: + sdk = Path( + r"C:\Users\nickc\Projects\temperature-sensor-prepared" + r"\build-esp32c6-save-bench-smoke\sdkconfig" + ) + text = sdk.read_text(encoding="utf-8") + text = text.replace( + "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", + "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set", + ) + text = text.replace( + "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", + "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set", + ) + sdk.write_text(text, encoding="utf-8") + + +def restore_wpa3() -> None: + from run_fastest_path import cmake_configure + + sdk = Path( + r"C:\Users\nickc\Projects\temperature-sensor-prepared" + r"\build-esp32c6-save-bench-smoke\sdkconfig" + ) + text = sdk.read_text(encoding="utf-8") + text = text.replace( + "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set", + "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", + ) + text = text.replace( + "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set", + "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", + ) + if "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y" not in text: + text += "\nCONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y\n" + sdk.write_text(text, encoding="utf-8") + cmake_configure( + { + **CUR, + "AE_EXP_FAST_DISABLE_WPA3": "", + "AE_EXP_FAST_AUTH": "0", + "AE_EXP_FAST_TEST_ID": "99", + } + ) + log("WPA3 SAE restore attempted") + + +def main() -> int: + global TEST_NO + log("=== CONTINUE PRE sweep retry=10 (R0 was unreliable) ===") + write_chat( + 13, + 12, + "PRE_200_R0FAIL", + { + "del": 13, + "plan": 20, + "cyc": 740, + "conn": 151, + "p90": 750, + "pre": 200, + "post": 300, + "pm": 0, + "cbm": 0, + }, + BEST, + "PRE_200 with retry=10", + ) + TEST_NO = 14 + ensure_receiver() + force_wpa3_off() + + best_pre = 200 + pre_vals = [200, 150, 100, 75, 50, 25, 0] + for i, pre in enumerate(pre_vals): + name = f"PRE_{pre}" + rr = run_test( + name, + {**CUR, "AE_EXP_FAST_TEST_ID": str(20 + i), "AE_EXP_FAST_PRE_MS": str(pre)}, + 20, + timeout_s=900, + ) + nxt = f"PRE_{pre_vals[i+1]}" if i + 1 < len(pre_vals) else "POST sweep" + if rr["del"] >= 20: + report(name, rr, nxt) + best_pre = pre + continue + if rr["del"] == 19: + report(name, rr, name + " repeat") + rr2 = run_test( + name + "_R", + { + **CUR, + "AE_EXP_FAST_TEST_ID": str(40 + i), + "AE_EXP_FAST_PRE_MS": str(pre), + }, + 20, + timeout_s=900, + ) + if rr2["del"] >= 19: + report(name + "_R", rr2, nxt) + best_pre = pre + continue + report(name + "_R", rr2, f"PRE stop; best_pre={best_pre}") + break + report(name, rr, f"PRE stop at {pre}; best_pre={best_pre}") + log(f"PRE {pre} too aggressive — stop") + break + CUR["AE_EXP_FAST_PRE_MS"] = str(best_pre) + + best_post = 300 + post_vals = [300, 250, 200, 150, 100, 75, 50, 25, 0] + for i, post in enumerate(post_vals): + name = f"POST_{post}" + rr = run_test( + name, + { + **CUR, + "AE_EXP_FAST_TEST_ID": str(50 + i), + "AE_EXP_FAST_POST_MS": str(post), + }, + 20, + timeout_s=900, + ) + nxt = f"POST_{post_vals[i+1]}" if i + 1 < len(post_vals) else "2D" + if rr["del"] >= 20: + report(name, rr, nxt) + best_post = post + continue + if rr["del"] == 19: + report(name, rr, name + " repeat") + rr2 = run_test( + name + "_R", + { + **CUR, + "AE_EXP_FAST_TEST_ID": str(60 + i), + "AE_EXP_FAST_POST_MS": str(post), + }, + 20, + timeout_s=900, + ) + if rr2["del"] >= 19: + report(name + "_R", rr2, nxt) + best_post = post + continue + report(name + "_R", rr2, f"POST stop; best_post={best_post}") + break + report(name, rr, f"POST stop; best_post={best_post}") + break + CUR["AE_EXP_FAST_POST_MS"] = str(best_post) + + p = int(CUR["AE_EXP_FAST_PRE_MS"]) + q = int(CUR["AE_EXP_FAST_POST_MS"]) + for i, (pp, qq) in enumerate( + [ + (max(0, p - 25), q + 25), + (p + 25, max(0, q - 25)), + (max(0, p - 50), q + 50), + (p + 50, max(0, q - 50)), + ] + ): + name = f"2D_p{pp}_q{qq}" + rr = run_test( + name, + { + **CUR, + "AE_EXP_FAST_TEST_ID": str(70 + i), + "AE_EXP_FAST_PRE_MS": str(pp), + "AE_EXP_FAST_POST_MS": str(qq), + }, + 20, + timeout_s=900, + ) + report(name, rr, "next 2D or VAL100") + if pass20(rr) and BEST and BEST.get("name") == name: + CUR["AE_EXP_FAST_PRE_MS"] = str(pp) + CUR["AE_EXP_FAST_POST_MS"] = str(qq) + + if BEST: + CUR["AE_EXP_FAST_PRE_MS"] = str(BEST.get("pre", CUR["AE_EXP_FAST_PRE_MS"])) + CUR["AE_EXP_FAST_POST_MS"] = str(BEST.get("post", CUR["AE_EXP_FAST_POST_MS"])) + + log(f"validation flags={CUR}") + v100 = run_test( + "VAL100", {**CUR, "AE_EXP_FAST_TEST_ID": "80"}, 100, timeout_s=45 * 100 + 600 + ) + report("VAL100", v100, "VAL200 or SAFE") + if v100["del"] < 98: + cur2 = dict(CUR) + cur2["AE_EXP_FAST_PRE_MS"] = "200" + cur2["AE_EXP_FAST_POST_MS"] = "300" + v100b = run_test( + "VAL100_SAFE", + {**cur2, "AE_EXP_FAST_TEST_ID": "81"}, + 100, + timeout_s=45 * 100 + 600, + ) + report("VAL100_SAFE", v100b, "VAL200") + if v100b["del"] > v100["del"]: + CUR.update(cur2) + v100 = v100b + if v100["del"] == 98: + v100r = run_test( + "VAL100_REPEAT", + {**CUR, "AE_EXP_FAST_TEST_ID": "82"}, + 100, + timeout_s=45 * 100 + 600, + ) + report("VAL100_REPEAT", v100r, "VAL200") + + v200 = run_test( + "VAL200", {**CUR, "AE_EXP_FAST_TEST_ID": "90"}, 200, timeout_s=45 * 200 + 600 + ) + report("VAL200", v200, "D1 AMPDU") + + d1 = run_test( + "D1_AMPDU_TX_OFF", + {**CUR, "AE_EXP_FAST_TEST_ID": "91", "AE_EXP_FAST_AMPDU_TX_OFF": "1"}, + 20, + timeout_s=900, + ) + report("D1_AMPDU_TX_OFF", d1, "STORAGE_RAM") + + ram = run_test( + "STORAGE_RAM", + {**CUR, "AE_EXP_FAST_TEST_ID": "92", "AE_EXP_FAST_STORAGE_RAM": "1"}, + 20, + timeout_s=900, + ) + report("STORAGE_RAM", ram, "report + restore WPA3") + + # LWIP IRAM A/B via sdkconfig patch if needed — deferred to report writer + log("=== SCREENING COMPLETE ===") + log(f"BEST {BEST}") + log(f"VAL200 {v200}") + STATE.write_text( + json.dumps({"best": BEST, "cur": CUR, "val200": v200}, indent=2), + encoding="utf-8", + ) + restore_wpa3() + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as ex: + log(f"FATAL {ex}") + raise diff --git a/experiments/run_fastest_path.py b/experiments/run_fastest_path.py new file mode 100644 index 0000000..229ff9a --- /dev/null +++ b/experiments/run_fastest_path.py @@ -0,0 +1,655 @@ +"""Silent fastest-path prepared Wi-Fi campaign orchestrator (ESP32-C6).""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared") +BUILD = ROOT / "build-esp32c6-save-bench-smoke" +AETHER = r"C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0" +PY = Path(r"C:\Espressif\python_env\idf6.0_py3.11_env\Scripts\python.exe") +CMAKE = Path(r"C:\Espressif\tools\cmake\3.30.2\bin\cmake.exe") +NINJA = Path(r"C:\Espressif\tools\ninja\1.12.1\ninja.exe") +RX_EXE = ROOT / "temperature_receiver" / "build-bisect" / "temperature_receiver.exe" +RX_SESSION = ROOT / "experiments" / "prepared_wifi_cache_rx_session" +RX_LOG = ROOT / "experiments" / "prepared_wifi_fastest_rx.log" +RESULTS = ROOT / "experiments" / "prepared_wifi_fastest_path.tsv" +PROGRESS = ROOT / "experiments" / "fastest_progress.log" +STATE = ROOT / "experiments" / "fastest_state.json" +CHAT = ROOT / "experiments" / "fastest_chat.txt" + +IDF_PATH = r"C:\Espressif\frameworks\esp-idf-v6.0.2" +CCACHE = r"C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64" + +BASE_CMAKE = { + "CPM_aether-client-cpp_SOURCE": AETHER, + "AE_EXP_PREPARED_WIFI_FASTEST": "1", + "AE_EXP_PREPARED_WIFI_BISECT": "", + "AE_EXP_BISECT_CONSOLE": "", + "AE_EXP_BISECT_SMOKE": "", + "AE_EXP_SKIP_DTOR_SAVE": "1", + "SERVICE_UID": "5aade50f-00d9-4624-b097-e203cdcf1e38", + "BENCH_CLIENT_ID": "prepared_wifi_bisect_v1", + "WIFI_SSID": "chirkov", + "WIFI_PASSWORD": "kcdjepWz51", + "AETHER_PREPARED_POST_SEND_HOLD_MS": "300", + "AE_EXP_FAST_DISABLE_WPA3": "", +} + +RESULT_RE = re.compile( + r"TEST_RESULT test_id=(?P\d+) n=(?P\d+) delivered=(?P\d+)/(?P\d+) " + r"connect_med_ms=(?P\d+) cycle_med_ms=(?P\d+) p90_ms=(?P\d+) " + r"max_ms=(?P\d+) wifi_ready=(?P\d+) encode=(?P\d+) sendto=(?P\d+) " + r"nonce=(?P\d+) pre=(?P
\d+) post=(?P\d+) assoc=0x(?P[0-9a-fA-F]+) "
+    r"auth=(?P\d+) retry=(?P\d+) post_mode=(?P\d+) cb_any=(?P\d+) "
+    r"cb_match=(?P\d+) samples=(?P\d+)"
+)
+
+
+def env() -> dict:
+    e = os.environ.copy()
+    e["IDF_PATH"] = IDF_PATH
+    e["IDF_TOOLS_PATH"] = r"C:\Espressif"
+    extra = [
+        CCACHE,
+        r"C:\Espressif\tools\ninja\1.12.1",
+        r"C:\Espressif\tools\cmake\3.30.2\bin",
+        r"C:\msys64\ucrt64\bin",
+    ]
+    e["Path"] = ";".join(extra) + ";" + e.get("Path", "")
+    e.pop("CCACHE_DISABLE", None)
+    return e
+
+
+def log(msg: str) -> None:
+    line = time.strftime("%H:%M:%S") + " " + msg
+    print(line, flush=True)
+    PROGRESS.parent.mkdir(parents=True, exist_ok=True)
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(line + "\n")
+
+
+def write_chat(test_no: int, remaining: int, name: str, r: dict, best: dict | None, nxt: str) -> None:
+    result = "PASS" if r["del"] == r["plan"] and r["plan"] > 0 else "FAIL"
+    if name.startswith("CB") and r.get("cbm", 0) < 15:
+        result = "CALLBACK_NOT_USABLE"
+    best_s = "none"
+    if best:
+        best_s = (
+            f"{best.get('name','?')} cycle={best.get('cyc')}ms "
+            f"connect={best.get('conn')}ms del={best.get('del')}/{best.get('plan')}"
+        )
+    delta = ""
+    if best and best.get("name") != name:
+        delta = f"change vs best={r['cyc'] - best['cyc']:+d} ms"
+    elif best and best.get("name") == name:
+        delta = "change vs best=NEW BEST"
+    lines = [
+        f"[TEST {test_no}/{test_no + remaining}] {name}",
+        f"delivery={r['del']}/{r['plan']}",
+        f"median_cycle={r['cyc']} ms",
+        f"median_connect={r['conn']} ms",
+        f"p90={r['p90']}",
+        f"result={result}",
+        f"remaining={remaining}",
+        f"best={best_s}",
+        delta,
+        "BEST NOW:",
+        f"config={best.get('name') if best else 'none'}",
+        f"pre={best.get('pre') if best else '-'} post/callback={best.get('post') if best else '-'} mode={best.get('pm') if best else '-'}",
+        f"median={best.get('cyc') if best else '-'}",
+        f"NEXT: {nxt}",
+        "",
+    ]
+    text = "\n".join(x for x in lines if x is not None)
+    with CHAT.open("a", encoding="utf-8") as f:
+        f.write(text + "\n")
+    log("CHAT\n" + text)
+
+
+def cmake_configure(flags: dict) -> None:
+    args = [str(CMAKE), "-S", str(ROOT), "-B", str(BUILD), "-G", "Ninja"]
+    merged = dict(BASE_CMAKE)
+    merged.update(flags)
+    for k, v in merged.items():
+        args.append(f"-D{k}={v}")
+    log("cmake " + " ".join(f"{k}={v}" for k, v in flags.items()))
+    r = subprocess.run(args, cwd=ROOT, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "fastest_cmake.err").write_text(
+            r.stdout + "\n" + r.stderr, encoding="utf-8"
+        )
+        raise RuntimeError("cmake failed")
+
+
+def ninja_build() -> None:
+    r = subprocess.run(
+        [str(NINJA), "-C", str(BUILD)],
+        env=env(),
+        capture_output=True,
+        text=True,
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "fastest_build.err").write_text(
+            r.stdout[-8000:] + "\n" + r.stderr[-8000:], encoding="utf-8"
+        )
+        raise RuntimeError("ninja failed")
+    log("build ok")
+
+
+def flash() -> None:
+    cmd = [
+        str(PY),
+        "-m",
+        "esptool",
+        "--chip",
+        "esp32c6",
+        "-p",
+        "COM7",
+        "-b",
+        "460800",
+        "write-flash",
+        "--flash-size",
+        "4MB",
+        "0x0",
+        str(BUILD / "bootloader" / "bootloader.bin"),
+        "0x8000",
+        str(BUILD / "partition_table" / "partition-table.bin"),
+        "0x10000",
+        str(BUILD / "temperature_sensor.bin"),
+    ]
+    r = subprocess.run(cmd, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "fastest_flash.err").write_text(
+            r.stdout + "\n" + r.stderr, encoding="utf-8"
+        )
+        raise RuntimeError("flash failed")
+    log("flash ok")
+
+
+def ensure_receiver() -> None:
+    # leave running if alive
+    try:
+        subprocess.run(
+            ["tasklist", "/FI", "IMAGENAME eq temperature_receiver.exe"],
+            capture_output=True,
+            text=True,
+            check=False,
+        )
+    except Exception:
+        pass
+    out = subprocess.run(
+        ["tasklist", "/FI", "IMAGENAME eq temperature_receiver.exe"],
+        capture_output=True,
+        text=True,
+    ).stdout
+    if "temperature_receiver.exe" in out:
+        return
+    RX_SESSION.mkdir(parents=True, exist_ok=True)
+    env2 = env()
+    env2["AE_RECEIVER_SESSION_DIR"] = str(RX_SESSION)
+    RX_LOG.parent.mkdir(parents=True, exist_ok=True)
+    # append mode via Start-Process equivalent
+    with RX_LOG.open("a", encoding="utf-8") as outf, (
+        ROOT / "experiments" / "prepared_wifi_fastest_rx.log.err"
+    ).open("a", encoding="utf-8") as errf:
+        subprocess.Popen(
+            [str(RX_EXE)],
+            cwd=str(RX_SESSION),
+            env=env2,
+            stdout=outf,
+            stderr=errf,
+        )
+    time.sleep(4)
+    log("receiver started")
+
+
+def wait_result(
+    prev_count: int,
+    timeout_s: int,
+    expect_id: int | None = None,
+    expect_n: int | None = None,
+) -> dict:
+    deadline = time.time() + timeout_s
+    last_hb = 0.0
+    while time.time() < deadline:
+        now = time.time()
+        if now - last_hb >= 30:
+            last_hb = now
+            extra = ""
+            if RX_LOG.exists():
+                tail = RX_LOG.read_text(encoding="utf-8", errors="replace")[-400:]
+                extra = " rx_tail=" + tail.replace("\n", " | ")[-200:]
+            log(f"waiting TEST_RESULT prev={prev_count} left={int(deadline-now)}s{extra}")
+        if RX_LOG.exists():
+            text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+            matches = list(RESULT_RE.finditer(text))
+            if len(matches) > prev_count:
+                # Prefer the newest match that satisfies expect_* filters.
+                for m in reversed(matches[prev_count:]):
+                    d = {
+                        k: int(v, 16) if k == "assoc" else int(v)
+                        for k, v in m.groupdict().items()
+                    }
+                    if expect_id is not None and d["id"] != expect_id:
+                        continue
+                    if expect_n is not None and d["n"] != expect_n:
+                        continue
+                    return d
+                # New lines exist but none match filters yet — keep waiting.
+        time.sleep(2)
+    raise TimeoutError("no TEST_RESULT")
+
+
+def append_tsv(name: str, r: dict) -> None:
+    new = not RESULTS.exists()
+    with RESULTS.open("a", encoding="utf-8") as f:
+        if new:
+            f.write(
+                "Variant\tAssociation\tAuth\tPRE\tPOST\tDelivered\tN\t"
+                "ConnectMed\tCycleMed\tp90\tmax\tWifiReady\tEncode\tSendto\t"
+                "CbAny\tCbMatch\tPostMode\n"
+            )
+        assoc = f"0x{r['assoc']:02x}"
+        f.write(
+            f"{name}\t{assoc}\t{r['auth']}\t{r['pre']}\t{r['post']}\t"
+            f"{r['del']}/{r['plan']}\t{r['n']}\t{r['conn']}\t{r['cyc']}\t"
+            f"{r['p90']}\t{r['mx']}\t{r['wr']}\t{r['enc']}\t{r['st']}\t"
+            f"{r['cba']}\t{r['cbm']}\t{r['pm']}\n"
+        )
+
+
+def count_results() -> int:
+    if not RX_LOG.exists():
+        return 0
+    return len(RESULT_RE.findall(RX_LOG.read_text(encoding="utf-8", errors="replace")))
+
+
+def run_test(
+    name: str,
+    flags: dict,
+    n: int = 20,
+    timeout_s: int | None = None,
+    rebuild: bool = True,
+) -> dict:
+    flags = dict(flags)
+    flags.setdefault("AE_EXP_FAST_N", str(n))
+    flags.setdefault("AETHER_PREPARED_NONCE_RESERVE", str(max(n, 20)))
+    ensure_receiver()
+    prev = count_results()
+    if rebuild:
+        cmake_configure(flags)
+        ninja_build()
+    flash()
+    to = timeout_s if timeout_s is not None else max(180, 25 * n + 180)
+    expect_id = None
+    try:
+        expect_id = int(flags.get("AE_EXP_FAST_TEST_ID") or 0) or None
+    except Exception:
+        expect_id = None
+    r = wait_result(prev, to, expect_id=expect_id, expect_n=n)
+    r["name"] = name
+    append_tsv(name, r)
+    log(
+        f"[TEST] {name} delivery={r['del']}/{r['plan']} "
+        f"median_cycle={r['cyc']} ms median_connect={r['conn']} ms "
+        f"p90={r['p90']} wifi_ready={r['wr']}/{r['plan']}"
+    )
+    STATE.write_text(json.dumps({"last": name, "result": r}, indent=2), encoding="utf-8")
+    return r
+
+
+def pass20(r: dict) -> bool:
+    return r["del"] == r["plan"] and r["plan"] > 0
+
+
+def main() -> int:
+    RESULTS.parent.mkdir(parents=True, exist_ok=True)
+    log("=== FASTEST PATH CAMPAIGN START ===")
+
+    remaining = 17
+    best = None
+
+    def consider(name: str, r: dict) -> None:
+        nonlocal best
+        if not pass20(r) and r["n"] <= 20:
+            return
+        if best is None or r["cyc"] < best["cyc"] or (
+            r["cyc"] == best["cyc"] and r["del"] > best["del"]
+        ):
+            if r["cyc"] + 20 < (best["cyc"] if best else 10**9) or best is None:
+                best = dict(r)
+                best["name"] = name
+            elif best and abs(r["cyc"] - best["cyc"]) <= 20:
+                # not a clear improvement
+                if r["del"] > best["del"]:
+                    best = dict(r)
+                    best["name"] = name
+
+    # 3. BASE
+    base_flags = {
+        "AE_EXP_FAST_TEST_ID": "1",
+        "AE_EXP_FAST_PRE_MS": "200",
+        "AE_EXP_FAST_POST_MS": "300",
+        "AE_EXP_FAST_USE_BSSID": "0",
+        "AE_EXP_FAST_FAST_SCAN": "0",
+        "AE_EXP_FAST_AUTH": "0",
+        "AE_EXP_FAST_RETRY": "10",
+        "AE_EXP_FAST_POST_MODE": "0",
+        "AE_EXP_FAST_AMPDU_TX_OFF": "0",
+        "AE_EXP_FAST_STORAGE_RAM": "0",
+    }
+    r = run_test("BASE", base_flags, 20)
+    if r["del"] < 20:
+        log("BASE not 20/20 — repeating")
+        r = run_test("BASE_REPEAT", base_flags, 20)
+    if r["cyc"] > 1100 and r["del"] >= 18:
+        log("WARN BASE median much worse than C6/C8 ~850ms")
+    consider("BASE", r)
+    remaining -= 1
+    log(f"BEST NOW {best}")
+
+    # 4. A1 A2 A3
+    a_tests = [
+        ("A1_BSSID", {**base_flags, "AE_EXP_FAST_TEST_ID": "2", "AE_EXP_FAST_USE_BSSID": "1"}),
+        ("A2_FAST_SCAN", {**base_flags, "AE_EXP_FAST_TEST_ID": "3", "AE_EXP_FAST_FAST_SCAN": "1"}),
+        ("A3_BSSID_FAST_SCAN", {
+            **base_flags,
+            "AE_EXP_FAST_TEST_ID": "4",
+            "AE_EXP_FAST_USE_BSSID": "1",
+            "AE_EXP_FAST_FAST_SCAN": "1",
+        }),
+    ]
+    assoc_best_flags = dict(base_flags)
+    assoc_best_name = "BASE"
+    for name, flags in a_tests:
+        rr = run_test(name, flags, 20)
+        consider(name, rr)
+        remaining -= 1
+        if pass20(rr) and best and best.get("name") == name:
+            assoc_best_flags = dict(flags)
+            assoc_best_name = name
+        log(f"BEST NOW {best}")
+
+    # Keep BASE flags if A* not clearly faster
+    if best and best.get("name") in ("BASE", "BASE_REPEAT"):
+        assoc_best_flags = dict(base_flags)
+        assoc_best_name = "BASE"
+
+    # 5 AUTH
+    auth_flags = dict(assoc_best_flags)
+    r_auth1 = run_test("AUTH1_WPA3", {**auth_flags, "AE_EXP_FAST_TEST_ID": "5", "AE_EXP_FAST_AUTH": "0"}, 20)
+    consider("AUTH1_WPA3", r_auth1)
+    remaining -= 1
+    r_auth2 = run_test("AUTH2_H2E", {**auth_flags, "AE_EXP_FAST_TEST_ID": "6", "AE_EXP_FAST_AUTH": "1"}, 20)
+    consider("AUTH2_H2E", r_auth2)
+    remaining -= 1
+    r_auth3 = run_test("AUTH3_WPA2", {**auth_flags, "AE_EXP_FAST_TEST_ID": "7", "AE_EXP_FAST_AUTH": "2"}, 20)
+    consider("AUTH3_WPA2", r_auth3)
+    remaining -= 1
+
+    auth_choice = 0
+    auth_name = "AUTH1_WPA3"
+    # pick fastest reliable among auth tests that passed; do not auto-weaken security
+    for name, rr, aval in (
+        ("AUTH1_WPA3", r_auth1, 0),
+        ("AUTH2_H2E", r_auth2, 1),
+        ("AUTH3_WPA2", r_auth3, 2),
+    ):
+        if pass20(rr) and (best is None or rr["cyc"] + 20 <= (best["cyc"] if best else 10**9)):
+            if best and best.get("name") == name:
+                auth_choice = aval
+                auth_name = name
+    if best and str(best.get("name", "")).startswith("AUTH"):
+        if best["name"] == "AUTH2_H2E":
+            auth_choice = 1
+            auth_name = "AUTH2_H2E"
+        elif best["name"] == "AUTH3_WPA2":
+            auth_choice = 2
+            auth_name = "AUTH3_WPA2"
+        else:
+            auth_choice = 0
+            auth_name = "AUTH1_WPA3"
+
+    cur = dict(assoc_best_flags)
+    cur["AE_EXP_FAST_AUTH"] = str(auth_choice)
+    log(f"auth selected {auth_name}={auth_choice} negotiated AUTH1={r_auth1['auth']} AUTH2={r_auth2['auth']} AUTH3={r_auth3['auth']}")
+
+    # 6 retry
+    r0 = run_test("R0", {**cur, "AE_EXP_FAST_TEST_ID": "8", "AE_EXP_FAST_RETRY": "0"}, 20)
+    consider("R0", r0)
+    remaining -= 1
+    r1 = run_test("R1", {**cur, "AE_EXP_FAST_TEST_ID": "9", "AE_EXP_FAST_RETRY": "1"}, 20)
+    consider("R1", r1)
+    remaining -= 1
+    r3 = run_test("R3", {**cur, "AE_EXP_FAST_TEST_ID": "10", "AE_EXP_FAST_RETRY": "3"}, 20)
+    consider("R3", r3)
+    remaining -= 1
+    retry = 10
+    # keep 10 unless a lower retry has same success-path median and no worse p90/delivery
+    for name, rr, rv in (("R0", r0, 0), ("R1", r1, 1), ("R3", r3, 3)):
+        if pass20(rr) and rr["wr"] >= 18:
+            retry = rv  # last passing smaller? we'll set to first that matches median
+            break
+    # if success median same as AUTH, use smallest retry that didn't lose delivery
+    if pass20(r0) and r0["wr"] >= 19:
+        retry = 0
+    elif pass20(r1) and r1["wr"] >= 19:
+        retry = 1
+    elif pass20(r3) and r3["wr"] >= 19:
+        retry = 3
+    cur["AE_EXP_FAST_RETRY"] = str(retry)
+    log(f"retry selected {retry}")
+
+    # 7 callback
+    cb0 = run_test(
+        "CB0",
+        {**cur, "AE_EXP_FAST_TEST_ID": "11", "AE_EXP_FAST_POST_MODE": "1", "AE_EXP_FAST_POST_MS": "0"},
+        20,
+    )
+    remaining -= 1
+    cb_ok = pass20(cb0) and cb0["cbm"] >= 15
+    if not cb_ok:
+        log("CALLBACK_NOT_USABLE")
+        cur["AE_EXP_FAST_POST_MODE"] = "0"
+        cur["AE_EXP_FAST_POST_MS"] = "300"
+    else:
+        consider("CB0", cb0)
+        cur["AE_EXP_FAST_POST_MODE"] = "1"
+        cur["AE_EXP_FAST_POST_MS"] = "0"
+        if cb0["del"] < 20:
+            cb1 = run_test(
+                "CB1_10",
+                {**cur, "AE_EXP_FAST_TEST_ID": "12", "AE_EXP_FAST_POST_MODE": "2"},
+                20,
+            )
+            remaining -= 1
+            if pass20(cb1):
+                consider("CB1_10", cb1)
+                cur["AE_EXP_FAST_POST_MODE"] = "2"
+            else:
+                cb2 = run_test(
+                    "CB2_25",
+                    {**cur, "AE_EXP_FAST_TEST_ID": "13", "AE_EXP_FAST_POST_MODE": "3"},
+                    20,
+                )
+                remaining -= 1
+                if pass20(cb2):
+                    consider("CB2_25", cb2)
+                    cur["AE_EXP_FAST_POST_MODE"] = "3"
+                else:
+                    log("callback delivery weak — revert to 300ms hold")
+                    cur["AE_EXP_FAST_POST_MODE"] = "0"
+                    cur["AE_EXP_FAST_POST_MS"] = "300"
+                    cb_ok = False
+
+    # 8 PRE sweep
+    pre_vals = [200, 150, 100, 75, 50, 25, 0]
+    best_pre = 200
+    for i, pre in enumerate(pre_vals):
+        name = f"PRE_{pre}"
+        rr = run_test(
+            name,
+            {**cur, "AE_EXP_FAST_TEST_ID": str(20 + i), "AE_EXP_FAST_PRE_MS": str(pre)},
+            20,
+        )
+        remaining -= 1
+        if rr["del"] >= 20:
+            consider(name, rr)
+            best_pre = pre
+            continue
+        if rr["del"] == 19:
+            rr2 = run_test(name + "_R", {**cur, "AE_EXP_FAST_TEST_ID": str(40 + i), "AE_EXP_FAST_PRE_MS": str(pre)}, 20)
+            remaining -= 1
+            if rr2["del"] >= 19:
+                consider(name + "_R", rr2)
+                best_pre = pre
+                continue
+        log(f"PRE {pre} too aggressive ({rr['del']}/20) — stop PRE sweep")
+        break
+    cur["AE_EXP_FAST_PRE_MS"] = str(best_pre)
+
+    # 9 POST sweep if not callback
+    best_post = int(cur.get("AE_EXP_FAST_POST_MS", "300"))
+    if cur.get("AE_EXP_FAST_POST_MODE", "0") == "0":
+        post_vals = [300, 250, 200, 150, 100, 75, 50, 25, 0]
+        for i, post in enumerate(post_vals):
+            name = f"POST_{post}"
+            rr = run_test(
+                name,
+                {**cur, "AE_EXP_FAST_TEST_ID": str(50 + i), "AE_EXP_FAST_POST_MS": str(post)},
+                20,
+            )
+            remaining -= 1
+            if rr["del"] >= 20:
+                consider(name, rr)
+                best_post = post
+                continue
+            if rr["del"] == 19:
+                rr2 = run_test(
+                    name + "_R",
+                    {**cur, "AE_EXP_FAST_TEST_ID": str(60 + i), "AE_EXP_FAST_POST_MS": str(post)},
+                    20,
+                )
+                remaining -= 1
+                if rr2["del"] >= 19:
+                    consider(name + "_R", rr2)
+                    best_post = post
+                    continue
+            log(f"POST {post} too aggressive — stop POST sweep")
+            break
+        cur["AE_EXP_FAST_POST_MS"] = str(best_post)
+
+    # 10 2D neighbors
+    p = int(cur["AE_EXP_FAST_PRE_MS"])
+    q = int(cur.get("AE_EXP_FAST_POST_MS", "0"))
+    neighbors = [
+        (max(0, p - 25), q + 25),
+        (p + 25, max(0, q - 25)),
+        (max(0, p - 50), q + 50),
+        (p + 50, max(0, q - 50)),
+    ]
+    if cur.get("AE_EXP_FAST_POST_MODE", "0") != "0":
+        neighbors = []  # callback path: don't 2D post delay
+    for i, (pp, qq) in enumerate(neighbors):
+        name = f"2D_p{pp}_q{qq}"
+        rr = run_test(
+            name,
+            {
+                **cur,
+                "AE_EXP_FAST_TEST_ID": str(70 + i),
+                "AE_EXP_FAST_PRE_MS": str(pp),
+                "AE_EXP_FAST_POST_MS": str(qq),
+            },
+            20,
+        )
+        remaining -= 1
+        if pass20(rr):
+            consider(name, rr)
+            if best and best.get("name") == name:
+                cur["AE_EXP_FAST_PRE_MS"] = str(pp)
+                cur["AE_EXP_FAST_POST_MS"] = str(qq)
+
+    # 11 reliability 100 then 200
+    cand_name = best["name"] if best else "BASE"
+    log(f"validation candidate {cand_name} flags={cur}")
+    v100 = run_test("VAL100", {**cur, "AE_EXP_FAST_TEST_ID": "80"}, 100, timeout_s=45 * 100)
+    remaining -= 1
+    if v100["del"] < 98:
+        log("VAL100 < 98 — try BASE delays as fallback candidate")
+        cur2 = dict(cur)
+        cur2["AE_EXP_FAST_PRE_MS"] = "200"
+        if cur2.get("AE_EXP_FAST_POST_MODE", "0") == "0":
+            cur2["AE_EXP_FAST_POST_MS"] = "300"
+        v100b = run_test("VAL100_SAFE", {**cur2, "AE_EXP_FAST_TEST_ID": "81"}, 100, timeout_s=45 * 100)
+        if v100b["del"] > v100["del"]:
+            cur = cur2
+            v100 = v100b
+    if v100["del"] == 98:
+        v100r = run_test("VAL100_REPEAT", {**cur, "AE_EXP_FAST_TEST_ID": "82"}, 100, timeout_s=45 * 100)
+        remaining -= 1
+
+    v200 = run_test("VAL200", {**cur, "AE_EXP_FAST_TEST_ID": "90"}, 200, timeout_s=45 * 200)
+    remaining -= 1
+
+    # 12 AMPDU: default already TX off; explicit D1
+    d1 = run_test(
+        "D1_AMPDU_TX_OFF",
+        {**cur, "AE_EXP_FAST_TEST_ID": "91", "AE_EXP_FAST_AMPDU_TX_OFF": "1"},
+        20,
+    )
+    remaining -= 1
+    consider("D1_AMPDU_TX_OFF", d1)
+
+    # 13 storage RAM
+    ram = run_test(
+        "STORAGE_RAM",
+        {**cur, "AE_EXP_FAST_TEST_ID": "92", "AE_EXP_FAST_STORAGE_RAM": "1"},
+        20,
+    )
+    remaining -= 1
+    consider("STORAGE_RAM", ram)
+
+    log("=== SCREENING COMPLETE ===")
+    log(f"BEST {best}")
+    log(f"VAL200 {v200}")
+    STATE.write_text(
+        json.dumps({"best": best, "cur": cur, "val200": v200}, indent=2),
+        encoding="utf-8",
+    )
+    return 0
+
+
+def one_base_no_rebuild() -> dict:
+    flags = {
+        "AE_EXP_FAST_TEST_ID": "1",
+        "AE_EXP_FAST_PRE_MS": "200",
+        "AE_EXP_FAST_POST_MS": "300",
+        "AE_EXP_FAST_USE_BSSID": "0",
+        "AE_EXP_FAST_FAST_SCAN": "0",
+        "AE_EXP_FAST_AUTH": "0",
+        "AE_EXP_FAST_RETRY": "10",
+        "AE_EXP_FAST_POST_MODE": "0",
+        "AE_EXP_FAST_AMPDU_TX_OFF": "0",
+        "AE_EXP_FAST_STORAGE_RAM": "0",
+    }
+    r = run_test("BASE", flags, 20, timeout_s=900, rebuild=False)
+    write_chat(1, 21, "BASE", r, r, "A1 BASE+cached BSSID x20")
+    return r
+
+
+if __name__ == "__main__":
+    try:
+        if "--one-base" in sys.argv:
+            one_base_no_rebuild()
+            sys.exit(0)
+        sys.exit(main())
+    except Exception as ex:
+        log(f"FATAL {ex}")
+        raise
diff --git a/experiments/run_fastest_safe_val.py b/experiments/run_fastest_safe_val.py
new file mode 100644
index 0000000..8d7706a
--- /dev/null
+++ b/experiments/run_fastest_safe_val.py
@@ -0,0 +1,322 @@
+"""Safer validation after both aggressive VAL100s failed <98/100."""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from run_fastest_path import (  # noqa: E402
+    RESULTS,
+    STATE,
+    cmake_configure,
+    ensure_receiver,
+    log,
+    run_test,
+    write_chat,
+)
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+
+BASE_AUTH = {
+    "AE_EXP_FAST_USE_BSSID": "0",
+    "AE_EXP_FAST_FAST_SCAN": "0",
+    "AE_EXP_FAST_AUTH": "2",
+    "AE_EXP_FAST_RETRY": "10",
+    "AE_EXP_FAST_POST_MODE": "0",
+    "AE_EXP_FAST_AMPDU_TX_OFF": "0",
+    "AE_EXP_FAST_STORAGE_RAM": "0",
+    "AE_EXP_FAST_DISABLE_WPA3": "1",
+}
+
+# Candidates ordered: safer delays first after failed aggressive VAL100s
+CANDIDATES = [
+    ("SAFE_200_300", {**BASE_AUTH, "AE_EXP_FAST_PRE_MS": "200", "AE_EXP_FAST_POST_MS": "300"}),
+    ("MID_200_200", {**BASE_AUTH, "AE_EXP_FAST_PRE_MS": "200", "AE_EXP_FAST_POST_MS": "200"}),
+    ("FAST_200_150", {**BASE_AUTH, "AE_EXP_FAST_PRE_MS": "200", "AE_EXP_FAST_POST_MS": "150"}),
+    ("FAST_150_200", {**BASE_AUTH, "AE_EXP_FAST_PRE_MS": "150", "AE_EXP_FAST_POST_MS": "200"}),
+]
+
+BEST = {
+    "name": "POST_150_R",
+    "cyc": 550,
+    "conn": 125,
+    "del": 20,
+    "plan": 20,
+    "pre": 200,
+    "post": 150,
+    "pm": 0,
+}
+
+TEST_NO = 28
+REMAINING = 10
+
+
+def force_wpa3_off() -> None:
+    sdk = ROOT / "build-esp32c6-save-bench-smoke" / "sdkconfig"
+    text = sdk.read_text(encoding="utf-8")
+    text = text.replace(
+        "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y",
+        "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set",
+    )
+    text = text.replace(
+        "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y",
+        "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set",
+    )
+    sdk.write_text(text, encoding="utf-8")
+
+
+def restore_wpa3() -> None:
+    sdk = ROOT / "build-esp32c6-save-bench-smoke" / "sdkconfig"
+    text = sdk.read_text(encoding="utf-8")
+    text = text.replace(
+        "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set",
+        "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y",
+    )
+    text = text.replace(
+        "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set",
+        "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y",
+    )
+    if "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y" not in text:
+        text += "\nCONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y\n"
+    sdk.write_text(text, encoding="utf-8")
+    cmake_configure(
+        {
+            **BASE_AUTH,
+            "AE_EXP_FAST_DISABLE_WPA3": "",
+            "AE_EXP_FAST_AUTH": "0",
+            "AE_EXP_FAST_PRE_MS": "200",
+            "AE_EXP_FAST_POST_MS": "300",
+            "AE_EXP_FAST_TEST_ID": "99",
+        }
+    )
+    log("WPA3 SAE restore attempted")
+
+
+def report(name: str, r: dict, nxt: str) -> None:
+    global TEST_NO, REMAINING, BEST
+    REMAINING = max(0, REMAINING - 1)
+    write_chat(TEST_NO, REMAINING, name, r, BEST, nxt)
+    TEST_NO += 1
+    STATE.write_text(
+        json.dumps({"best": BEST, "last": name, "result": r}, indent=2),
+        encoding="utf-8",
+    )
+
+
+def write_report(winner_name: str, flags: dict, v100s: list, v200: dict, d1: dict, ram: dict) -> None:
+    tsv = RESULTS.read_text(encoding="utf-8") if RESULTS.exists() else ""
+    old, new = 850, v200.get("cyc", 0)
+    saving = old - new
+    pct = 100.0 * saving / old if old else 0
+    speedup = (old / new) if new else 0
+    v100_lines = "\n".join(
+        f"- {n}: {r['del']}/{r['plan']} cyc={r['cyc']} conn={r['conn']} p90={r['p90']}"
+        for n, r in v100s
+    )
+    md = f"""# Prepared Wi-Fi Fastest Path Report (ESP32-C6)
+
+## Pins
+- temperature-sensor branch: `thermometer-prepared-send-v0`
+- aether-client-cpp: `157aadbec8e7b852d0f89274307ff7cb8103e5f7` **unchanged=yes**
+- ESP-IDF v6.0.2 · Silent Release · WIFI_PS_NONE · Wi-Fi 4 · auto PHY · max TX
+- No sleep/reboot in bench; 1 s gap outside timer
+
+## OLD vs NEW
+- OLD: ~{old} ms (prior C6/C8 static-IP cycle)
+- NEW: **{new} ms** (VAL200 median cycle)
+- Absolute saving: **{saving} ms**
+- Percent saving: **{pct:.1f}%**
+- Speedup: **{speedup:.2f}x**
+
+## BEST RELIABLE CONFIG (measurement winner: {winner_name})
+- Protocol: 802.11b/g/n only
+- Channel cache: yes · BSSID: **no** · Static IP: yes · Static ARP: yes
+- Scan: default (not FAST_SCAN)
+- Auth: **WPA2-PSK** (negotiated authmode=3) via benchmark-only `CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=n`
+  - Security decision deferred; production not auto-weakened
+- Retry max: **10**
+- PRE: **{flags['AE_EXP_FAST_PRE_MS']} ms**
+- POST: **{flags['AE_EXP_FAST_POST_MS']} ms** fixed (callback not usable)
+- AMPDU TX: sdkconfig already off; D1={d1['del']}/{d1['plan']} cyc={d1['cyc']}
+- STORAGE_RAM: {ram['del']}/{ram['plan']} cyc={ram['cyc']}
+- IRAM: WIFI IRAM opts already on; LWIP_IRAM was off (not A/B'd if sendto not bottleneck)
+
+## Callback verdict
+- `esp_wifi_set_tx_done_cb` fires (cb_any high) but **cb_match=0**; delivery 18/20
+- **CALLBACK_NOT_USABLE** for normal lwIP UDP
+
+## Auth / association summary
+- BSSID / FAST_SCAN: no clear ≥20 ms win
+- WPA3 BOTH / H2E: authmode stays 7 on transition AP
+- WPA2 threshold alone: still authmode 7
+- WPA3 SAE compile-off: authmode **3**, connect ~128 ms @ 200/300
+
+## VAL100 candidates
+{v100_lines}
+
+## VAL200
+- delivered **{v200['del']}/{v200['plan']}**
+- connect median={v200['conn']} ms · cycle median={v200['cyc']} ms · p90={v200['p90']} · max={v200['mx']}
+
+## Production
+`SendPreparedOnce` **not** switched to winner.
+
+## TSV
+`experiments/prepared_wifi_fastest_path.tsv`
+
+```
+{tsv}
+```
+"""
+    path = ROOT / "experiments" / "PREPARED_WIFI_FASTEST_PATH_REPORT.md"
+    path.write_text(md, encoding="utf-8")
+    log(f"wrote {path}")
+
+
+def main() -> int:
+    global BEST
+    log("=== SAFE/MID VAL cascade after 94% and 97% ===")
+    ensure_receiver()
+    force_wpa3_off()
+
+    v100s: list[tuple[str, dict]] = [
+        ("VAL100_PRIMARY_200_150", {"del": 94, "plan": 100, "cyc": 560, "conn": 131, "p90": 620}),
+        ("VAL100_SECONDARY_150_200", {"del": 97, "plan": 100, "cyc": 560, "conn": 131, "p90": 610}),
+    ]
+
+    winner_flags = None
+    winner_name = None
+    winner_r = None
+
+    for i, (name, flags) in enumerate(CANDIDATES):
+        if winner_flags is not None and winner_r and winner_r.get("del", 0) >= 99:
+            break
+        # Prefer safer delays first; only try FAST_* if SAFE/MID did not reach 99
+        if name.startswith("FAST_") and any(
+            x[0].startswith("VAL100_SAFE") or x[0].startswith("VAL100_MID")
+            for x in v100s
+        ):
+            safe_mid = [x for x in v100s if "SAFE" in x[0] or "MID" in x[0]]
+            if safe_mid and max(x[1]["del"] for x in safe_mid) >= 99:
+                continue
+        tid = 83 + i
+        r = run_test(
+            f"VAL100_{name}",
+            {**flags, "AE_EXP_FAST_TEST_ID": str(tid)},
+            100,
+            timeout_s=45 * 100 + 600,
+        )
+        report(f"VAL100_{name}", r, "next candidate or VAL200")
+        v100s.append((f"VAL100_{name}", r))
+        if r["del"] >= 99:
+            winner_flags = flags
+            winner_name = name
+            winner_r = r
+            BEST = {
+                **r,
+                "name": name,
+                "pre": int(flags["AE_EXP_FAST_PRE_MS"]),
+                "post": int(flags["AE_EXP_FAST_POST_MS"]),
+            }
+            break
+        if r["del"] >= 98:
+            rr = run_test(
+                f"VAL100_{name}_R",
+                {**flags, "AE_EXP_FAST_TEST_ID": str(tid + 10)},
+                100,
+                timeout_s=45 * 100 + 600,
+            )
+            report(f"VAL100_{name}_R", rr, "next")
+            v100s.append((f"VAL100_{name}_R", rr))
+            if rr["del"] >= 98:
+                winner_flags = flags
+                winner_name = name
+                winner_r = rr
+                BEST = {
+                    **rr,
+                    "name": name,
+                    "pre": int(flags["AE_EXP_FAST_PRE_MS"]),
+                    "post": int(flags["AE_EXP_FAST_POST_MS"]),
+                }
+                if rr["del"] >= 99:
+                    break
+                # 98/98 — keep searching for better delivery unless last
+
+    if winner_flags is None:
+        # fall back to best delivery among all v100s
+        best_del = -1
+        for n, r in v100s:
+            if r["del"] > best_del or (r["del"] == best_del and r["cyc"] < (winner_r or {}).get("cyc", 10**9)):
+                best_del = r["del"]
+                winner_r = r
+                winner_name = n
+        # map name to flags
+        if "200_300" in (winner_name or "") or "SAFE" in (winner_name or ""):
+            winner_flags = CANDIDATES[0][1]
+        elif "200_200" in (winner_name or "") or "MID" in (winner_name or ""):
+            winner_flags = CANDIDATES[1][1]
+        elif "200_150" in (winner_name or ""):
+            winner_flags = CANDIDATES[2][1]
+        else:
+            winner_flags = CANDIDATES[3][1]
+        log(f"no >=99/100; falling back to best delivery {winner_name} {best_del}/100")
+
+    log(f"VAL200 winner={winner_name} flags={winner_flags}")
+    v200 = run_test(
+        "VAL200",
+        {**winner_flags, "AE_EXP_FAST_TEST_ID": "90"},
+        200,
+        timeout_s=45 * 200 + 900,
+    )
+    report("VAL200", v200, "D1 AMPDU")
+    BEST = {
+        **v200,
+        "name": winner_name,
+        "pre": int(winner_flags["AE_EXP_FAST_PRE_MS"]),
+        "post": int(winner_flags["AE_EXP_FAST_POST_MS"]),
+    }
+
+    d1 = run_test(
+        "D1_AMPDU_TX_OFF",
+        {**winner_flags, "AE_EXP_FAST_TEST_ID": "91", "AE_EXP_FAST_AMPDU_TX_OFF": "1"},
+        20,
+        timeout_s=900,
+    )
+    report("D1_AMPDU_TX_OFF", d1, "STORAGE_RAM")
+
+    ram = run_test(
+        "STORAGE_RAM",
+        {**winner_flags, "AE_EXP_FAST_TEST_ID": "92", "AE_EXP_FAST_STORAGE_RAM": "1"},
+        20,
+        timeout_s=900,
+    )
+    report("STORAGE_RAM", ram, "write report + restore WPA3")
+
+    write_report(winner_name or "?", winner_flags, v100s, v200, d1, ram)
+    STATE.write_text(
+        json.dumps(
+            {
+                "best": BEST,
+                "winner_name": winner_name,
+                "winner_flags": winner_flags,
+                "val200": v200,
+                "v100s": [(n, r) for n, r in v100s],
+            },
+            indent=2,
+            default=str,
+        ),
+        encoding="utf-8",
+    )
+    restore_wpa3()
+    log("=== ALL DONE ===")
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as ex:
+        log(f"FATAL {ex}")
+        raise
diff --git a/experiments/run_fastest_validate.py b/experiments/run_fastest_validate.py
new file mode 100644
index 0000000..0ed6475
--- /dev/null
+++ b/experiments/run_fastest_validate.py
@@ -0,0 +1,290 @@
+"""VAL100/200 + AMPDU/STORAGE + report for fastest-path campaign."""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from run_fastest_path import (  # noqa: E402
+    RESULTS,
+    STATE,
+    cmake_configure,
+    ensure_receiver,
+    log,
+    run_test,
+    write_chat,
+)
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+
+# Primary: POST_150_R (reliable PRE=200). Secondary: 2D_p150_q200.
+PRIMARY = {
+    "AE_EXP_FAST_PRE_MS": "200",
+    "AE_EXP_FAST_POST_MS": "150",
+    "AE_EXP_FAST_USE_BSSID": "0",
+    "AE_EXP_FAST_FAST_SCAN": "0",
+    "AE_EXP_FAST_AUTH": "2",
+    "AE_EXP_FAST_RETRY": "10",
+    "AE_EXP_FAST_POST_MODE": "0",
+    "AE_EXP_FAST_AMPDU_TX_OFF": "0",
+    "AE_EXP_FAST_STORAGE_RAM": "0",
+    "AE_EXP_FAST_DISABLE_WPA3": "1",
+}
+SECONDARY = {**PRIMARY, "AE_EXP_FAST_PRE_MS": "150", "AE_EXP_FAST_POST_MS": "200"}
+
+BEST = {
+    "name": "POST_150_R",
+    "cyc": 550,
+    "conn": 125,
+    "del": 20,
+    "plan": 20,
+    "pre": 200,
+    "post": 150,
+    "pm": 0,
+    "p90": 580,
+    "auth": 3,
+}
+
+TEST_NO = 26
+REMAINING = 8
+
+
+def force_wpa3_off() -> None:
+    sdk = ROOT / "build-esp32c6-save-bench-smoke" / "sdkconfig"
+    text = sdk.read_text(encoding="utf-8")
+    text = text.replace(
+        "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y",
+        "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set",
+    )
+    text = text.replace(
+        "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y",
+        "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set",
+    )
+    sdk.write_text(text, encoding="utf-8")
+
+
+def restore_wpa3() -> None:
+    sdk = ROOT / "build-esp32c6-save-bench-smoke" / "sdkconfig"
+    text = sdk.read_text(encoding="utf-8")
+    text = text.replace(
+        "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set",
+        "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y",
+    )
+    text = text.replace(
+        "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set",
+        "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y",
+    )
+    if "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y" not in text:
+        text += "\nCONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y\n"
+    sdk.write_text(text, encoding="utf-8")
+    cmake_configure(
+        {
+            **PRIMARY,
+            "AE_EXP_FAST_DISABLE_WPA3": "",
+            "AE_EXP_FAST_AUTH": "0",
+            "AE_EXP_FAST_TEST_ID": "99",
+        }
+    )
+    log("WPA3 SAE restore attempted")
+
+
+def report(name: str, r: dict, nxt: str, best: dict) -> dict:
+    global TEST_NO, REMAINING, BEST
+    if r["del"] == r["plan"] and (
+        r["cyc"] + 20 < best["cyc"]
+        or (r["plan"] >= 100 and r["del"] >= int(0.99 * r["plan"]) and r["cyc"] < best["cyc"])
+    ):
+        BEST = dict(r)
+        BEST["name"] = name
+        best = BEST
+    REMAINING = max(0, REMAINING - 1)
+    write_chat(TEST_NO, REMAINING, name, r, best, nxt)
+    TEST_NO += 1
+    STATE.write_text(
+        json.dumps({"best": best, "last": name, "result": r}, indent=2),
+        encoding="utf-8",
+    )
+    return best
+
+
+def write_final_report(winner_flags: dict, v200: dict, extras: dict) -> None:
+    report_md = ROOT / "experiments" / "PREPARED_WIFI_FASTEST_PATH_REPORT.md"
+    tsv = RESULTS.read_text(encoding="utf-8") if RESULTS.exists() else ""
+    old = 850
+    new = v200.get("cyc", BEST["cyc"])
+    saving = old - new
+    pct = 100.0 * saving / old if old else 0
+    speedup = old / new if new else 0
+    body = f"""# Prepared Wi-Fi Fastest Path Report (ESP32-C6)
+
+## Pins
+- temperature-sensor branch: `thermometer-prepared-send-v0`
+- aether-client-cpp: `157aadbec8e7b852d0f89274307ff7cb8103e5f7` **unchanged=yes**
+- ESP-IDF: v6.0.2
+- Silent Release, WIFI_PS_NONE, Wi-Fi 4 only, auto PHY, max TX power
+- No sleep/reboot during benchmark; 1 s gap outside timer
+
+## OLD vs NEW
+- OLD: ~{old} ms static-IP cycle (prior C6/C8)
+- NEW: **{new} ms** (VAL200 median)
+- Absolute saving: **{saving} ms**
+- Percent saving: **{pct:.1f}%**
+- Speedup: **{speedup:.2f}x**
+
+## BEST RELIABLE CONFIG (measurement winner)
+- Wi-Fi protocol: 802.11b/g/n only
+- Channel cache: yes
+- BSSID cache: **no**
+- Static IP + netmask + gateway: yes
+- Static ARP (gateway MAC): yes
+- Scan method: default (not WIFI_FAST_SCAN)
+- WPA mode: **WPA2-PSK** (`authmode=3`) via benchmark-only `CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=n`
+  - Production security decision deferred; do **not** auto-weaken production
+- SAE method: N/A (WPA2)
+- Association retry max: **10** (R0/R1 unreliable on delivery)
+- PRE: **{winner_flags.get('AE_EXP_FAST_PRE_MS')} ms**
+- POST: **{winner_flags.get('AE_EXP_FAST_POST_MS')} ms** fixed hold (callback not usable)
+- AMPDU TX: already off in sdkconfig; D1 explicit off = {extras.get('ampdu','n/a')}
+- Wi-Fi storage: NVS default; STORAGE_RAM = {extras.get('ram','n/a')}
+- IRAM: ESP_WIFI_IRAM_OPT/RX_IRAM already on; LWIP_IRAM was off (see notes)
+
+## Callback verdict
+- API: `esp_wifi_set_tx_done_cb` (IDF 6.0.2 `esp_private/wifi.h`)
+- CB0: callback **fires** (cb_any=18/20) but **fingerprint match=0**; delivery 18/20
+- Verdict: **CALLBACK_NOT_USABLE** for normal lwIP UDP (cannot reliably bind to our frame)
+
+## Auth results
+- AUTH1 WPA3 BOTH: negotiated authmode=7 (WPA2/WPA3 transition AP info), ~840 ms
+- AUTH2 H2E-only: authmode still 7, delivery 18/20
+- AUTH3 WPA2 threshold only: still authmode=7 (ESP still picks WPA3 path)
+- AUTH3B WPA3 SAE compile-disabled: **authmode=3 (WPA2_PSK)**, connect ~128 ms, cycle ~710 ms @ PRE/POST 200/300
+
+## Association extras (on WPA3 BASE)
+- A1 BSSID / A2 FAST_SCAN / A3 both: no clear ≥20 ms win; some delivery loss
+
+## Delay search
+- PRE: 200 reliable; 150 too aggressive with POST=300 (14/20)
+- POST: 150 reliable (20/20 after repeat); 100 too aggressive (13/20)
+- 2D: `PRE=150,POST=200` also 20/20 @ 540 ms screen (secondary candidate)
+
+## Validation
+- VAL100 primary: {extras.get('v100_primary')}
+- VAL100 secondary: {extras.get('v100_secondary')}
+- VAL200 winner: **{v200.get('del')}/{v200.get('plan')}** median_cycle={v200.get('cyc')} connect={v200.get('conn')} p90={v200.get('p90')} max={v200.get('mx')}
+
+## Production note
+`SendPreparedOnce` **not** switched to winner automatically.
+
+## TSV
+See `experiments/prepared_wifi_fastest_path.tsv`.
+
+### Raw TSV snapshot
+```
+{tsv}
+```
+"""
+    report_md.write_text(body, encoding="utf-8")
+    log(f"wrote {report_md}")
+
+
+def main() -> int:
+    global BEST
+    log("=== VAL + AMPDU + REPORT ===")
+    ensure_receiver()
+    force_wpa3_off()
+
+    v100a = run_test(
+        "VAL100_PRIMARY",
+        {**PRIMARY, "AE_EXP_FAST_TEST_ID": "80"},
+        100,
+        timeout_s=45 * 100 + 600,
+    )
+    BEST = report("VAL100_PRIMARY", v100a, "VAL100 secondary", BEST)
+
+    v100b = run_test(
+        "VAL100_SECONDARY",
+        {**SECONDARY, "AE_EXP_FAST_TEST_ID": "81"},
+        100,
+        timeout_s=45 * 100 + 600,
+    )
+    BEST = report("VAL100_SECONDARY", v100b, "pick winner VAL200", BEST)
+
+    winner = PRIMARY
+    winner_name = "PRIMARY_PRE200_POST150"
+    # Prefer >=99/100; if secondary much faster and >=98, prefer it
+    if v100b["del"] >= 99 and (
+        v100b["cyc"] + 20 < v100a["cyc"] or v100b["del"] > v100a["del"]
+    ):
+        winner = SECONDARY
+        winner_name = "SECONDARY_PRE150_POST200"
+    elif v100a["del"] < 98 and v100b["del"] > v100a["del"]:
+        winner = SECONDARY
+        winner_name = "SECONDARY_PRE150_POST200"
+    elif v100a["del"] == 98:
+        v100ar = run_test(
+            "VAL100_PRIMARY_R",
+            {**PRIMARY, "AE_EXP_FAST_TEST_ID": "82"},
+            100,
+            timeout_s=45 * 100 + 600,
+        )
+        BEST = report("VAL100_PRIMARY_R", v100ar, "VAL200", BEST)
+        v100a = v100ar
+
+    log(f"winner for VAL200: {winner_name} {winner}")
+    v200 = run_test(
+        "VAL200",
+        {**winner, "AE_EXP_FAST_TEST_ID": "90"},
+        200,
+        timeout_s=45 * 200 + 900,
+    )
+    BEST = report("VAL200", v200, "D1 AMPDU", BEST)
+
+    d1 = run_test(
+        "D1_AMPDU_TX_OFF",
+        {**winner, "AE_EXP_FAST_TEST_ID": "91", "AE_EXP_FAST_AMPDU_TX_OFF": "1"},
+        20,
+        timeout_s=900,
+    )
+    BEST = report("D1_AMPDU_TX_OFF", d1, "STORAGE_RAM", BEST)
+
+    ram = run_test(
+        "STORAGE_RAM",
+        {**winner, "AE_EXP_FAST_TEST_ID": "92", "AE_EXP_FAST_STORAGE_RAM": "1"},
+        20,
+        timeout_s=900,
+    )
+    BEST = report("STORAGE_RAM", ram, "write report", BEST)
+
+    extras = {
+        "v100_primary": f"{v100a['del']}/{v100a['plan']} cyc={v100a['cyc']}",
+        "v100_secondary": f"{v100b['del']}/{v100b['plan']} cyc={v100b['cyc']}",
+        "ampdu": f"{d1['del']}/{d1['plan']} cyc={d1['cyc']}",
+        "ram": f"{ram['del']}/{ram['plan']} cyc={ram['cyc']}",
+    }
+    write_final_report(winner, v200, extras)
+    STATE.write_text(
+        json.dumps(
+            {
+                "best": BEST,
+                "winner": winner,
+                "winner_name": winner_name,
+                "val200": v200,
+                "extras": extras,
+            },
+            indent=2,
+        ),
+        encoding="utf-8",
+    )
+    restore_wpa3()
+    log("=== ALL DONE ===")
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as ex:
+        log(f"FATAL {ex}")
+        raise
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index f0e6ea6..7612562 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -28,6 +28,11 @@ elseif(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20)
     "prepared_keep_wifi_up_5x20_bench.cpp"
     "prepared_send/prepared_send.cpp"
   )
+elseif(AE_EXP_PREPARED_WIFI_FASTEST)
+  list(APPEND src_list
+    "prepared_wifi_fastest_path_bench.cpp"
+    "prepared_send/prepared_send.cpp"
+  )
 elseif(AE_EXP_PREPARED_WIFI_BISECT)
   list(APPEND src_list
     "prepared_wifi_single_factor_bisect_bench.cpp"
@@ -166,6 +171,18 @@ set(AE_EXP_PREPARED_MESSAGE_E2E "" CACHE STRING "No-sleep prepared-message E2E b
 set(AE_EXP_PREPARED_WIFI_CACHE_5X20 "" CACHE STRING "Silent 5x20 prepared Wi-Fi cache bench (set to 1)")
 set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 "" CACHE STRING "Silent 5x20 keep-Wi-Fi-up prepared bench (set to 1)")
 set(AE_EXP_PREPARED_WIFI_BISECT "" CACHE STRING "Silent single-factor prepared Wi-Fi bisect (set to 1)")
+set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING "Silent fastest-path prepared campaign (set to 1)")
+set(AE_EXP_FAST_N "" CACHE STRING "Fastest-path prepared count")
+set(AE_EXP_FAST_TEST_ID "" CACHE STRING "Fastest-path test id")
+set(AE_EXP_FAST_PRE_MS "" CACHE STRING "Fastest-path pre-send delay ms")
+set(AE_EXP_FAST_POST_MS "" CACHE STRING "Fastest-path post-send delay ms")
+set(AE_EXP_FAST_USE_BSSID "" CACHE STRING "Fastest-path cached BSSID (0/1)")
+set(AE_EXP_FAST_FAST_SCAN "" CACHE STRING "Fastest-path WIFI_FAST_SCAN (0/1)")
+set(AE_EXP_FAST_AUTH "" CACHE STRING "Fastest-path auth 0=WPA3 1=H2E 2=WPA2")
+set(AE_EXP_FAST_RETRY "" CACHE STRING "Fastest-path association retry max")
+set(AE_EXP_FAST_POST_MODE "" CACHE STRING "Fastest-path post 0=delay 1=cb 2=cb+10 3=cb+25")
+set(AE_EXP_FAST_AMPDU_TX_OFF "" CACHE STRING "Fastest-path AMPDU TX off (0/1)")
+set(AE_EXP_FAST_STORAGE_RAM "" CACHE STRING "Fastest-path WIFI_STORAGE_RAM (0/1)")
 set(AE_EXP_BISECT_CONSOLE "" CACHE STRING "Bisect USB stage markers (set to 1)")
 set(AE_EXP_BISECT_SMOKE "" CACHE STRING "Bisect smoke B1/2 prepared (set to 1)")
 set(BENCH_CLIENT_ID "" CACHE STRING "Bench SelectClient id (default prepared_message_bench_v1)")
@@ -207,10 +224,23 @@ ae_exp_define_if_set(AE_EXP_PREPARED_MESSAGE_E2E)
 ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_CACHE_5X20)
 ae_exp_define_if_set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20)
 ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_BISECT)
+ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_FASTEST)
+ae_exp_define_if_set(AE_EXP_FAST_N)
+ae_exp_define_if_set(AE_EXP_FAST_TEST_ID)
+ae_exp_define_if_set(AE_EXP_FAST_PRE_MS)
+ae_exp_define_if_set(AE_EXP_FAST_POST_MS)
+ae_exp_define_if_set(AE_EXP_FAST_USE_BSSID)
+ae_exp_define_if_set(AE_EXP_FAST_FAST_SCAN)
+ae_exp_define_if_set(AE_EXP_FAST_AUTH)
+ae_exp_define_if_set(AE_EXP_FAST_RETRY)
+ae_exp_define_if_set(AE_EXP_FAST_POST_MODE)
+ae_exp_define_if_set(AE_EXP_FAST_AMPDU_TX_OFF)
+ae_exp_define_if_set(AE_EXP_FAST_STORAGE_RAM)
 ae_exp_define_if_set(AE_EXP_BISECT_CONSOLE)
 ae_exp_define_if_set(AE_EXP_BISECT_SMOKE)
 if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR
    AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1" OR
+   AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1" OR
    (AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1" AND
     NOT AE_EXP_BISECT_CONSOLE STREQUAL "1"))
   target_compile_definitions(aether PUBLIC "AE_EXP_SILENT=1")
diff --git a/main/bench_payload.h b/main/bench_payload.h
index 4049e32..4045d7f 100644
--- a/main/bench_payload.h
+++ b/main/bench_payload.h
@@ -112,8 +112,54 @@ struct BisectPayload {
 };
 #pragma pack(pop)
 
+static constexpr std::uint8_t kFastMagic = 0xB1;
+
+enum class FastMsgType : std::uint8_t {
+  kFull = 1,
+  kPrepared = 2,
+  kFinal = 3,
+};
+
+enum class FastAssocBits : std::uint8_t {
+  kBssid = 1 << 0,
+  kChannel = 1 << 1,
+  kFastScan = 1 << 2,
+  kStaticIp = 1 << 3,
+  kStaticArp = 1 << 4,
+  kAmpduTxOff = 1 << 5,
+  kStorageRam = 1 << 6,
+  kCallback = 1 << 7,
+};
+
+#pragma pack(push, 1)
+struct FastPayload {
+  std::uint8_t magic{kFastMagic};
+  std::uint8_t type{0};
+  std::uint8_t test_id{0};
+  std::uint8_t prepared_index{0};
+  std::uint16_t sequence_global{0};
+  std::uint32_t cycle_us{0};
+  std::uint32_t connect_us{0};
+  std::uint16_t pre_ms{0};
+  std::uint16_t post_ms{0};
+  std::uint8_t status_flags{0};
+  std::uint8_t assoc_bits{0};
+  std::uint8_t auth_negotiated{0};
+  std::uint8_t retry_max{0};
+  std::uint16_t wifi_ready_count{0};
+  std::uint16_t encode_count{0};
+  std::uint16_t sendto_count{0};
+  std::uint16_t nonce_consumed{0};
+  std::uint8_t cb_any{0};
+  std::uint8_t cb_match{0};
+  std::uint8_t cb_count{0};
+  std::uint8_t post_mode{0};
+};
+#pragma pack(pop)
+
 static_assert(sizeof(Payload) == 19, "bench payload size");
 static_assert(sizeof(BisectPayload) == 34, "bisect payload size");
+static_assert(sizeof(FastPayload) == 34, "fast payload size");
 
 inline char const* BisectVariantName(std::uint8_t id) {
   switch (static_cast(id)) {
@@ -219,6 +265,22 @@ inline bool DecodeBisect(Buffer const& data, BisectPayload& out) {
   return out.magic == kBisectMagic;
 }
 
+template 
+inline Buffer EncodeFast(FastPayload const& p) {
+  Buffer out(sizeof(FastPayload));
+  std::memcpy(out.data(), &p, sizeof(FastPayload));
+  return out;
+}
+
+template 
+inline bool DecodeFast(Buffer const& data, FastPayload& out) {
+  if (data.size() < sizeof(FastPayload)) {
+    return false;
+  }
+  std::memcpy(&out, data.data(), sizeof(FastPayload));
+  return out.magic == kFastMagic;
+}
+
 }  // namespace temp_sensor::bench
 
 #endif  // TEMP_SENSOR_BENCH_PAYLOAD_H_
diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp
index 658e7eb..7b5ea4a 100644
--- a/main/prepared_send/prepared_send.cpp
+++ b/main/prepared_send/prepared_send.cpp
@@ -11,6 +11,7 @@
 #include "prepared_send/prepared_send.h"
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -619,6 +620,90 @@ HotSendStatus EncodeAndUdpSend(ae::DataBuffer const& payload) {
   return HotSendStatus::kSent;
 }
 
+#if defined(ESP_PLATFORM)
+std::uint8_t g_fast_fp[16]{};
+std::uint16_t g_fast_fp_len = 0;
+std::atomic g_fast_cb_any{0};
+std::atomic g_fast_cb_match{0};
+std::atomic g_fast_cb_count{0};
+
+void FastTxDoneCb(std::uint8_t, std::uint8_t* data, std::uint16_t* data_len,
+                  bool) {
+  g_fast_cb_count.fetch_add(1, std::memory_order_relaxed);
+  g_fast_cb_any.store(1, std::memory_order_relaxed);
+  if (data == nullptr || data_len == nullptr || g_fast_fp_len == 0) {
+    return;
+  }
+  std::uint16_t const n = *data_len;
+  if (n < g_fast_fp_len) {
+    return;
+  }
+  for (std::uint16_t i = 0; i + g_fast_fp_len <= n; ++i) {
+    if (std::memcmp(data + i, g_fast_fp, g_fast_fp_len) == 0) {
+      g_fast_cb_match.store(1, std::memory_order_relaxed);
+      return;
+    }
+  }
+}
+
+void ResetFastTxDone() {
+  g_fast_cb_any.store(0, std::memory_order_relaxed);
+  g_fast_cb_match.store(0, std::memory_order_relaxed);
+  g_fast_cb_count.store(0, std::memory_order_relaxed);
+}
+
+HotSendStatus EncodeAndUdpSendTracked(ae::DataBuffer const& payload) {
+  if (!g_prepared_send_message_block.is_valid()) {
+    return HotSendStatus::kNoPreparedBlock;
+  }
+  if (g_prepared_send_message_block.Resolve()->message_left == 0) {
+    return HotSendStatus::kNonceExhausted;
+  }
+
+  ae::DataBuffer packet;
+  auto encode_result = ae::prepared_packet::EncodePacket(
+      g_prepared_send_message_block, payload, packet);
+  if (!encode_result) {
+    ClearPreparedSendBlock();
+    return HotSendStatus::kEncodeFailed;
+  }
+
+  g_fast_fp_len = static_cast(
+      packet.size() < sizeof(g_fast_fp) ? packet.size() : sizeof(g_fast_fp));
+  if (g_fast_fp_len > 0) {
+    std::memcpy(g_fast_fp, packet.data() + (packet.size() - g_fast_fp_len),
+                g_fast_fp_len);
+  }
+
+  auto const resolved_block = g_prepared_send_message_block.Resolve();
+  auto endpoint = resolved_block->endpoint;
+
+  sockaddr_storage dest_storage{};
+  socklen_t dest_len = 0;
+  if (!FillUdpDestination(endpoint, reinterpret_cast(&dest_storage),
+                          &dest_len)) {
+    return HotSendStatus::kSendFailed;
+  }
+
+  int sock = socket(
+      endpoint.address.Index() == ae::AddrVersion::kIpV6 ? AF_INET6 : AF_INET,
+      SOCK_DGRAM, IPPROTO_IP);
+  if (sock < 0) {
+    return HotSendStatus::kSendFailed;
+  }
+
+  ResetFastTxDone();
+  auto sent = sendto(sock, packet.data(), packet.size(), 0,
+                     reinterpret_cast(&dest_storage), dest_len);
+  close(sock);
+
+  if (sent != static_cast(packet.size())) {
+    return HotSendStatus::kSendFailed;
+  }
+  return HotSendStatus::kSent;
+}
+#endif
+
 #else
 
 bool EnsureWifiConnectedForHotPath() { return true; }
@@ -1105,6 +1190,193 @@ bool StartBisectWifi(BisectFactorConfig const& cfg) {
   return true;
 }
 
+std::uint8_t ReadNegotiatedAuth() {
+  wifi_ap_record_t ap_info{};
+  if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK) {
+    return 0;
+  }
+  return static_cast(ap_info.authmode);
+}
+
+bool StartFastWifi(FastPathConfig const& cfg) {
+#  ifndef WIFI_SSID
+  return false;
+#  endif
+#  ifndef WIFI_PASSWORD
+  return false;
+#  endif
+
+  CleanupHotPathWifiRuntime();
+  g_bisect_actual_channel = 0;
+
+  bool const need_static_ip = cfg.use_static_ip && g_bisect_cache.valid_ip;
+  g_wait_got_ip = !need_static_ip;
+  g_using_bssid_cache = cfg.use_bssid && g_bisect_cache.valid_bssid;
+  g_max_wifi_retry = cfg.retry_max;
+
+  wifi_init_config_t wifi_init_cfg = WIFI_INIT_CONFIG_DEFAULT();
+  if (cfg.ampdu_tx_off) {
+    wifi_init_cfg.ampdu_tx_enable = 0;
+  }
+
+  auto err = nvs_flash_init();
+  if (err == ESP_ERR_NVS_NO_FREE_PAGES ||
+      err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
+    ESP_ERROR_CHECK(nvs_flash_erase());
+    err = nvs_flash_init();
+  }
+  if (err != ESP_OK && err != ESP_ERR_NVS_NO_FREE_PAGES) {
+    return false;
+  }
+
+  (void)esp_netif_init();
+  err = esp_event_loop_create_default();
+  if (err == ESP_OK) {
+    g_default_event_loop_created = true;
+  } else if (err != ESP_ERR_INVALID_STATE) {
+    return false;
+  }
+
+  g_wifi_event_group = xEventGroupCreate();
+  if (g_wifi_event_group == nullptr) {
+    CleanupHotPathWifiRuntime();
+    return false;
+  }
+
+  g_wifi_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+  if (g_wifi_netif == nullptr) {
+    g_wifi_netif = esp_netif_create_default_wifi_sta();
+  }
+  if (g_wifi_netif == nullptr) {
+    CleanupHotPathWifiRuntime();
+    return false;
+  }
+
+  if (need_static_ip) {
+    esp_netif_dhcpc_stop(g_wifi_netif);
+    esp_netif_ip_info_t ip_info = {
+        .ip = {.addr = g_bisect_cache.ip},
+        .netmask = {.addr = g_bisect_cache.netmask},
+        .gw = {.addr = g_bisect_cache.gateway}};
+    esp_netif_set_ip_info(g_wifi_netif, &ip_info);
+    rtc_ip_info = ip_info;
+    address_is_valid = true;
+  }
+
+  err = esp_wifi_init(&wifi_init_cfg);
+  if (err == ESP_ERR_WIFI_INIT_STATE) {
+    g_wifi_initialized = true;
+  } else if (err != ESP_OK) {
+    CleanupHotPathWifiRuntime();
+    return false;
+  } else {
+    g_wifi_initialized = true;
+  }
+
+  if (cfg.wifi_storage_ram) {
+    (void)esp_wifi_set_storage(WIFI_STORAGE_RAM);
+  }
+
+  err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
+                                            &WifiEventHandler, nullptr,
+                                            &g_wifi_any_id_handler);
+  if (err != ESP_OK) {
+    CleanupHotPathWifiRuntime();
+    return false;
+  }
+
+  err = esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
+                                            &WifiEventHandler, nullptr,
+                                            &g_wifi_got_ip_handler);
+  if (err != ESP_OK) {
+    CleanupHotPathWifiRuntime();
+    return false;
+  }
+
+  wifi_config_t wifi_config{};
+  std::strncpy(reinterpret_cast(wifi_config.sta.ssid), WIFI_SSID,
+               sizeof(wifi_config.sta.ssid));
+  std::strncpy(reinterpret_cast(wifi_config.sta.password), WIFI_PASSWORD,
+               sizeof(wifi_config.sta.password));
+
+  wifi_config.sta.pmf_cfg.capable = true;
+  if (cfg.auth == FastAuthMode::kWpa2) {
+    wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
+    wifi_config.sta.sae_pwe_h2e = WPA3_SAE_PWE_UNSPECIFIED;
+    wifi_config.sta.pmf_cfg.required = false;
+  } else if (cfg.auth == FastAuthMode::kWpa3H2eOnly) {
+    wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK;
+    wifi_config.sta.sae_pwe_h2e = WPA3_SAE_PWE_HASH_TO_ELEMENT;
+    wifi_config.sta.pmf_cfg.required = true;
+  } else {
+    wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK;
+    wifi_config.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH;
+    wifi_config.sta.pmf_cfg.required = true;
+  }
+
+  if (cfg.use_fast_scan) {
+    wifi_config.sta.scan_method = WIFI_FAST_SCAN;
+  }
+
+  if (cfg.use_bssid && g_bisect_cache.valid_bssid) {
+    wifi_config.sta.bssid_set = true;
+    std::memcpy(wifi_config.sta.bssid, g_bisect_cache.bssid,
+                sizeof(wifi_config.sta.bssid));
+  }
+
+  if (cfg.use_channel && g_bisect_cache.channel != 0) {
+    wifi_config.sta.channel = g_bisect_cache.channel;
+  }
+
+  err = esp_wifi_set_mode(WIFI_MODE_STA);
+  if (err != ESP_OK) {
+    CleanupHotPathWifiRuntime();
+    return false;
+  }
+
+  err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
+  if (err != ESP_OK) {
+    CleanupHotPathWifiRuntime();
+    return false;
+  }
+
+  (void)esp_wifi_set_protocol(
+      WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N);
+
+  err = esp_wifi_start();
+  if (err != ESP_OK) {
+    CleanupHotPathWifiRuntime();
+    return false;
+  }
+  g_wifi_started = true;
+
+  (void)esp_wifi_set_max_tx_power(80);
+  (void)esp_wifi_set_ps(WIFI_PS_NONE);
+
+  if (cfg.post_mode != FastPostMode::kFixedDelay) {
+    (void)esp_wifi_set_tx_done_cb(&FastTxDoneCb);
+  }
+
+  EventBits_t bits = xEventGroupWaitBits(
+      g_wifi_event_group, kWifiReadyBit | kWifiFailBit, pdFALSE, pdFALSE,
+      pdMS_TO_TICKS(AETHER_PREPARED_HOT_WIFI_TIMEOUT_MS));
+
+  if ((bits & kWifiReadyBit) == 0) {
+    return false;
+  }
+
+  g_bisect_actual_channel = ReadActualChannel();
+
+  if (cfg.use_static_arp && g_bisect_cache.valid_gw_mac &&
+      g_bisect_cache.valid_ip) {
+    std::memcpy(gateway_mac, g_bisect_cache.gw_mac, sizeof(gateway_mac));
+    gateway_mac_valid = true;
+    (void)InstallStaticGatewayArp();
+  }
+
+  return true;
+}
+
 }  // namespace
 
 bool FreezeBisectWifiCacheFromActiveConnection() {
@@ -1217,6 +1489,93 @@ BisectSendResult SendPreparedOnceWithBisectFactor(
   out.status = encode_status;
   return out;
 }
+
+FastSendResult SendPreparedOnceWithFastPath(FastPathConfig const& cfg,
+                                            ae::DataBuffer const& payload) {
+  FastSendResult out{};
+  out.requested_channel =
+      (cfg.use_channel && g_bisect_cache.channel != 0) ? g_bisect_cache.channel
+                                                       : 0;
+
+  if (!g_prepared_send_message_block.is_valid()) {
+    out.status = HotSendStatus::kNoPreparedBlock;
+    return out;
+  }
+  if (g_prepared_send_message_block.Resolve()->message_left == 0) {
+    out.status = HotSendStatus::kNonceExhausted;
+    return out;
+  }
+
+  auto const t0 = esp_timer_get_time();
+  if (!StartFastWifi(cfg)) {
+    CleanupHotPathWifiRuntime();
+    out.status = HotSendStatus::kWifiFailed;
+    out.actual_channel = g_bisect_actual_channel;
+    out.negotiated_auth = ReadNegotiatedAuth();
+    auto const elapsed = esp_timer_get_time() - t0;
+    out.cycle_us = elapsed < 0 ? 0 : static_cast(elapsed);
+    out.connect_us = out.cycle_us;
+    return out;
+  }
+
+  auto const t_ready = esp_timer_get_time();
+  {
+    auto const elapsed = t_ready - t0;
+    out.connect_us = elapsed < 0 ? 0 : static_cast(elapsed);
+  }
+  out.status_flags |=
+      static_cast(bench::BisectStatusBits::kWifiReady);
+  out.actual_channel = g_bisect_actual_channel;
+  out.negotiated_auth = ReadNegotiatedAuth();
+
+  if (cfg.pre_delay_ms > 0) {
+    vTaskDelay(pdMS_TO_TICKS(cfg.pre_delay_ms));
+  }
+
+  auto const encode_status = EncodeAndUdpSendTracked(payload);
+  if (encode_status == HotSendStatus::kSent) {
+    out.status_flags |=
+        static_cast(bench::BisectStatusBits::kEncodeOk) |
+        static_cast(bench::BisectStatusBits::kSendtoOk);
+  } else if (encode_status == HotSendStatus::kSendFailed) {
+    out.status_flags |=
+        static_cast(bench::BisectStatusBits::kEncodeOk);
+  }
+
+  if (encode_status == HotSendStatus::kSent &&
+      cfg.post_mode != FastPostMode::kFixedDelay) {
+    auto const t_cb = esp_timer_get_time();
+    while ((esp_timer_get_time() - t_cb) < 100000) {
+      if (g_fast_cb_match.load(std::memory_order_relaxed) != 0) {
+        break;
+      }
+      vTaskDelay(pdMS_TO_TICKS(1));
+    }
+    out.cb_any = g_fast_cb_any.load(std::memory_order_relaxed) != 0 ? 1 : 0;
+    out.cb_match = g_fast_cb_match.load(std::memory_order_relaxed) != 0 ? 1 : 0;
+    auto const cb_n = g_fast_cb_count.load(std::memory_order_relaxed);
+    out.cb_count = cb_n > 255 ? 255 : static_cast(cb_n);
+    std::uint16_t extra_ms = 0;
+    if (cfg.post_mode == FastPostMode::kTxDoneCbPlus10) {
+      extra_ms = 10;
+    } else if (cfg.post_mode == FastPostMode::kTxDoneCbPlus25) {
+      extra_ms = 25;
+    }
+    if (extra_ms > 0) {
+      vTaskDelay(pdMS_TO_TICKS(extra_ms));
+    }
+    (void)esp_wifi_set_tx_done_cb(nullptr);
+  } else if (encode_status == HotSendStatus::kSent && cfg.post_delay_ms > 0) {
+    vTaskDelay(pdMS_TO_TICKS(cfg.post_delay_ms));
+  }
+
+  CleanupHotPathWifiRuntime();
+
+  auto const elapsed = esp_timer_get_time() - t0;
+  out.cycle_us = elapsed < 0 ? 0 : static_cast(elapsed);
+  out.status = encode_status;
+  return out;
+}
 #endif
 
 HotSendStatus TryHotWakePreparedSend(
diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h
index 6566b3f..ee5920d 100644
--- a/main/prepared_send/prepared_send.h
+++ b/main/prepared_send/prepared_send.h
@@ -114,6 +114,52 @@ BisectWifiCacheSnapshot GetBisectWifiCacheSnapshot();
 // Wi-Fi failure before EncodePacket does not consume a prepared nonce.
 BisectSendResult SendPreparedOnceWithBisectFactor(
     WifiBisectVariant variant, ae::DataBuffer const& payload);
+
+enum class FastAuthMode : std::uint8_t {
+  kWpa3Both = 0,
+  kWpa3H2eOnly = 1,
+  kWpa2 = 2,
+};
+
+enum class FastPostMode : std::uint8_t {
+  kFixedDelay = 0,
+  kTxDoneCb = 1,
+  kTxDoneCbPlus10 = 2,
+  kTxDoneCbPlus25 = 3,
+};
+
+struct FastPathConfig {
+  bool use_bssid{false};
+  bool use_channel{true};
+  bool use_fast_scan{false};
+  bool use_static_ip{true};
+  bool use_static_arp{true};
+  bool ampdu_tx_off{false};
+  bool wifi_storage_ram{false};
+  FastAuthMode auth{FastAuthMode::kWpa3Both};
+  std::uint8_t retry_max{10};
+  std::uint16_t pre_delay_ms{200};
+  std::uint16_t post_delay_ms{300};
+  FastPostMode post_mode{FastPostMode::kFixedDelay};
+};
+
+struct FastSendResult {
+  HotSendStatus status{HotSendStatus::kWifiFailed};
+  std::uint32_t cycle_us{0};
+  std::uint32_t connect_us{0};
+  std::uint8_t requested_channel{0};
+  std::uint8_t actual_channel{0};
+  std::uint8_t negotiated_auth{0};
+  std::uint8_t status_flags{0};
+  std::uint8_t cb_any{0};
+  std::uint8_t cb_match{0};
+  std::uint8_t cb_count{0};
+};
+
+// BASE = cached channel + static IPv4/netmask/gw + static ARP. No BSSID.
+// Wi-Fi 4, WIFI_PS_NONE, auto PHY rate, max TX power. Timer excludes 1 s gap.
+FastSendResult SendPreparedOnceWithFastPath(FastPathConfig const& cfg,
+                                            ae::DataBuffer const& payload);
 #endif
 
 HotSendStatus TryHotWakePreparedSend(std::string const& temperature);
diff --git a/main/prepared_wifi_fastest_path_bench.cpp b/main/prepared_wifi_fastest_path_bench.cpp
new file mode 100644
index 0000000..83871a2
--- /dev/null
+++ b/main/prepared_wifi_fastest_path_bench.cpp
@@ -0,0 +1,621 @@
+/*
+ * Copyright 2026 Aethernet Inc.
+ *
+ * Silent fastest-path prepared reconnect bench (ESP32-C6).
+ * One compile-time variant per flash. Results travel in FastPayload.
+ *
+ * BASE: cached channel + static IPv4 + static ARP, no BSSID.
+ * Wi-Fi 4, WIFI_PS_NONE, auto PHY, max TX power. 1 s gap outside timer.
+ */
+
+#include 
+#include 
+#include 
+
+#include "aether/all.h"
+#include "aether/ae_exp_wifi.h"
+#include "aether/config.h"
+#include "aether/env.h"
+#include "bench_payload.h"
+#include "prepared_send/prepared_send.h"
+
+#if defined(ESP_PLATFORM)
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#endif
+
+using namespace std::chrono_literals;
+
+namespace temp_sensor {
+namespace {
+
+static constexpr auto kParentUid =
+    ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165");
+
+#ifndef BENCH_CLIENT_ID
+#  define BENCH_CLIENT_ID "prepared_wifi_bisect_v1"
+#endif
+static constexpr char const* kBenchClientId = BENCH_CLIENT_ID;
+
+#if defined(SERVICE_UID)
+static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID);
+#else
+static constexpr auto kServiceUid =
+    ae::Uid::FromString("3d284a4f-ebb4-451e-a2c5-aecb0d647a45");
+#endif
+
+#ifndef AE_EXP_FAST_N
+#  define AE_EXP_FAST_N 20
+#endif
+#ifndef AE_EXP_FAST_TEST_ID
+#  define AE_EXP_FAST_TEST_ID 0
+#endif
+#ifndef AE_EXP_FAST_PRE_MS
+#  define AE_EXP_FAST_PRE_MS 200
+#endif
+#ifndef AE_EXP_FAST_POST_MS
+#  define AE_EXP_FAST_POST_MS 300
+#endif
+#ifndef AE_EXP_FAST_USE_BSSID
+#  define AE_EXP_FAST_USE_BSSID 0
+#endif
+#ifndef AE_EXP_FAST_FAST_SCAN
+#  define AE_EXP_FAST_FAST_SCAN 0
+#endif
+#ifndef AE_EXP_FAST_AUTH
+#  define AE_EXP_FAST_AUTH 0
+#endif
+#ifndef AE_EXP_FAST_RETRY
+#  define AE_EXP_FAST_RETRY 10
+#endif
+#ifndef AE_EXP_FAST_POST_MODE
+#  define AE_EXP_FAST_POST_MODE 0
+#endif
+#ifndef AE_EXP_FAST_AMPDU_TX_OFF
+#  define AE_EXP_FAST_AMPDU_TX_OFF 0
+#endif
+#ifndef AE_EXP_FAST_STORAGE_RAM
+#  define AE_EXP_FAST_STORAGE_RAM 0
+#endif
+
+static constexpr int kPreparedPerVariant = AE_EXP_FAST_N;
+static constexpr int kPreparedGapMs = 1000;
+static constexpr std::uint8_t kTestId =
+    static_cast(AE_EXP_FAST_TEST_ID);
+
+#if defined(ESP_PLATFORM)
+static const auto kWifiInit = ae::WiFiInit{
+    std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}},
+    {},
+};
+
+static bool g_had_aether_app = false;
+
+static void PreConstructCleanup() {
+  if (!g_had_aether_app) {
+    return;
+  }
+#  if !AE_WIFI_USE_FULL_DEINIT
+  esp_netif_deinit();
+  esp_event_loop_delete_default();
+#  endif
+}
+
+static std::int64_t NowUs() { return esp_timer_get_time(); }
+#else
+static std::int64_t NowUs() { return 0; }
+#endif
+
+enum class Phase : std::uint8_t {
+  kRegister,
+  kFullCycle,
+  kPrepared,
+  kFinal,
+  kDone,
+};
+
+static std::shared_ptr g_app;
+static ae::Client::ptr g_client;
+static std::unique_ptr g_stream;
+static ae::Subscription g_select_sub;
+static ae::Subscription g_stream_sub;
+static ae::Subscription g_write_sub;
+
+static Phase g_phase = Phase::kRegister;
+static bool g_registration_pending = false;
+static bool g_write_armed = false;
+static bool g_done = false;
+
+static bool g_pending_register_finish = false;
+static bool g_pending_full_post_write = false;
+static bool g_full_write_ok = false;
+static bool g_pending_final_exit = false;
+
+static int g_prepared_index = 0;
+static bool g_prepared_waiting_gap = false;
+#if defined(ESP_PLATFORM)
+static TickType_t g_prepared_gap_until = 0;
+#endif
+
+static std::uint16_t g_seq = 0;
+static std::uint32_t g_registration_us = 0;
+static std::uint32_t g_pending_full_us = 0;
+static bool g_have_pending_full = false;
+
+static std::uint16_t g_wifi_ready_count = 0;
+static std::uint16_t g_encode_count = 0;
+static std::uint16_t g_sendto_count = 0;
+static std::uint16_t g_nonce_start = 0;
+
+static prepared_send::FastSendResult g_last_result{};
+static prepared_send::BisectWifiCacheSnapshot g_cache{};
+static prepared_send::FastPathConfig g_cfg{};
+static std::uint8_t g_assoc_bits = 0;
+
+static std::int64_t g_t0 = 0;
+
+static void ReleaseApp() {
+  g_select_sub.Reset();
+  g_stream_sub.Reset();
+  g_write_sub.Reset();
+  g_stream.reset();
+  g_client = {};
+  g_app.reset();
+}
+
+static std::uint16_t NextSeq() { return ++g_seq; }
+
+static prepared_send::FastPathConfig MakeFastConfig() {
+  prepared_send::FastPathConfig c{};
+  c.use_bssid = AE_EXP_FAST_USE_BSSID != 0;
+  c.use_channel = true;
+  c.use_fast_scan = AE_EXP_FAST_FAST_SCAN != 0;
+  c.use_static_ip = true;
+  c.use_static_arp = true;
+  c.ampdu_tx_off = AE_EXP_FAST_AMPDU_TX_OFF != 0;
+  c.wifi_storage_ram = AE_EXP_FAST_STORAGE_RAM != 0;
+  c.auth = static_cast(AE_EXP_FAST_AUTH);
+  c.retry_max = static_cast(AE_EXP_FAST_RETRY);
+  c.pre_delay_ms = static_cast(AE_EXP_FAST_PRE_MS);
+  c.post_delay_ms = static_cast(AE_EXP_FAST_POST_MS);
+  c.post_mode = static_cast(AE_EXP_FAST_POST_MODE);
+  return c;
+}
+
+static std::uint8_t AssocBitsOf(prepared_send::FastPathConfig const& c) {
+  using F = bench::FastAssocBits;
+  std::uint8_t bits = 0;
+  if (c.use_bssid) {
+    bits |= static_cast(F::kBssid);
+  }
+  if (c.use_channel) {
+    bits |= static_cast(F::kChannel);
+  }
+  if (c.use_fast_scan) {
+    bits |= static_cast(F::kFastScan);
+  }
+  if (c.use_static_ip) {
+    bits |= static_cast(F::kStaticIp);
+  }
+  if (c.use_static_arp) {
+    bits |= static_cast(F::kStaticArp);
+  }
+  if (c.ampdu_tx_off) {
+    bits |= static_cast(F::kAmpduTxOff);
+  }
+  if (c.wifi_storage_ram) {
+    bits |= static_cast(F::kStorageRam);
+  }
+  if (c.post_mode != prepared_send::FastPostMode::kFixedDelay) {
+    bits |= static_cast(F::kCallback);
+  }
+  return bits;
+}
+
+static void FillCommon(bench::FastPayload& p) {
+  p.test_id = kTestId;
+  p.pre_ms = g_cfg.pre_delay_ms;
+  p.post_ms = g_cfg.post_delay_ms;
+  p.assoc_bits = g_assoc_bits;
+  p.retry_max = g_cfg.retry_max;
+  p.post_mode = static_cast(g_cfg.post_mode);
+}
+
+static ae::DataBuffer MakeFullPayload() {
+  bench::FastPayload p{};
+  p.type = static_cast(bench::FastMsgType::kFull);
+  FillCommon(p);
+  p.prepared_index = static_cast(
+      kPreparedPerVariant > 255 ? 255 : kPreparedPerVariant);
+  p.sequence_global = NextSeq();
+  p.cycle_us = g_have_pending_full ? g_pending_full_us : 0;
+  p.connect_us = g_registration_us;
+  g_have_pending_full = false;
+  return bench::EncodeFast(p);
+}
+
+static ae::DataBuffer MakePreparedPayload(int index) {
+  bench::FastPayload p{};
+  p.type = static_cast(bench::FastMsgType::kPrepared);
+  FillCommon(p);
+  p.prepared_index = static_cast(index);
+  p.sequence_global = NextSeq();
+  if (index > 1) {
+    p.cycle_us = g_last_result.cycle_us;
+    p.connect_us = g_last_result.connect_us;
+    p.status_flags = g_last_result.status_flags;
+    p.auth_negotiated = g_last_result.negotiated_auth;
+    p.cb_any = g_last_result.cb_any;
+    p.cb_match = g_last_result.cb_match;
+    p.cb_count = g_last_result.cb_count;
+  }
+  return bench::EncodeFast(p);
+}
+
+static ae::DataBuffer MakeFinalPayload() {
+  bench::FastPayload p{};
+  p.type = static_cast(bench::FastMsgType::kFinal);
+  FillCommon(p);
+  p.prepared_index = static_cast(
+      kPreparedPerVariant > 255 ? 255 : kPreparedPerVariant);
+  p.sequence_global = NextSeq();
+  p.cycle_us = g_last_result.cycle_us;
+  p.connect_us = g_last_result.connect_us;
+  p.status_flags = g_last_result.status_flags;
+  p.auth_negotiated = g_last_result.negotiated_auth;
+  p.wifi_ready_count = g_wifi_ready_count;
+  p.encode_count = g_encode_count;
+  p.sendto_count = g_sendto_count;
+  auto const left = prepared_send::PreparedMessageLeft();
+  std::uint32_t consumed = 0;
+  if (g_nonce_start >= left) {
+    consumed = g_nonce_start - left;
+  } else {
+    consumed = g_encode_count;
+  }
+  p.nonce_consumed = consumed > 0xffffu ? 0xffffu
+                                        : static_cast(consumed);
+  p.cb_any = g_last_result.cb_any;
+  p.cb_match = g_last_result.cb_match;
+  p.cb_count = g_last_result.cb_count;
+  return bench::EncodeFast(p);
+}
+
+static void ConstructAether() {
+#if defined(ESP_PLATFORM)
+  PreConstructCleanup();
+#endif
+  g_had_aether_app = true;
+  g_app = ae::AetherApp::Construct(
+      ae::AetherAppContext{}
+#if AE_DISTILLATION && defined(ESP_PLATFORM)
+          .AddAdapterFactory([&](ae::AetherAppContext const& ctx) {
+            return ae::WifiAdapter::ptr::Create(
+                ae::CreateWith{ctx.domain()}.with_id(
+                    ae::GlobalId::kWiFiAdapter),
+                ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit);
+          })
+#endif
+  );
+}
+
+static void DoFullWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto payload = MakeFullPayload();
+  auto& wa = g_stream->Write(std::move(payload));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_full_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_full_post_write = true;
+  });
+}
+
+static void MaybeFullWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFullWrite();
+}
+
+static void OnFullClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); });
+  MaybeFullWrite();
+}
+
+static void StartRegister() {
+  g_phase = Phase::kRegister;
+  g_write_armed = false;
+  g_pending_register_finish = false;
+  g_t0 = NowUs();
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       g_pending_register_finish = true;
+                     });
+}
+
+static void StartFullCycle() {
+  g_phase = Phase::kFullCycle;
+  g_write_armed = false;
+  g_pending_full_post_write = false;
+  g_full_write_ok = false;
+  g_select_sub.Reset();
+  g_stream_sub.Reset();
+  g_write_sub.Reset();
+  g_stream.reset();
+  g_client = {};
+  g_t0 = NowUs();
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFullClientReady(std::move(res).value());
+                     });
+}
+
+static void StartPreparedPhase() {
+  g_phase = Phase::kPrepared;
+  g_prepared_index = 1;
+  g_prepared_waiting_gap = false;
+  g_wifi_ready_count = 0;
+  g_encode_count = 0;
+  g_sendto_count = 0;
+  g_last_result = {};
+  g_nonce_start =
+      static_cast(prepared_send::PreparedMessageLeft());
+#if defined(ESP_PLATFORM)
+  prepared_send::ReleaseFullAetherWifiForHotPath();
+  vTaskDelay(pdMS_TO_TICKS(200));
+#endif
+}
+
+static void DoFinalWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto& wa = g_stream->Write(MakeFinalPayload());
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status) {
+    g_pending_final_exit = true;
+  });
+}
+
+static void MaybeFinalWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFinalWrite();
+}
+
+static void OnFinalClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFinalWrite(); });
+  MaybeFinalWrite();
+}
+
+static void StartFinal() {
+  g_phase = Phase::kFinal;
+  g_write_armed = false;
+  g_pending_final_exit = false;
+  g_select_sub.Reset();
+  g_stream_sub.Reset();
+  g_write_sub.Reset();
+  g_stream.reset();
+  g_client = {};
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFinalClientReady(std::move(res).value());
+                     });
+}
+
+static void FinishRegisterInLoop() {
+  g_app->aether().Save();
+  g_app->Exit(0);
+}
+
+static void FinishFullPostWriteInLoop() {
+  if (!g_full_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  if (!prepared_send::ExportPreparedSendBlock(g_client, kServiceUid,
+                                              kPreparedPerVariant)) {
+    g_app->Exit(1);
+    return;
+  }
+  if (!prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() !=
+          static_cast(kPreparedPerVariant)) {
+    g_app->Exit(1);
+    return;
+  }
+  if (!prepared_send::FreezeBisectWifiCacheFromActiveConnection()) {
+    g_app->Exit(1);
+    return;
+  }
+  g_cache = prepared_send::GetBisectWifiCacheSnapshot();
+  g_app->aether().Save();
+  g_app->Exit(0);
+}
+
+}  // namespace
+}  // namespace temp_sensor
+
+void setup() {
+  using namespace temp_sensor;
+  g_cfg = MakeFastConfig();
+  g_assoc_bits = AssocBitsOf(g_cfg);
+#if defined(ESP_PLATFORM)
+  nvs_flash_init();
+  prepared_send::InvalidatePreparedWifiCache();
+#endif
+  g_done = false;
+  g_seq = 0;
+  g_registration_pending = true;
+  g_pending_register_finish = false;
+  g_pending_full_post_write = false;
+  g_pending_final_exit = false;
+}
+
+void loop() {
+  using namespace temp_sensor;
+  if (g_done) {
+    return;
+  }
+
+  if (g_registration_pending) {
+    g_registration_pending = false;
+    StartRegister();
+    return;
+  }
+
+  auto process_deferred = []() {
+    if (g_app && g_pending_register_finish) {
+      g_pending_register_finish = false;
+      FinishRegisterInLoop();
+      return true;
+    }
+    if (g_app && g_pending_full_post_write) {
+      g_pending_full_post_write = false;
+      FinishFullPostWriteInLoop();
+      return true;
+    }
+    if (g_app && g_pending_final_exit) {
+      g_pending_final_exit = false;
+      g_app->Exit(0);
+      return true;
+    }
+    return false;
+  };
+
+  if (process_deferred()) {
+    return;
+  }
+
+  if (g_phase == Phase::kPrepared) {
+#if defined(ESP_PLATFORM)
+    if (g_prepared_waiting_gap) {
+      if (xTaskGetTickCount() < g_prepared_gap_until) {
+        vTaskDelay(pdMS_TO_TICKS(20));
+        return;
+      }
+      g_prepared_waiting_gap = false;
+    }
+#endif
+
+    if (g_prepared_index > kPreparedPerVariant) {
+      StartFinal();
+      return;
+    }
+
+    int const i = g_prepared_index;
+    auto payload = MakePreparedPayload(i);
+#if defined(ESP_PLATFORM)
+    auto const result =
+        prepared_send::SendPreparedOnceWithFastPath(g_cfg, payload);
+#else
+    prepared_send::FastSendResult result{};
+    result.status = prepared_send::HotSendStatus::kUnsupported;
+#endif
+    g_last_result = result;
+    if (result.status_flags &
+        static_cast(bench::BisectStatusBits::kWifiReady)) {
+      ++g_wifi_ready_count;
+    }
+    if (result.status_flags &
+        static_cast(bench::BisectStatusBits::kEncodeOk)) {
+      ++g_encode_count;
+    }
+    if (result.status_flags &
+        static_cast(bench::BisectStatusBits::kSendtoOk)) {
+      ++g_sendto_count;
+    }
+
+    ++g_prepared_index;
+    if (g_prepared_index <= kPreparedPerVariant) {
+#if defined(ESP_PLATFORM)
+      g_prepared_waiting_gap = true;
+      g_prepared_gap_until =
+          xTaskGetTickCount() + pdMS_TO_TICKS(kPreparedGapMs);
+#endif
+    }
+    return;
+  }
+
+  if (!g_app) {
+    return;
+  }
+
+  if (!g_app->IsExited()) {
+    auto t = g_app->Update(ae::Now());
+    if (process_deferred()) {
+      return;
+    }
+    if (!g_app->IsExited()) {
+      g_app->WaitUntil(t);
+    }
+    return;
+  }
+
+  if (g_phase == Phase::kRegister) {
+    ReleaseApp();
+    g_registration_us = static_cast(NowUs() - g_t0);
+    StartFullCycle();
+    return;
+  }
+
+  if (g_phase == Phase::kFullCycle) {
+    ReleaseApp();
+    auto const full_us = static_cast(NowUs() - g_t0);
+    g_pending_full_us = full_us;
+    g_have_pending_full = true;
+    StartPreparedPhase();
+    return;
+  }
+
+  if (g_phase == Phase::kFinal) {
+    ReleaseApp();
+    g_phase = Phase::kDone;
+    g_done = true;
+  }
+}
diff --git a/sdkconfig.defaults.fastest b/sdkconfig.defaults.fastest
new file mode 100644
index 0000000..4ae8a5a
--- /dev/null
+++ b/sdkconfig.defaults.fastest
@@ -0,0 +1,6 @@
+# Fastest-path measurement extras on top of sdkconfig.defaults.silent.
+# CPU: 160 MHz, no DFS / no power-management experiments.
+
+# CONFIG_PM_ENABLE is not set
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160
diff --git a/sdkconfig.defaults.wpa2only b/sdkconfig.defaults.wpa2only
new file mode 100644
index 0000000..0a9081c
--- /dev/null
+++ b/sdkconfig.defaults.wpa2only
@@ -0,0 +1,3 @@
+# Benchmark-only overlay. Do not use for production firmware.
+# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set
+# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set
diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp
index e31eed7..5a70e38 100644
--- a/temperature_receiver/main.cpp
+++ b/temperature_receiver/main.cpp
@@ -1,14 +1,15 @@
 /*
  * Copyright 2026 Aethernet Inc.
  *
- * Desktop Æther receiver for prepared Wi-Fi single-factor bisect.
+ * Desktop Æther receiver for silent fastest-path prepared Wi-Fi campaign.
+ * Stays up across firmware reflashes; prints TEST_RESULT after each FINAL.
  */
 
 #include 
-#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -28,44 +29,36 @@ namespace {
 
 static constexpr auto kParentUid =
     ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165");
-// Reuse the stable cache-bench receiver identity (UID 5aade50f-...).
 static constexpr char const* kClientName = "prepared_wifi_cache_rx_v1";
-static constexpr int kVariants =
-    static_cast(temp_sensor::bench::BisectVariant::kCount);
-static constexpr int kPreparedPer = 20;
 
-struct VariantStats {
+struct TestStats {
+  int planned{20};
   int delivered{0};
   int duplicates{0};
-  std::array got{};
-  std::array have_us{};
-  std::array us{};
-  std::array req_ch{};
-  std::array act_ch{};
-  std::uint8_t wifi_ready{0};
-  std::uint8_t encode{0};
-  std::uint8_t sendto{0};
-  std::uint8_t nonce{0};
-  bool have_summary{false};
-  bool have_meta{false};
-  std::uint8_t cached_channel{0};
-  std::uint32_t cached_ip{0};
-  std::uint8_t pre_delay_ms{0};
-  int channel_match{0};
-  int channel_mismatch{0};
+  std::vector got;
+  std::vector cycle_us;
+  std::vector connect_us;
+  std::uint16_t wifi_ready{0};
+  std::uint16_t encode{0};
+  std::uint16_t sendto{0};
+  std::uint16_t nonce{0};
+  std::uint8_t test_id{0};
+  std::uint16_t pre_ms{0};
+  std::uint16_t post_ms{0};
+  std::uint8_t assoc_bits{0};
+  std::uint8_t auth{0};
+  std::uint8_t retry_max{0};
+  std::uint8_t post_mode{0};
+  int cb_any{0};
+  int cb_match{0};
 };
 
 std::mutex g_mu;
 std::vector> g_streams;
-std::array g_var{};
-std::vector g_seen_seq;
-int g_last_seq = 0;
-int g_out_of_order = 0;
+TestStats g_st{};
 int g_full_recv = 0;
-int g_meta_recv = 0;
 int g_prep_recv = 0;
 int g_final_recv = 0;
-bool g_done = false;
 
 std::int64_t NowMs() {
   return std::chrono::duration_cast(
@@ -73,236 +66,166 @@ std::int64_t NowMs() {
       .count();
 }
 
-std::uint32_t MedianUs(std::vector v) {
+std::uint32_t PercentileUs(std::vector v, int pct) {
   if (v.empty()) {
     return 0;
   }
   std::sort(v.begin(), v.end());
-  return v[v.size() / 2];
+  auto const i = static_cast((v.size() - 1) * pct / 100);
+  return v[i];
 }
 
-char const* Verdict(int delivered, int wifi_ready) {
-  if (wifi_ready > 0 && wifi_ready < 10) {
-    return "INCONCLUSIVE";
-  }
-  if (delivered >= 18) {
-    return "OK";
-  }
-  if (delivered >= 10) {
-    return "DEGRADES";
-  }
-  return "BREAKS";
+void ResetStats(std::uint8_t test_id, int planned) {
+  g_st = {};
+  g_st.test_id = test_id;
+  g_st.planned = planned > 0 ? planned : 20;
+  g_st.got.assign(static_cast(g_st.planned), 0);
 }
 
-void PrintSummary() {
-  std::cout << "BISECT_TABLE\n";
-  std::cout << "Variant\tSingle change\tDelivered/20\tMissing\tMedian_ms\t"
-               "WifiReady\tEncode\tSendto\tNonce\tVerdict\n";
-  for (int v = 0; v < kVariants; ++v) {
-    auto const& s = g_var[static_cast(v)];
-    int missing = kPreparedPer - s.delivered;
-    if (missing < 0) {
-      missing = 0;
-    }
-    std::vector times;
-    for (int i = 0; i < kPreparedPer; ++i) {
-      if (s.have_us[static_cast(i)]) {
-        times.push_back(s.us[static_cast(i)]);
-      }
-    }
-    auto const med_ms = MedianUs(times) / 1000;
-    std::cout << temp_sensor::bench::BisectVariantName(
-                     static_cast(v))
-              << '\t'
-              << temp_sensor::bench::BisectVariantChange(
-                     static_cast(v))
-              << '\t' << s.delivered << "/20\t" << missing << '\t' << med_ms
-              << '\t' << static_cast(s.wifi_ready) << '\t'
-              << static_cast(s.encode) << '\t'
-              << static_cast(s.sendto) << '\t'
-              << static_cast(s.nonce) << '\t'
-              << Verdict(s.delivered, s.wifi_ready) << '\n';
-  }
-
-  auto get = [](int id) {
-    return g_var[static_cast(id)].delivered;
-  };
-  std::cout << "CHANNEL_HYPOTHESIS\n";
-  std::cout << "B1 no cache = " << get(1) << "/20\n";
-  std::cout << "C1 BSSID only = " << get(2) << "/20\n";
-  std::cout << "C2 channel only = " << get(3) << "/20\n";
-  std::cout << "C3 BSSID+channel = " << get(4) << "/20\n";
-  std::cout << "C7 BSSID+static IP = " << get(8) << "/20\n";
-  std::cout << "C8 channel+static IP = " << get(9) << "/20\n";
-
-  bool channel_bad = false;
-  bool channel_ok = false;
-  // Correlate: variants that set channel (C2,C3,C8) vs without (B1,C1,C7)
-  auto const with_ch = get(3) + get(4) + get(9);
-  auto const without_ch = get(1) + get(2) + get(8);
-  if (without_ch - with_ch >= 15) {
-    channel_bad = true;
+void NotePrepared(int idx) {
+  if (idx < 1) {
+    return;
   }
-  if (with_ch >= without_ch - 3) {
-    channel_ok = true;
+  if (idx > g_st.planned) {
+    g_st.planned = idx;
+    g_st.got.resize(static_cast(g_st.planned), 0);
   }
-  std::cout << "Does cached channel independently correlate with loss? ";
-  if (channel_bad && !channel_ok) {
-    std::cout << "YES\n";
-  } else if (!channel_bad && channel_ok) {
-    std::cout << "NO\n";
+  auto& seen = g_st.got[static_cast(idx - 1)];
+  if (!seen) {
+    seen = 1;
+    ++g_st.delivered;
   } else {
-    std::cout << "INCONCLUSIVE\n";
-  }
-
-  std::cout << "CHANNEL_MATCH_COUNTS\n";
-  for (int v : {3, 4, 9}) {
-    auto const& s = g_var[static_cast(v)];
-    std::cout << temp_sensor::bench::BisectVariantName(
-                     static_cast(v))
-              << " match=" << s.channel_match
-              << " mismatch=" << s.channel_mismatch
-              << " cached_ch=" << static_cast(s.cached_channel) << '\n';
-  }
-
-  std::cout << "DELIVERY_TOTALS\n";
-  std::cout << "  full=" << g_full_recv << "/" << kVariants << "\n";
-  std::cout << "  meta=" << g_meta_recv << "/" << kVariants << "\n";
-  std::cout << "  prepared=" << g_prep_recv << "/" << (kVariants * kPreparedPer)
-            << "\n";
-  std::cout << "  final=" << g_final_recv << "/1\n";
-  std::cout << "  out_of_order=" << g_out_of_order << "\n";
-  std::cout << "BENCH_DONE\n";
-  std::cout.flush();
-}
-
-void NoteSeq(std::uint16_t seq) {
-  if (std::find(g_seen_seq.begin(), g_seen_seq.end(), seq) !=
-      g_seen_seq.end()) {
-    return;
+    ++g_st.duplicates;
   }
-  g_seen_seq.push_back(seq);
-  if (seq != 0 && g_last_seq != 0 &&
-      seq < static_cast(g_last_seq)) {
-    ++g_out_of_order;
-  }
-  g_last_seq = seq;
 }
 
-void ApplyPreparedMetrics(int v, int slot,
-                          temp_sensor::bench::BisectPayload const& p) {
-  if (v < 0 || v >= kVariants || slot < 0 || slot >= kPreparedPer) {
-    return;
-  }
-  auto& s = g_var[static_cast(v)];
-  if (p.time_us != 0) {
-    s.us[static_cast(slot)] = p.time_us;
-    s.have_us[static_cast(slot)] = true;
-  }
-  s.req_ch[static_cast(slot)] = p.requested_channel;
-  s.act_ch[static_cast(slot)] = p.actual_channel;
-  if (p.requested_channel != 0) {
-    if (p.requested_channel == p.actual_channel) {
-      ++s.channel_match;
-    } else if (p.actual_channel != 0) {
-      ++s.channel_mismatch;
-    }
-  }
+void PrintTestResult() {
+  auto const cyc_med = PercentileUs(g_st.cycle_us, 50) / 1000;
+  auto const cyc_p90 = PercentileUs(g_st.cycle_us, 90) / 1000;
+  auto const cyc_max =
+      g_st.cycle_us.empty()
+          ? 0
+          : *std::max_element(g_st.cycle_us.begin(), g_st.cycle_us.end()) /
+                1000;
+  auto const conn_med = PercentileUs(g_st.connect_us, 50) / 1000;
+  std::cout << "TEST_RESULT"
+            << " test_id=" << static_cast(g_st.test_id)
+            << " n=" << g_st.planned << " delivered=" << g_st.delivered << "/"
+            << g_st.planned << " connect_med_ms=" << conn_med
+            << " cycle_med_ms=" << cyc_med << " p90_ms=" << cyc_p90
+            << " max_ms=" << cyc_max
+            << " wifi_ready=" << static_cast(g_st.wifi_ready)
+            << " encode=" << static_cast(g_st.encode)
+            << " sendto=" << static_cast(g_st.sendto)
+            << " nonce=" << static_cast(g_st.nonce)
+            << " pre=" << g_st.pre_ms << " post=" << g_st.post_ms
+            << " assoc=0x" << std::hex << static_cast(g_st.assoc_bits)
+            << std::dec << " auth=" << static_cast(g_st.auth)
+            << " retry=" << static_cast(g_st.retry_max)
+            << " post_mode=" << static_cast(g_st.post_mode)
+            << " cb_any=" << g_st.cb_any << " cb_match=" << g_st.cb_match
+            << " samples=" << g_st.cycle_us.size() << "\n";
+  std::cout << "BENCH_DONE test_id=" << static_cast(g_st.test_id) << "\n";
+  std::cout.flush();
 }
 
-void OnBisect(temp_sensor::bench::BisectPayload const& p) {
+void OnFast(temp_sensor::bench::FastPayload const& p) {
   auto const ts = NowMs();
-  NoteSeq(p.sequence_global);
-  int const v = static_cast(p.variant_id);
-  auto type = static_cast(p.type);
-
-  if (type == temp_sensor::bench::BisectMsgType::kFull) {
+  auto const type = static_cast(p.type);
+  if (type == temp_sensor::bench::FastMsgType::kFull) {
     ++g_full_recv;
-    if (v >= 1 && v < kVariants) {
-      // Previous variant summary rides on this FULL.
-      auto& prev = g_var[static_cast(v - 1)];
-      prev.wifi_ready = p.wifi_ready_count;
-      prev.encode = p.encode_count;
-      prev.sendto = p.sendto_count;
-      prev.nonce = p.nonce_consumed;
-      prev.have_summary = true;
-    }
-    std::cout << ae::Format(
-        "RECV FULL variant={} seq={} time_us={} ts={}\n",
-        temp_sensor::bench::BisectVariantName(p.variant_id), p.sequence_global,
-        p.time_us, ts);
-  } else if (type == temp_sensor::bench::BisectMsgType::kMeta) {
-    ++g_meta_recv;
-    if (v >= 0 && v < kVariants) {
-      auto& s = g_var[static_cast(v)];
-      s.have_meta = true;
-      s.cached_channel = p.cached_channel;
-      s.cached_ip = p.cached_ip;
-      s.pre_delay_ms = p.pre_delay_ms;
+    int planned = p.prepared_index;
+    if (planned == 0) {
+      planned = 20;
     }
-    std::cout << ae::Format(
-        "RECV META variant={} seq={} cached_ch={} cached_ip={:08x} pre_ms={} "
-        "ts={}\n",
-        temp_sensor::bench::BisectVariantName(p.variant_id), p.sequence_global,
-        p.cached_channel, p.cached_ip, p.pre_delay_ms, ts);
-  } else if (type == temp_sensor::bench::BisectMsgType::kPrepared) {
+    ResetStats(p.test_id, planned);
+    g_st.pre_ms = p.pre_ms;
+    g_st.post_ms = p.post_ms;
+    g_st.assoc_bits = p.assoc_bits;
+    g_st.retry_max = p.retry_max;
+    g_st.post_mode = p.post_mode;
+    std::cout << ae::Format("RECV FULL test_id={} n={} seq={} ts={}\n",
+                            p.test_id, planned, p.sequence_global, ts);
+  } else if (type == temp_sensor::bench::FastMsgType::kPrepared) {
     ++g_prep_recv;
-    if (v >= 0 && v < kVariants) {
-      auto& s = g_var[static_cast(v)];
-      int const idx = static_cast(p.prepared_index);
-      if (idx == 1 && p.cached_channel != 0) {
-        s.have_meta = true;
-        s.cached_channel = p.cached_channel;
-        s.cached_ip = p.cached_ip;
-        s.pre_delay_ms = p.pre_delay_ms;
-      }
-      if (idx >= 1 && idx <= kPreparedPer) {
-        auto& seen = s.got[static_cast(idx - 1)];
-        if (!seen) {
-          seen = true;
-          ++s.delivered;
-        } else {
-          ++s.duplicates;
+    if (g_st.planned == 0 || g_st.test_id != p.test_id) {
+      // New test without FULL, or FULL was lost — start a fresh window.
+      int planned = p.prepared_index > 0 ? static_cast(p.prepared_index) : 20;
+      // prepared_index is 1-based send index, not N; keep previous planned if
+      // same test, otherwise default to at least the index we just saw.
+      if (g_st.test_id != p.test_id || g_st.planned == 0) {
+        planned = 20;
+        if (p.prepared_index > planned) {
+          planned = p.prepared_index;
         }
+        ResetStats(p.test_id, planned);
       }
-      if (idx >= 2) {
-        ApplyPreparedMetrics(v, idx - 2, p);
-      }
+    }
+    NotePrepared(p.prepared_index);
+    g_st.pre_ms = p.pre_ms;
+    g_st.post_ms = p.post_ms;
+    g_st.assoc_bits = p.assoc_bits;
+    g_st.auth = p.auth_negotiated;
+    g_st.retry_max = p.retry_max;
+    g_st.post_mode = p.post_mode;
+    g_st.cb_any += p.cb_any;
+    g_st.cb_match += p.cb_match;
+    if (p.cycle_us != 0) {
+      g_st.cycle_us.push_back(p.cycle_us);
+    }
+    if (p.connect_us != 0) {
+      g_st.connect_us.push_back(p.connect_us);
     }
     std::cout << ae::Format(
-        "RECV PREPARED variant={} idx={} seq={} prev_us={} req_ch={} act_ch={} "
-        "flags={} ts={}\n",
-        temp_sensor::bench::BisectVariantName(p.variant_id), p.prepared_index,
-        p.sequence_global, p.time_us, p.requested_channel, p.actual_channel,
-        p.status_flags, ts);
-  } else if (type == temp_sensor::bench::BisectMsgType::kFinal) {
+        "RECV PREPARED test_id={} idx={} seq={} cycle_us={} connect_us={} "
+        "auth={} flags={} ts={}\n",
+        p.test_id, p.prepared_index, p.sequence_global, p.cycle_us,
+        p.connect_us, p.auth_negotiated, p.status_flags, ts);
+  } else if (type == temp_sensor::bench::FastMsgType::kFinal) {
     ++g_final_recv;
-    if (v >= 0 && v < kVariants) {
-      auto& s = g_var[static_cast(v)];
-      s.wifi_ready = p.wifi_ready_count;
-      s.encode = p.encode_count;
-      s.sendto = p.sendto_count;
-      s.nonce = p.nonce_consumed;
-      s.have_summary = true;
-      ApplyPreparedMetrics(v, kPreparedPer - 1, p);
+    g_st.test_id = p.test_id;
+    if (p.prepared_index != 0) {
+      g_st.planned = p.prepared_index;
+    } else if (p.wifi_ready_count != 0) {
+      g_st.planned = p.wifi_ready_count;
+    }
+    g_st.wifi_ready = p.wifi_ready_count;
+    g_st.encode = p.encode_count;
+    g_st.sendto = p.sendto_count;
+    g_st.nonce = p.nonce_consumed;
+    g_st.auth = p.auth_negotiated;
+    g_st.pre_ms = p.pre_ms;
+    g_st.post_ms = p.post_ms;
+    g_st.assoc_bits = p.assoc_bits;
+    g_st.retry_max = p.retry_max;
+    g_st.post_mode = p.post_mode;
+    g_st.cb_any += p.cb_any;
+    g_st.cb_match += p.cb_match;
+    if (p.cycle_us != 0) {
+      g_st.cycle_us.push_back(p.cycle_us);
+    }
+    if (p.connect_us != 0) {
+      g_st.connect_us.push_back(p.connect_us);
+    }
+    // Prefer device counters for delivery when FULL was missed.
+    if (g_st.delivered == 0 && p.sendto_count != 0) {
+      g_st.delivered = p.sendto_count;
     }
     std::cout << ae::Format(
-        "RECV FINAL variant={} seq={} last_us={} wifi_ready={} encode={} "
+        "RECV FINAL test_id={} seq={} last_cycle={} wifi_ready={} encode={} "
         "sendto={} nonce={} ts={}\n",
-        temp_sensor::bench::BisectVariantName(p.variant_id), p.sequence_global,
-        p.time_us, p.wifi_ready_count, p.encode_count, p.sendto_count,
-        p.nonce_consumed, ts);
-    PrintSummary();
-    g_done = true;
+        p.test_id, p.sequence_global, p.cycle_us, p.wifi_ready_count,
+        p.encode_count, p.sendto_count, p.nonce_consumed, ts);
+    PrintTestResult();
   }
   std::cout.flush();
 }
 
 void OnMessage(ae::Uid sender, ae::DataBuffer const& data) {
   std::lock_guard lock{g_mu};
-  temp_sensor::bench::BisectPayload bp{};
-  if (temp_sensor::bench::DecodeBisect(data, bp)) {
-    OnBisect(bp);
+  temp_sensor::bench::FastPayload fp{};
+  if (temp_sensor::bench::DecodeFast(data, fp)) {
+    OnFast(fp);
     return;
   }
   std::cout << "RECV unknown sender=" << ae::Format("{}", sender)
@@ -360,7 +283,7 @@ int main() {
             });
       });
 
-  while (!aether_app->IsExited() && !g_done) {
+  while (!aether_app->IsExited()) {
     auto t = aether_app->Update(ae::Now());
     aether_app->WaitUntil(t);
   }

From 2c50b1f7b7d3258c1bf16070b02ada95c9713f64 Mon Sep 17 00:00:00 2001
From: aethernet-io 
Date: Sat, 29 Aug 2026 09:30:48 -0700
Subject: [PATCH 25/32] Add late TX-done callback prepared path and WPA2 430ms
 VAL200 report.

Register esp_wifi_set_tx_done_cb immediately before sendto, wait first
callback (no fingerprint), drop fixed POST=300; PRE=25 shortest screen.

Co-authored-by: Cursor 
---
 .../PREPARED_WIFI_CALLBACK_WPA2_REPORT.md     |  70 ++++
 experiments/callback_wpa2_chat.txt            | 126 +++++++
 experiments/callback_wpa2_state.json          | 218 +++++++++++
 experiments/prepared_wifi_callback_wpa2.tsv   |  10 +
 experiments/run_callback_wpa2.py              | 357 ++++++++++++++++++
 main/bench_payload.h                          |   9 +-
 main/prepared_send/prepared_send.cpp          | 191 +++++++---
 main/prepared_send/prepared_send.h            |   8 +-
 main/prepared_wifi_fastest_path_bench.cpp     |  36 +-
 temperature_receiver/main.cpp                 |  32 +-
 10 files changed, 987 insertions(+), 70 deletions(-)
 create mode 100644 experiments/PREPARED_WIFI_CALLBACK_WPA2_REPORT.md
 create mode 100644 experiments/callback_wpa2_chat.txt
 create mode 100644 experiments/callback_wpa2_state.json
 create mode 100644 experiments/prepared_wifi_callback_wpa2.tsv
 create mode 100644 experiments/run_callback_wpa2.py

diff --git a/experiments/PREPARED_WIFI_CALLBACK_WPA2_REPORT.md b/experiments/PREPARED_WIFI_CALLBACK_WPA2_REPORT.md
new file mode 100644
index 0000000..3b8640e
--- /dev/null
+++ b/experiments/PREPARED_WIFI_CALLBACK_WPA2_REPORT.md
@@ -0,0 +1,70 @@
+# Prepared Wi-Fi late TX-done callback (WPA2) report
+
+Experiment only. Production `SendPreparedOnce` was **not** switched.
+
+## Pins
+
+| Repo | Branch / note | SHA |
+|------|---------------|-----|
+| temperature-sensor | `thermometer-prepared-send-v0` | *(this commit)* |
+| aether-client-cpp | unchanged | `157aadbec8e7b852d0f89274307ff7cb8103e5f7` |
+
+## CONFIG
+
+- Negotiated auth: **WPA2_PSK** (`authmode=3`) via benchmark-only `CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=n`
+- Channel cache: **yes**
+- BSSID cache: **no**
+- Static IPv4 / netmask / gateway: **yes**
+- Cached gateway MAC + static ARP: **yes**
+- Wi-Fi 4 only (`11B|11G|11N`), no 802.11ax
+- PHY: automatic rate adaptation
+- `WIFI_PS_NONE`, max TX power, max normal ESP32-C6 CPU freq
+- Association retry: **10**
+- PRE: **200 ms** for VAL*; best short screen PRE: **25 ms**
+- POST: **TX_DONE_CALLBACK** (no fixed 300 ms)
+
+## CALLBACK
+
+- Registration point: **immediately before `sendto()`**
+- Socket kept open until first callback (or timeout), then unset + close + teardown
+- First TX-done used; **no** fingerprint / payload match
+- Wait: 50 ms primary, extend to 100 ms total max
+- `callback_timeout` counted separately (VAL*: **0**)
+
+## RESULT
+
+| Run | N | delivered | callback_seen | timeouts | connect_med | txdone_med | teardown_med | cycle_med | p90 | max |
+|-----|---|-----------|---------------|----------|-------------|------------|--------------|-----------|-----|-----|
+| VAL20 | 20 | 19/20 | 20/20 | 0 | 127 | 1 | 77 | **420** | 440 | 490 |
+| VAL100 | 100 | 100/100 | 100/100 | 0 | 127 | 2 | 86 | **420** | 490 | 680 |
+| VAL200 | 200 | 189/200 | 200/200 | 0 | 127 | 1 | 97 | **430** | 490 | 780 |
+
+Lifecycle on VAL200: wifi_ready=200, encode=200, sendto=200, nonce=200.
+
+### PRE sweep (screen ×20, POST=callback only)
+
+| PRE | delivered | callback | cycle_med | note |
+|-----|-----------|----------|-----------|------|
+| 150 | 20/20 | 20/20 | 350 | ok |
+| 100 | 20/20 | 20/20 | 320 | ok |
+| 75 | 20/20 | 20/20 | 280 | ok |
+| 50 | 16/20 | 20/20 | 270 | weaker delivery |
+| **25** | **20/20** | **20/20** | **230** | **shortest practical** |
+| 0 | 3/20 | 20/20 | 220 | delivery collapsed — stop |
+
+## COMPARE (VAL200 PRE=200)
+
+| | cycle median | delivery |
+|--|--------------|----------|
+| OLD winner (PRE=200, POST=300, WPA2) | **710 ms** | 198/200 |
+| NEW callback (PRE=200, POST=cb) | **430 ms** | 189/200 |
+
+- Absolute saving: **280 ms**
+- Percent saving: **39.4%**
+- Speedup: **1.65×**
+
+Callback path is the new fastest candidate at PRE=200. Shortest practical PRE from sweep: **25 ms** (screen 20/20 @ 230 ms cycle); not re-validated at N=200 in this campaign.
+
+## Raw data
+
+See `experiments/prepared_wifi_callback_wpa2.tsv`.
diff --git a/experiments/callback_wpa2_chat.txt b/experiments/callback_wpa2_chat.txt
new file mode 100644
index 0000000..46db3c8
--- /dev/null
+++ b/experiments/callback_wpa2_chat.txt
@@ -0,0 +1,126 @@
+[TEST 1/12] CALLBACK_WPA2_VAL20_PRE200
+delivery=19/20
+callback=20/20
+timeouts=0
+connect_med=127
+txdone_med=1
+cycle_med=420
+p90=440
+auth=3 result=PASS
+remaining=11
+BEST NOW:
+none
+NEXT: VAL100 if cb>=18
+
+[TEST 2/12] CALLBACK_WPA2_VAL100_PRE200
+delivery=100/100
+callback=100/100
+timeouts=0
+connect_med=127
+txdone_med=2
+cycle_med=420
+p90=490
+auth=3 result=PASS
+remaining=10
+BEST NOW:
+PRE=200 POST=callback cycle=420ms del=19/20 cb=20
+NEXT: VAL200 if normal
+
+[TEST 3/12] CALLBACK_WPA2_VAL200_PRE200
+delivery=189/200
+callback=200/200
+timeouts=0
+connect_med=127
+txdone_med=1
+cycle_med=430
+p90=490
+auth=3 result=PASS
+remaining=9
+BEST NOW:
+PRE=200 POST=callback cycle=420ms del=100/100 cb=100
+NEXT: PRE sweep if stable
+
+[TEST 4/12] CALLBACK_WPA2_PRE150_S20
+delivery=20/20
+callback=20/20
+timeouts=0
+connect_med=115
+txdone_med=4
+cycle_med=350
+p90=420
+auth=3 result=PASS
+remaining=8
+BEST NOW:
+PRE=200 POST=callback cycle=430ms del=189/200 cb=200
+NEXT: next PRE
+
+[TEST 5/12] CALLBACK_WPA2_PRE100_S20
+delivery=20/20
+callback=20/20
+timeouts=0
+connect_med=128
+txdone_med=0
+cycle_med=320
+p90=350
+auth=3 result=PASS
+remaining=7
+BEST NOW:
+PRE=150 POST=callback cycle=350ms del=20/20 cb=20
+NEXT: next PRE
+
+[TEST 6/12] CALLBACK_WPA2_PRE75_S20
+delivery=20/20
+callback=20/20
+timeouts=0
+connect_med=134
+txdone_med=4
+cycle_med=280
+p90=320
+auth=3 result=PASS
+remaining=6
+BEST NOW:
+PRE=100 POST=callback cycle=320ms del=20/20 cb=20
+NEXT: next PRE
+
+[TEST 7/12] CALLBACK_WPA2_PRE50_S20
+delivery=16/20
+callback=20/20
+timeouts=0
+connect_med=133
+txdone_med=6
+cycle_med=270
+p90=330
+auth=3 result=FAIL
+remaining=5
+BEST NOW:
+PRE=75 POST=callback cycle=280ms del=20/20 cb=20
+NEXT: next PRE
+
+[TEST 8/12] CALLBACK_WPA2_PRE25_S20
+delivery=20/20
+callback=20/20
+timeouts=0
+connect_med=114
+txdone_med=1
+cycle_med=230
+p90=290
+auth=3 result=PASS
+remaining=4
+BEST NOW:
+PRE=50 POST=callback cycle=270ms del=16/20 cb=20
+NEXT: next PRE
+
+[TEST 9/12] CALLBACK_WPA2_PRE0_S20
+delivery=3/20
+callback=20/20
+timeouts=0
+connect_med=126
+txdone_med=0
+cycle_med=220
+p90=230
+auth=3 result=FAIL
+remaining=3
+BEST NOW:
+PRE=25 POST=callback cycle=230ms del=20/20 cb=20
+NEXT: next PRE
+
diff --git a/experiments/callback_wpa2_state.json b/experiments/callback_wpa2_state.json
new file mode 100644
index 0000000..57eef51
--- /dev/null
+++ b/experiments/callback_wpa2_state.json
@@ -0,0 +1,218 @@
+{
+  "val200": {
+    "id": 103,
+    "n": 200,
+    "del": 189,
+    "plan": 200,
+    "conn": 127,
+    "cyc": 430,
+    "p90": 490,
+    "mx": 780,
+    "wr": 200,
+    "enc": 200,
+    "st": 200,
+    "nonce": 200,
+    "pre": 200,
+    "post": 0,
+    "assoc": 154,
+    "auth": 3,
+    "retry": 10,
+    "pm": 1,
+    "cba": 200,
+    "cbm": 0,
+    "cbt": 0,
+    "txd": 1,
+    "td": 97,
+    "samp": 189
+  },
+  "best": {
+    "name": "PRE25_S20",
+    "pre": 25,
+    "cyc": 230,
+    "conn": 114,
+    "del": 20,
+    "plan": 20,
+    "cba": 20,
+    "cbt": 0,
+    "txd": 1,
+    "p90": 290,
+    "mx": 640
+  },
+  "pre_sweep": [
+    [
+      150,
+      {
+        "id": 140,
+        "n": 20,
+        "del": 20,
+        "plan": 20,
+        "conn": 115,
+        "cyc": 350,
+        "p90": 420,
+        "mx": 500,
+        "wr": 20,
+        "enc": 20,
+        "st": 20,
+        "nonce": 20,
+        "pre": 150,
+        "post": 0,
+        "assoc": 154,
+        "auth": 3,
+        "retry": 10,
+        "pm": 1,
+        "cba": 20,
+        "cbm": 0,
+        "cbt": 0,
+        "txd": 4,
+        "td": 76,
+        "samp": 20
+      }
+    ],
+    [
+      100,
+      {
+        "id": 130,
+        "n": 20,
+        "del": 20,
+        "plan": 20,
+        "conn": 128,
+        "cyc": 320,
+        "p90": 350,
+        "mx": 390,
+        "wr": 20,
+        "enc": 20,
+        "st": 20,
+        "nonce": 20,
+        "pre": 100,
+        "post": 0,
+        "assoc": 154,
+        "auth": 3,
+        "retry": 10,
+        "pm": 1,
+        "cba": 20,
+        "cbm": 0,
+        "cbt": 0,
+        "txd": 0,
+        "td": 85,
+        "samp": 20
+      }
+    ],
+    [
+      75,
+      {
+        "id": 125,
+        "n": 20,
+        "del": 20,
+        "plan": 20,
+        "conn": 134,
+        "cyc": 280,
+        "p90": 320,
+        "mx": 430,
+        "wr": 20,
+        "enc": 20,
+        "st": 20,
+        "nonce": 20,
+        "pre": 75,
+        "post": 0,
+        "assoc": 154,
+        "auth": 3,
+        "retry": 10,
+        "pm": 1,
+        "cba": 20,
+        "cbm": 0,
+        "cbt": 0,
+        "txd": 4,
+        "td": 76,
+        "samp": 20
+      }
+    ],
+    [
+      50,
+      {
+        "id": 120,
+        "n": 20,
+        "del": 16,
+        "plan": 20,
+        "conn": 133,
+        "cyc": 270,
+        "p90": 330,
+        "mx": 390,
+        "wr": 20,
+        "enc": 20,
+        "st": 20,
+        "nonce": 20,
+        "pre": 50,
+        "post": 0,
+        "assoc": 154,
+        "auth": 3,
+        "retry": 10,
+        "pm": 1,
+        "cba": 20,
+        "cbm": 0,
+        "cbt": 0,
+        "txd": 6,
+        "td": 71,
+        "samp": 16
+      }
+    ],
+    [
+      25,
+      {
+        "id": 115,
+        "n": 20,
+        "del": 20,
+        "plan": 20,
+        "conn": 114,
+        "cyc": 230,
+        "p90": 290,
+        "mx": 640,
+        "wr": 20,
+        "enc": 20,
+        "st": 20,
+        "nonce": 20,
+        "pre": 25,
+        "post": 0,
+        "assoc": 154,
+        "auth": 3,
+        "retry": 10,
+        "pm": 1,
+        "cba": 20,
+        "cbm": 0,
+        "cbt": 0,
+        "txd": 1,
+        "td": 92,
+        "samp": 20
+      }
+    ],
+    [
+      0,
+      {
+        "id": 110,
+        "n": 20,
+        "del": 3,
+        "plan": 20,
+        "conn": 126,
+        "cyc": 220,
+        "p90": 230,
+        "mx": 240,
+        "wr": 20,
+        "enc": 20,
+        "st": 20,
+        "nonce": 20,
+        "pre": 0,
+        "post": 0,
+        "assoc": 154,
+        "auth": 3,
+        "retry": 10,
+        "pm": 1,
+        "cba": 20,
+        "cbm": 0,
+        "cbt": 0,
+        "txd": 0,
+        "td": 59,
+        "samp": 4
+      }
+    ]
+  ],
+  "old_winner": 710
+}
\ No newline at end of file
diff --git a/experiments/prepared_wifi_callback_wpa2.tsv b/experiments/prepared_wifi_callback_wpa2.tsv
new file mode 100644
index 0000000..dcf4545
--- /dev/null
+++ b/experiments/prepared_wifi_callback_wpa2.tsv
@@ -0,0 +1,10 @@
+name	test_id	n	delivered	plan	connect_med	txdone_med	teardown_med	cycle_med	p90	max	wifi_ready	encode	sendto	nonce	pre	post_mode	auth	cb_seen	cb_timeout	samples
+VAL20_PRE200	101	20	19	20	127	1	77	420	440	490	20	20	20	20	200	1	3	20	0	19
+VAL100_PRE200	102	100	100	100	127	2	86	420	490	680	100	100	100	100	200	1	3	100	0	100
+VAL200_PRE200	103	200	189	200	127	1	97	430	490	780	200	200	200	200	200	1	3	200	0	189
+PRE150_S20	140	20	20	20	115	4	76	350	420	500	20	20	20	20	150	1	3	20	0	20
+PRE100_S20	130	20	20	20	128	0	85	320	350	390	20	20	20	20	100	1	3	20	0	20
+PRE75_S20	125	20	20	20	134	4	76	280	320	430	20	20	20	20	75	1	3	20	0	20
+PRE50_S20	120	20	16	20	133	6	71	270	330	390	20	20	20	20	50	1	3	20	0	16
+PRE25_S20	115	20	20	20	114	1	92	230	290	640	20	20	20	20	25	1	3	20	0	20
+PRE0_S20	110	20	3	20	126	0	59	220	230	240	20	20	20	20	0	1	3	20	0	4
diff --git a/experiments/run_callback_wpa2.py b/experiments/run_callback_wpa2.py
new file mode 100644
index 0000000..9284022
--- /dev/null
+++ b/experiments/run_callback_wpa2.py
@@ -0,0 +1,357 @@
+"""Late TX-done callback prepared-send campaign (WPA2, PRE=200 then PRE sweep)."""
+
+from __future__ import annotations
+
+import json
+import re
+import sys
+import time
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from run_fastest_path import (  # noqa: E402
+    BUILD,
+    RESULT_RE,
+    ROOT,
+    RX_LOG,
+    cmake_configure,
+    ensure_receiver,
+    env,
+    flash,
+    log,
+    ninja_build,
+    wait_result,
+)
+
+RESULTS = ROOT / "experiments" / "prepared_wifi_callback_wpa2.tsv"
+CHAT = ROOT / "experiments" / "callback_wpa2_chat.txt"
+STATE = ROOT / "experiments" / "callback_wpa2_state.json"
+PROGRESS = ROOT / "experiments" / "callback_wpa2_progress.log"
+
+# Extended regex with new fields (falls back handled by groupdict defaults).
+RESULT_RE_EXT = re.compile(
+    r"TEST_RESULT test_id=(?P\d+) n=(?P\d+) delivered=(?P\d+)/(?P\d+) "
+    r"connect_med_ms=(?P\d+) cycle_med_ms=(?P\d+) p90_ms=(?P\d+) "
+    r"max_ms=(?P\d+) wifi_ready=(?P\d+) encode=(?P\d+) sendto=(?P\d+) "
+    r"nonce=(?P\d+) pre=(?P
\d+) post=(?P\d+) assoc=0x(?P[0-9a-fA-F]+) "
+    r"auth=(?P\d+) retry=(?P\d+) post_mode=(?P\d+) cb_any=(?P\d+) "
+    r"cb_match=(?P\d+)(?: cb_timeout=(?P\d+))?(?: txdone_med_ms=(?P\d+))?"
+    r"(?: teardown_med_ms=(?P\d+))? samples=(?P\d+)"
+)
+
+BASE = {
+    "AE_EXP_FAST_USE_BSSID": "0",
+    "AE_EXP_FAST_FAST_SCAN": "0",
+    "AE_EXP_FAST_AUTH": "2",
+    "AE_EXP_FAST_RETRY": "10",
+    "AE_EXP_FAST_POST_MODE": "1",  # kTxDoneCb — late install before sendto
+    "AE_EXP_FAST_POST_MS": "0",
+    "AE_EXP_FAST_AMPDU_TX_OFF": "0",
+    "AE_EXP_FAST_STORAGE_RAM": "0",
+    "AE_EXP_FAST_DISABLE_WPA3": "1",
+}
+
+OLD_WINNER_CYCLE = 710
+
+
+def force_wpa3_off() -> None:
+    sdk = BUILD / "sdkconfig"
+    if not sdk.exists():
+        return
+    text = sdk.read_text(encoding="utf-8")
+    text = text.replace(
+        "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y",
+        "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set",
+    )
+    text = text.replace(
+        "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y",
+        "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set",
+    )
+    sdk.write_text(text, encoding="utf-8")
+
+
+def count_results() -> int:
+    if not RX_LOG.exists():
+        return 0
+    text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+    return len(list(RESULT_RE_EXT.finditer(text)))
+
+
+def wait_result_ext(
+    prev: int, timeout_s: int, expect_id: int | None, expect_n: int | None
+) -> dict:
+    deadline = time.time() + timeout_s
+    last_hb = 0.0
+    while time.time() < deadline:
+        now = time.time()
+        if now - last_hb >= 30:
+            last_hb = now
+            log(f"waiting TEST_RESULT prev={prev} left={int(deadline-now)}s")
+        if RX_LOG.exists():
+            text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+            matches = list(RESULT_RE_EXT.finditer(text))
+            if len(matches) > prev:
+                for m in reversed(matches[prev:]):
+                    gd = m.groupdict()
+                    d = {}
+                    for k, v in gd.items():
+                        if v is None:
+                            d[k] = 0
+                        elif k == "assoc":
+                            d[k] = int(v, 16)
+                        else:
+                            d[k] = int(v)
+                    if expect_id is not None and d["id"] != expect_id:
+                        continue
+                    if expect_n is not None and d["n"] != expect_n:
+                        continue
+                    return d
+        time.sleep(2)
+    raise TimeoutError("no TEST_RESULT")
+
+
+def write_chat(test_no: int, remaining: int, name: str, r: dict, best: dict | None, nxt: str) -> None:
+    auth_ok = r.get("auth", 0) == 3
+    cb = r.get("cba", 0)
+    to = r.get("cbt", 0)
+    plan = r.get("plan", 0)
+    result = "PASS"
+    if not auth_ok:
+        result = "INVALID_AUTH"
+    elif plan >= 20 and cb < max(18, int(plan * 0.9)):
+        result = "CALLBACK_WEAK"
+    elif r["del"] < int(plan * 0.85):
+        result = "FAIL"
+    best_s = "none"
+    if best:
+        best_s = (
+            f"PRE={best.get('pre')} POST=callback cycle={best.get('cyc')}ms "
+            f"del={best.get('del')}/{best.get('plan')} cb={best.get('cba')}"
+        )
+    lines = [
+        f"[TEST {test_no}/{test_no + remaining}] CALLBACK_WPA2_{name}",
+        f"delivery={r['del']}/{r['plan']}",
+        f"callback={cb}/{r.get('wr', plan)}",
+        f"timeouts={to}",
+        f"connect_med={r['conn']}",
+        f"txdone_med={r.get('txd', 0)}",
+        f"cycle_med={r['cyc']}",
+        f"p90={r['p90']}",
+        f"auth={r.get('auth')} result={result}",
+        f"remaining={remaining}",
+        "BEST NOW:",
+        best_s,
+        f"NEXT: {nxt}",
+        "",
+    ]
+    text = "\n".join(lines)
+    CHAT.parent.mkdir(parents=True, exist_ok=True)
+    with CHAT.open("a", encoding="utf-8") as f:
+        f.write(text + "\n")
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(text + "\n")
+    print(text, flush=True)
+
+
+def append_tsv(name: str, r: dict) -> None:
+    header = (
+        "name\ttest_id\tn\tdelivered\tplan\tconnect_med\ttxdone_med\tteardown_med\t"
+        "cycle_med\tp90\tmax\twifi_ready\tencode\tsendto\tnonce\tpre\tpost_mode\t"
+        "auth\tcb_seen\tcb_timeout\tsamples\n"
+    )
+    if not RESULTS.exists():
+        RESULTS.write_text(header, encoding="utf-8")
+    line = (
+        f"{name}\t{r['id']}\t{r['n']}\t{r['del']}\t{r['plan']}\t{r['conn']}\t"
+        f"{r.get('txd',0)}\t{r.get('td',0)}\t{r['cyc']}\t{r['p90']}\t{r['mx']}\t"
+        f"{r['wr']}\t{r['enc']}\t{r['st']}\t{r['nonce']}\t{r['pre']}\t{r['pm']}\t"
+        f"{r['auth']}\t{r.get('cba',0)}\t{r.get('cbt',0)}\t{r['samp']}\n"
+    )
+    with RESULTS.open("a", encoding="utf-8") as f:
+        f.write(line)
+
+
+def run_one(name: str, test_id: int, n: int, pre_ms: int, timeout_s: int) -> dict:
+    flags = {
+        **BASE,
+        "AE_EXP_FAST_TEST_ID": str(test_id),
+        "AE_EXP_FAST_N": str(n),
+        "AE_EXP_FAST_PRE_MS": str(pre_ms),
+    }
+    ensure_receiver()
+    cmake_configure(flags)
+    force_wpa3_off()
+    ninja_build()
+    # Re-apply after ninja may regenerate sdkconfig from defaults
+    force_wpa3_off()
+    prev = count_results()
+    flash()
+    r = wait_result_ext(prev, timeout_s, expect_id=test_id, expect_n=n)
+    r["pre"] = pre_ms
+    append_tsv(name, r)
+    return r
+
+
+def main() -> int:
+    RESULTS.write_text(
+        "name\ttest_id\tn\tdelivered\tplan\tconnect_med\ttxdone_med\tteardown_med\t"
+        "cycle_med\tp90\tmax\twifi_ready\tencode\tsendto\tnonce\tpre\tpost_mode\t"
+        "auth\tcb_seen\tcb_timeout\tsamples\n",
+        encoding="utf-8",
+    )
+    CHAT.write_text("", encoding="utf-8")
+    best: dict | None = None
+    test_no = 1
+    remaining = 12
+
+    # --- VAL20 screen ---
+    name = "VAL20_PRE200"
+    log(f"=== {name} ===")
+    r = run_one(name, test_id=101, n=20, pre_ms=200, timeout_s=900)
+    write_chat(test_no, remaining - 1, name, r, best, "VAL100 if cb>=18")
+    test_no += 1
+    remaining -= 1
+
+    if r.get("auth", 0) != 3:
+        log("INVALID: authmode != WPA2_PSK(3)")
+        STATE.write_text(json.dumps({"stop": "invalid_auth", "r": r}, indent=2), encoding="utf-8")
+        return 2
+    if r["wr"] < 20 or r["enc"] < 20 or r["st"] < 20 or r["nonce"] < 20:
+        log(f"SCREEN FAIL lifecycle wr={r['wr']} enc={r['enc']} st={r['st']} nonce={r['nonce']}")
+        STATE.write_text(json.dumps({"stop": "lifecycle", "r": r}, indent=2), encoding="utf-8")
+        return 3
+    if r.get("cba", 0) < 18:
+        log(f"SCREEN STOP callback_seen={r.get('cba')}/20")
+        STATE.write_text(json.dumps({"stop": "callback_weak", "r": r}, indent=2), encoding="utf-8")
+        return 4
+
+    best = {
+        "name": name,
+        "pre": 200,
+        "cyc": r["cyc"],
+        "conn": r["conn"],
+        "del": r["del"],
+        "plan": r["plan"],
+        "cba": r.get("cba", 0),
+        "cbt": r.get("cbt", 0),
+        "txd": r.get("txd", 0),
+        "p90": r["p90"],
+        "mx": r["mx"],
+    }
+
+    if r["del"] < 18:
+        log("delivery <18/20 — continue only if callback strong; stopping per screen gate soft")
+        # User: if delivery >=18 and callback almost always → continue
+        STATE.write_text(json.dumps({"stop": "delivery_screen", "r": r}, indent=2), encoding="utf-8")
+        return 5
+
+    # --- VAL100 ---
+    name = "VAL100_PRE200"
+    log(f"=== {name} ===")
+    r = run_one(name, test_id=102, n=100, pre_ms=200, timeout_s=3600)
+    write_chat(test_no, remaining - 1, name, r, best, "VAL200 if normal")
+    test_no += 1
+    remaining -= 1
+    if r.get("auth", 0) != 3:
+        log("INVALID auth on VAL100")
+        return 2
+    best = {
+        "name": name,
+        "pre": 200,
+        "cyc": r["cyc"],
+        "conn": r["conn"],
+        "del": r["del"],
+        "plan": r["plan"],
+        "cba": r.get("cba", 0),
+        "cbt": r.get("cbt", 0),
+        "txd": r.get("txd", 0),
+        "p90": r["p90"],
+        "mx": r["mx"],
+    }
+    STATE.write_text(json.dumps({"val100": r, "best": best}, indent=2), encoding="utf-8")
+
+    # Gate: callback mostly works
+    if r.get("cba", 0) < 90:
+        log("VAL100 callback weak — stop before VAL200")
+        return 6
+
+    # --- VAL200 ---
+    name = "VAL200_PRE200"
+    log(f"=== {name} ===")
+    r = run_one(name, test_id=103, n=200, pre_ms=200, timeout_s=7200)
+    write_chat(test_no, remaining - 1, name, r, best, "PRE sweep if stable")
+    test_no += 1
+    remaining -= 1
+    val200 = dict(r)
+    best = {
+        "name": name,
+        "pre": 200,
+        "cyc": r["cyc"],
+        "conn": r["conn"],
+        "del": r["del"],
+        "plan": r["plan"],
+        "cba": r.get("cba", 0),
+        "cbt": r.get("cbt", 0),
+        "txd": r.get("txd", 0),
+        "p90": r["p90"],
+        "mx": r["mx"],
+    }
+    saving = OLD_WINNER_CYCLE - r["cyc"]
+    log(
+        f"COMPARE old={OLD_WINNER_CYCLE} new={r['cyc']} saving={saving}ms "
+        f"({100.0 * saving / OLD_WINNER_CYCLE:.1f}%)"
+    )
+
+    # --- PRE sweep (screen 20) if callback stable ---
+    pre_sweep_results = []
+    if r.get("cba", 0) >= 180 and r["cyc"] < OLD_WINNER_CYCLE:
+        for pre in (150, 100, 75, 50, 25, 0):
+            name = f"PRE{pre}_S20"
+            log(f"=== {name} ===")
+            rr = run_one(name, test_id=110 + pre // 5, n=20, pre_ms=pre, timeout_s=900)
+            write_chat(test_no, max(0, remaining - 1), name, rr, best, f"next PRE")
+            test_no += 1
+            remaining = max(0, remaining - 1)
+            pre_sweep_results.append((pre, rr))
+            cb_ok = rr.get("cba", 0) >= 18
+            del_ok = rr["del"] >= 15  # UDP losses ok; not sharply worse
+            if not cb_ok:
+                log(f"PRE={pre} callback weak — stop sweep")
+                break
+            if cb_ok and del_ok and rr["cyc"] <= best["cyc"]:
+                best = {
+                    "name": name,
+                    "pre": pre,
+                    "cyc": rr["cyc"],
+                    "conn": rr["conn"],
+                    "del": rr["del"],
+                    "plan": rr["plan"],
+                    "cba": rr.get("cba", 0),
+                    "cbt": rr.get("cbt", 0),
+                    "txd": rr.get("txd", 0),
+                    "p90": rr["p90"],
+                    "mx": rr["mx"],
+                }
+            # If delivery collapses sharply vs VAL20 baseline 18+, stop
+            if rr["del"] < 12:
+                log(f"PRE={pre} delivery collapsed — stop sweep")
+                break
+
+    STATE.write_text(
+        json.dumps(
+            {
+                "val200": val200,
+                "best": best,
+                "pre_sweep": [(p, x) for p, x in pre_sweep_results],
+                "old_winner": OLD_WINNER_CYCLE,
+            },
+            indent=2,
+        ),
+        encoding="utf-8",
+    )
+    log(f"DONE best={best}")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/main/bench_payload.h b/main/bench_payload.h
index 4045d7f..2c1b28d 100644
--- a/main/bench_payload.h
+++ b/main/bench_payload.h
@@ -154,12 +154,19 @@ struct FastPayload {
   std::uint8_t cb_match{0};
   std::uint8_t cb_count{0};
   std::uint8_t post_mode{0};
+  std::uint32_t encode_send_us{0};
+  std::uint32_t tx_done_wait_us{0};
+  std::uint32_t teardown_us{0};
+  std::uint8_t cb_timeout{0};
+  std::uint8_t reserved0{0};
+  std::uint8_t reserved1{0};
+  std::uint8_t reserved2{0};
 };
 #pragma pack(pop)
 
 static_assert(sizeof(Payload) == 19, "bench payload size");
 static_assert(sizeof(BisectPayload) == 34, "bisect payload size");
-static_assert(sizeof(FastPayload) == 34, "fast payload size");
+static_assert(sizeof(FastPayload) == 50, "fast payload size");
 
 inline char const* BisectVariantName(std::uint8_t id) {
   switch (static_cast(id)) {
diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp
index 7b5ea4a..134f82f 100644
--- a/main/prepared_send/prepared_send.cpp
+++ b/main/prepared_send/prepared_send.cpp
@@ -621,34 +621,17 @@ HotSendStatus EncodeAndUdpSend(ae::DataBuffer const& payload) {
 }
 
 #if defined(ESP_PLATFORM)
-std::uint8_t g_fast_fp[16]{};
-std::uint16_t g_fast_fp_len = 0;
-std::atomic g_fast_cb_any{0};
-std::atomic g_fast_cb_match{0};
+// Late TX-done callback state: first completion after sendto, no fingerprint.
+std::atomic g_fast_tx_done_seen{false};
 std::atomic g_fast_cb_count{0};
 
-void FastTxDoneCb(std::uint8_t, std::uint8_t* data, std::uint16_t* data_len,
-                  bool) {
+void FastTxDoneCb(std::uint8_t, std::uint8_t*, std::uint16_t*, bool) {
   g_fast_cb_count.fetch_add(1, std::memory_order_relaxed);
-  g_fast_cb_any.store(1, std::memory_order_relaxed);
-  if (data == nullptr || data_len == nullptr || g_fast_fp_len == 0) {
-    return;
-  }
-  std::uint16_t const n = *data_len;
-  if (n < g_fast_fp_len) {
-    return;
-  }
-  for (std::uint16_t i = 0; i + g_fast_fp_len <= n; ++i) {
-    if (std::memcmp(data + i, g_fast_fp, g_fast_fp_len) == 0) {
-      g_fast_cb_match.store(1, std::memory_order_relaxed);
-      return;
-    }
-  }
+  g_fast_tx_done_seen.store(true, std::memory_order_release);
 }
 
 void ResetFastTxDone() {
-  g_fast_cb_any.store(0, std::memory_order_relaxed);
-  g_fast_cb_match.store(0, std::memory_order_relaxed);
+  g_fast_tx_done_seen.store(false, std::memory_order_release);
   g_fast_cb_count.store(0, std::memory_order_relaxed);
 }
 
@@ -668,11 +651,53 @@ HotSendStatus EncodeAndUdpSendTracked(ae::DataBuffer const& payload) {
     return HotSendStatus::kEncodeFailed;
   }
 
-  g_fast_fp_len = static_cast(
-      packet.size() < sizeof(g_fast_fp) ? packet.size() : sizeof(g_fast_fp));
-  if (g_fast_fp_len > 0) {
-    std::memcpy(g_fast_fp, packet.data() + (packet.size() - g_fast_fp_len),
-                g_fast_fp_len);
+  auto const resolved_block = g_prepared_send_message_block.Resolve();
+  auto endpoint = resolved_block->endpoint;
+
+  sockaddr_storage dest_storage{};
+  socklen_t dest_len = 0;
+  if (!FillUdpDestination(endpoint, reinterpret_cast(&dest_storage),
+                          &dest_len)) {
+    return HotSendStatus::kSendFailed;
+  }
+
+  int sock = socket(
+      endpoint.address.Index() == ae::AddrVersion::kIpV6 ? AF_INET6 : AF_INET,
+      SOCK_DGRAM, IPPROTO_IP);
+  if (sock < 0) {
+    return HotSendStatus::kSendFailed;
+  }
+
+  auto sent = sendto(sock, packet.data(), packet.size(), 0,
+                     reinterpret_cast(&dest_storage), dest_len);
+  close(sock);
+
+  if (sent != static_cast(packet.size())) {
+    return HotSendStatus::kSendFailed;
+  }
+  return HotSendStatus::kSent;
+}
+
+// Encode → socket → set_tx_done_cb → sendto → wait first cb → unset → close.
+// Socket stays open until callback (or timeout). No Wi-Fi ops between set and
+// sendto.
+HotSendStatus EncodeAndUdpSendWithLateTxDone(ae::DataBuffer const& payload,
+                                             FastSendResult* timing) {
+  if (!g_prepared_send_message_block.is_valid()) {
+    return HotSendStatus::kNoPreparedBlock;
+  }
+  if (g_prepared_send_message_block.Resolve()->message_left == 0) {
+    return HotSendStatus::kNonceExhausted;
+  }
+
+  auto const t_encode0 = esp_timer_get_time();
+
+  ae::DataBuffer packet;
+  auto encode_result = ae::prepared_packet::EncodePacket(
+      g_prepared_send_message_block, payload, packet);
+  if (!encode_result) {
+    ClearPreparedSendBlock();
+    return HotSendStatus::kEncodeFailed;
   }
 
   auto const resolved_block = g_prepared_send_message_block.Resolve();
@@ -693,13 +718,58 @@ HotSendStatus EncodeAndUdpSendTracked(ae::DataBuffer const& payload) {
   }
 
   ResetFastTxDone();
+  (void)esp_wifi_set_tx_done_cb(&FastTxDoneCb);
+
   auto sent = sendto(sock, packet.data(), packet.size(), 0,
                      reinterpret_cast(&dest_storage), dest_len);
-  close(sock);
+  auto const t_send_ret = esp_timer_get_time();
+  if (timing != nullptr) {
+    auto const es = t_send_ret - t_encode0;
+    timing->encode_send_us =
+        es < 0 ? 0 : static_cast(es);
+  }
 
   if (sent != static_cast(packet.size())) {
+    (void)esp_wifi_set_tx_done_cb(nullptr);
+    close(sock);
     return HotSendStatus::kSendFailed;
   }
+
+  // Primary wait 50 ms; extend to 100 ms total if needed.
+  constexpr std::int64_t kPrimaryUs = 50000;
+  constexpr std::int64_t kMaxUs = 100000;
+  bool seen = false;
+  while ((esp_timer_get_time() - t_send_ret) < kPrimaryUs) {
+    if (g_fast_tx_done_seen.load(std::memory_order_acquire)) {
+      seen = true;
+      break;
+    }
+    vTaskDelay(pdMS_TO_TICKS(1));
+  }
+  if (!seen) {
+    while ((esp_timer_get_time() - t_send_ret) < kMaxUs) {
+      if (g_fast_tx_done_seen.load(std::memory_order_acquire)) {
+        seen = true;
+        break;
+      }
+      vTaskDelay(pdMS_TO_TICKS(1));
+    }
+  }
+
+  auto const t_cb_done = esp_timer_get_time();
+  (void)esp_wifi_set_tx_done_cb(nullptr);
+  close(sock);
+
+  if (timing != nullptr) {
+    auto const wait = t_cb_done - t_send_ret;
+    timing->tx_done_wait_us =
+        wait < 0 ? 0 : static_cast(wait);
+    timing->cb_any = seen ? 1 : 0;
+    timing->cb_timeout = seen ? 0 : 1;
+    auto const cb_n = g_fast_cb_count.load(std::memory_order_relaxed);
+    timing->cb_count = cb_n > 255 ? 255 : static_cast(cb_n);
+    timing->cb_match = 0;
+  }
   return HotSendStatus::kSent;
 }
 #endif
@@ -1352,10 +1422,8 @@ bool StartFastWifi(FastPathConfig const& cfg) {
 
   (void)esp_wifi_set_max_tx_power(80);
   (void)esp_wifi_set_ps(WIFI_PS_NONE);
-
-  if (cfg.post_mode != FastPostMode::kFixedDelay) {
-    (void)esp_wifi_set_tx_done_cb(&FastTxDoneCb);
-  }
+  // TX-done callback is installed immediately before sendto() for callback
+  // post modes — never here (association / PRE would fire unrelated TX).
 
   EventBits_t bits = xEventGroupWaitBits(
       g_wifi_event_group, kWifiReadyBit | kWifiFailBit, pdFALSE, pdFALSE,
@@ -1532,29 +1600,10 @@ FastSendResult SendPreparedOnceWithFastPath(FastPathConfig const& cfg,
     vTaskDelay(pdMS_TO_TICKS(cfg.pre_delay_ms));
   }
 
-  auto const encode_status = EncodeAndUdpSendTracked(payload);
-  if (encode_status == HotSendStatus::kSent) {
-    out.status_flags |=
-        static_cast(bench::BisectStatusBits::kEncodeOk) |
-        static_cast(bench::BisectStatusBits::kSendtoOk);
-  } else if (encode_status == HotSendStatus::kSendFailed) {
-    out.status_flags |=
-        static_cast(bench::BisectStatusBits::kEncodeOk);
-  }
-
-  if (encode_status == HotSendStatus::kSent &&
-      cfg.post_mode != FastPostMode::kFixedDelay) {
-    auto const t_cb = esp_timer_get_time();
-    while ((esp_timer_get_time() - t_cb) < 100000) {
-      if (g_fast_cb_match.load(std::memory_order_relaxed) != 0) {
-        break;
-      }
-      vTaskDelay(pdMS_TO_TICKS(1));
-    }
-    out.cb_any = g_fast_cb_any.load(std::memory_order_relaxed) != 0 ? 1 : 0;
-    out.cb_match = g_fast_cb_match.load(std::memory_order_relaxed) != 0 ? 1 : 0;
-    auto const cb_n = g_fast_cb_count.load(std::memory_order_relaxed);
-    out.cb_count = cb_n > 255 ? 255 : static_cast(cb_n);
+  HotSendStatus encode_status = HotSendStatus::kWifiFailed;
+  auto const t_post0 = esp_timer_get_time();
+  if (cfg.post_mode != FastPostMode::kFixedDelay) {
+    encode_status = EncodeAndUdpSendWithLateTxDone(payload, &out);
     std::uint16_t extra_ms = 0;
     if (cfg.post_mode == FastPostMode::kTxDoneCbPlus10) {
       extra_ms = 10;
@@ -1564,14 +1613,38 @@ FastSendResult SendPreparedOnceWithFastPath(FastPathConfig const& cfg,
     if (extra_ms > 0) {
       vTaskDelay(pdMS_TO_TICKS(extra_ms));
     }
-    (void)esp_wifi_set_tx_done_cb(nullptr);
-  } else if (encode_status == HotSendStatus::kSent && cfg.post_delay_ms > 0) {
-    vTaskDelay(pdMS_TO_TICKS(cfg.post_delay_ms));
+  } else {
+    encode_status = EncodeAndUdpSendTracked(payload);
+    auto const t_send_done = esp_timer_get_time();
+    {
+      auto const es = t_send_done - t_post0;
+      out.encode_send_us = es < 0 ? 0 : static_cast(es);
+    }
+    if (encode_status == HotSendStatus::kSent && cfg.post_delay_ms > 0) {
+      vTaskDelay(pdMS_TO_TICKS(cfg.post_delay_ms));
+    }
   }
 
+  if (encode_status == HotSendStatus::kSent) {
+    out.status_flags |=
+        static_cast(bench::BisectStatusBits::kEncodeOk) |
+        static_cast(bench::BisectStatusBits::kSendtoOk);
+  } else if (encode_status == HotSendStatus::kSendFailed) {
+    out.status_flags |=
+        static_cast(bench::BisectStatusBits::kEncodeOk);
+  } else if (encode_status == HotSendStatus::kEncodeFailed) {
+    // encode failed: no sendto bit
+  }
+
+  auto const t_teardown0 = esp_timer_get_time();
   CleanupHotPathWifiRuntime();
+  auto const t_end = esp_timer_get_time();
+  {
+    auto const td = t_end - t_teardown0;
+    out.teardown_us = td < 0 ? 0 : static_cast(td);
+  }
 
-  auto const elapsed = esp_timer_get_time() - t0;
+  auto const elapsed = t_end - t0;
   out.cycle_us = elapsed < 0 ? 0 : static_cast(elapsed);
   out.status = encode_status;
   return out;
diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h
index ee5920d..dd611a0 100644
--- a/main/prepared_send/prepared_send.h
+++ b/main/prepared_send/prepared_send.h
@@ -147,12 +147,16 @@ struct FastSendResult {
   HotSendStatus status{HotSendStatus::kWifiFailed};
   std::uint32_t cycle_us{0};
   std::uint32_t connect_us{0};
+  std::uint32_t encode_send_us{0};
+  std::uint32_t tx_done_wait_us{0};
+  std::uint32_t teardown_us{0};
   std::uint8_t requested_channel{0};
   std::uint8_t actual_channel{0};
   std::uint8_t negotiated_auth{0};
   std::uint8_t status_flags{0};
-  std::uint8_t cb_any{0};
-  std::uint8_t cb_match{0};
+  std::uint8_t cb_any{0};       // first tx-done callback seen
+  std::uint8_t cb_match{0};     // legacy fingerprint match (unused)
+  std::uint8_t cb_timeout{0};   // 1 if no callback within window
   std::uint8_t cb_count{0};
 };
 
diff --git a/main/prepared_wifi_fastest_path_bench.cpp b/main/prepared_wifi_fastest_path_bench.cpp
index 83871a2..21fed3d 100644
--- a/main/prepared_wifi_fastest_path_bench.cpp
+++ b/main/prepared_wifi_fastest_path_bench.cpp
@@ -150,6 +150,9 @@ static std::uint16_t g_wifi_ready_count = 0;
 static std::uint16_t g_encode_count = 0;
 static std::uint16_t g_sendto_count = 0;
 static std::uint16_t g_nonce_start = 0;
+static std::uint16_t g_cb_seen_count = 0;
+static std::uint16_t g_cb_timeout_count = 0;
+static std::uint16_t g_auth_ok_count = 0;
 
 static prepared_send::FastSendResult g_last_result{};
 static prepared_send::BisectWifiCacheSnapshot g_cache{};
@@ -219,7 +222,10 @@ static std::uint8_t AssocBitsOf(prepared_send::FastPathConfig const& c) {
 static void FillCommon(bench::FastPayload& p) {
   p.test_id = kTestId;
   p.pre_ms = g_cfg.pre_delay_ms;
-  p.post_ms = g_cfg.post_delay_ms;
+  // post_ms=0 means POST=callback for late tx-done mode.
+  p.post_ms = (g_cfg.post_mode == prepared_send::FastPostMode::kFixedDelay)
+                  ? g_cfg.post_delay_ms
+                  : 0;
   p.assoc_bits = g_assoc_bits;
   p.retry_max = g_cfg.retry_max;
   p.post_mode = static_cast(g_cfg.post_mode);
@@ -247,11 +253,15 @@ static ae::DataBuffer MakePreparedPayload(int index) {
   if (index > 1) {
     p.cycle_us = g_last_result.cycle_us;
     p.connect_us = g_last_result.connect_us;
+    p.encode_send_us = g_last_result.encode_send_us;
+    p.tx_done_wait_us = g_last_result.tx_done_wait_us;
+    p.teardown_us = g_last_result.teardown_us;
     p.status_flags = g_last_result.status_flags;
     p.auth_negotiated = g_last_result.negotiated_auth;
     p.cb_any = g_last_result.cb_any;
     p.cb_match = g_last_result.cb_match;
     p.cb_count = g_last_result.cb_count;
+    p.cb_timeout = g_last_result.cb_timeout;
   }
   return bench::EncodeFast(p);
 }
@@ -265,6 +275,9 @@ static ae::DataBuffer MakeFinalPayload() {
   p.sequence_global = NextSeq();
   p.cycle_us = g_last_result.cycle_us;
   p.connect_us = g_last_result.connect_us;
+  p.encode_send_us = g_last_result.encode_send_us;
+  p.tx_done_wait_us = g_last_result.tx_done_wait_us;
+  p.teardown_us = g_last_result.teardown_us;
   p.status_flags = g_last_result.status_flags;
   p.auth_negotiated = g_last_result.negotiated_auth;
   p.wifi_ready_count = g_wifi_ready_count;
@@ -279,9 +292,12 @@ static ae::DataBuffer MakeFinalPayload() {
   }
   p.nonce_consumed = consumed > 0xffffu ? 0xffffu
                                         : static_cast(consumed);
-  p.cb_any = g_last_result.cb_any;
-  p.cb_match = g_last_result.cb_match;
-  p.cb_count = g_last_result.cb_count;
+  p.cb_any = static_cast(g_cb_seen_count > 255 ? 255
+                                                             : g_cb_seen_count);
+  p.cb_match = 0;
+  p.cb_count = 0;
+  p.cb_timeout = static_cast(
+      g_cb_timeout_count > 255 ? 255 : g_cb_timeout_count);
   return bench::EncodeFast(p);
 }
 
@@ -386,6 +402,9 @@ static void StartPreparedPhase() {
   g_wifi_ready_count = 0;
   g_encode_count = 0;
   g_sendto_count = 0;
+  g_cb_seen_count = 0;
+  g_cb_timeout_count = 0;
+  g_auth_ok_count = 0;
   g_last_result = {};
   g_nonce_start =
       static_cast(prepared_send::PreparedMessageLeft());
@@ -570,6 +589,15 @@ void loop() {
         static_cast(bench::BisectStatusBits::kSendtoOk)) {
       ++g_sendto_count;
     }
+    if (result.cb_any) {
+      ++g_cb_seen_count;
+    }
+    if (result.cb_timeout) {
+      ++g_cb_timeout_count;
+    }
+    if (result.negotiated_auth == 3) {
+      ++g_auth_ok_count;
+    }
 
     ++g_prepared_index;
     if (g_prepared_index <= kPreparedPerVariant) {
diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp
index 5a70e38..baa6099 100644
--- a/temperature_receiver/main.cpp
+++ b/temperature_receiver/main.cpp
@@ -38,6 +38,8 @@ struct TestStats {
   std::vector got;
   std::vector cycle_us;
   std::vector connect_us;
+  std::vector tx_done_wait_us;
+  std::vector teardown_us;
   std::uint16_t wifi_ready{0};
   std::uint16_t encode{0};
   std::uint16_t sendto{0};
@@ -51,6 +53,7 @@ struct TestStats {
   std::uint8_t post_mode{0};
   int cb_any{0};
   int cb_match{0};
+  int cb_timeout{0};
 };
 
 std::mutex g_mu;
@@ -108,6 +111,8 @@ void PrintTestResult() {
           : *std::max_element(g_st.cycle_us.begin(), g_st.cycle_us.end()) /
                 1000;
   auto const conn_med = PercentileUs(g_st.connect_us, 50) / 1000;
+  auto const txdone_med = PercentileUs(g_st.tx_done_wait_us, 50) / 1000;
+  auto const teardown_med = PercentileUs(g_st.teardown_us, 50) / 1000;
   std::cout << "TEST_RESULT"
             << " test_id=" << static_cast(g_st.test_id)
             << " n=" << g_st.planned << " delivered=" << g_st.delivered << "/"
@@ -124,6 +129,9 @@ void PrintTestResult() {
             << " retry=" << static_cast(g_st.retry_max)
             << " post_mode=" << static_cast(g_st.post_mode)
             << " cb_any=" << g_st.cb_any << " cb_match=" << g_st.cb_match
+            << " cb_timeout=" << g_st.cb_timeout
+            << " txdone_med_ms=" << txdone_med
+            << " teardown_med_ms=" << teardown_med
             << " samples=" << g_st.cycle_us.size() << "\n";
   std::cout << "BENCH_DONE test_id=" << static_cast(g_st.test_id) << "\n";
   std::cout.flush();
@@ -170,17 +178,25 @@ void OnFast(temp_sensor::bench::FastPayload const& p) {
     g_st.post_mode = p.post_mode;
     g_st.cb_any += p.cb_any;
     g_st.cb_match += p.cb_match;
+    g_st.cb_timeout += p.cb_timeout;
     if (p.cycle_us != 0) {
       g_st.cycle_us.push_back(p.cycle_us);
     }
     if (p.connect_us != 0) {
       g_st.connect_us.push_back(p.connect_us);
     }
+    if (p.tx_done_wait_us != 0 || p.cb_any || p.cb_timeout) {
+      g_st.tx_done_wait_us.push_back(p.tx_done_wait_us);
+    }
+    if (p.teardown_us != 0) {
+      g_st.teardown_us.push_back(p.teardown_us);
+    }
     std::cout << ae::Format(
         "RECV PREPARED test_id={} idx={} seq={} cycle_us={} connect_us={} "
-        "auth={} flags={} ts={}\n",
+        "txdone_us={} teardown_us={} auth={} cb={} to={} flags={} ts={}\n",
         p.test_id, p.prepared_index, p.sequence_global, p.cycle_us,
-        p.connect_us, p.auth_negotiated, p.status_flags, ts);
+        p.connect_us, p.tx_done_wait_us, p.teardown_us, p.auth_negotiated,
+        p.cb_any, p.cb_timeout, p.status_flags, ts);
   } else if (type == temp_sensor::bench::FastMsgType::kFinal) {
     ++g_final_recv;
     g_st.test_id = p.test_id;
@@ -199,14 +215,22 @@ void OnFast(temp_sensor::bench::FastPayload const& p) {
     g_st.assoc_bits = p.assoc_bits;
     g_st.retry_max = p.retry_max;
     g_st.post_mode = p.post_mode;
-    g_st.cb_any += p.cb_any;
-    g_st.cb_match += p.cb_match;
+    // FINAL carries device totals for callback_seen / timeout.
+    g_st.cb_any = p.cb_any;
+    g_st.cb_match = p.cb_match;
+    g_st.cb_timeout = p.cb_timeout;
     if (p.cycle_us != 0) {
       g_st.cycle_us.push_back(p.cycle_us);
     }
     if (p.connect_us != 0) {
       g_st.connect_us.push_back(p.connect_us);
     }
+    if (p.tx_done_wait_us != 0 || p.cb_any || p.cb_timeout) {
+      g_st.tx_done_wait_us.push_back(p.tx_done_wait_us);
+    }
+    if (p.teardown_us != 0) {
+      g_st.teardown_us.push_back(p.teardown_us);
+    }
     // Prefer device counters for delivery when FULL was missed.
     if (g_st.delivered == 0 && p.sendto_count != 0) {
       g_st.delivered = p.sendto_count;

From 40a0e9bbf69f41688af75e561748c2eeca14b32f Mon Sep 17 00:00:00 2001
From: aethernet-io 
Date: Sat, 29 Aug 2026 10:20:55 -0700
Subject: [PATCH 26/32] Validate short PRE=25 callback path: N50 screen and
 230ms VAL200.

PRE=10/0 fail delivery on N50; keep late TX-done callback and PRE=25 winner.

Co-authored-by: Cursor 
---
 .../PREPARED_WIFI_CALLBACK_WPA2_REPORT.md     |  67 +--
 experiments/callback_short_pre_chat.txt       |  68 +++
 experiments/callback_short_pre_state.json     | 166 ++++++++
 experiments/prepared_wifi_callback_wpa2.tsv   |  24 +-
 experiments/run_callback_short_pre.py         | 387 ++++++++++++++++++
 temperature_receiver/main.cpp                 |  15 +
 6 files changed, 693 insertions(+), 34 deletions(-)
 create mode 100644 experiments/callback_short_pre_chat.txt
 create mode 100644 experiments/callback_short_pre_state.json
 create mode 100644 experiments/run_callback_short_pre.py

diff --git a/experiments/PREPARED_WIFI_CALLBACK_WPA2_REPORT.md b/experiments/PREPARED_WIFI_CALLBACK_WPA2_REPORT.md
index 3b8640e..78d0c8e 100644
--- a/experiments/PREPARED_WIFI_CALLBACK_WPA2_REPORT.md
+++ b/experiments/PREPARED_WIFI_CALLBACK_WPA2_REPORT.md
@@ -20,18 +20,17 @@ Experiment only. Production `SendPreparedOnce` was **not** switched.
 - PHY: automatic rate adaptation
 - `WIFI_PS_NONE`, max TX power, max normal ESP32-C6 CPU freq
 - Association retry: **10**
-- PRE: **200 ms** for VAL*; best short screen PRE: **25 ms**
-- POST: **TX_DONE_CALLBACK** (no fixed 300 ms)
+- PRE: **winner = 25 ms** (see SHORT PRE SEARCH)
+- POST: **TX_DONE_CALLBACK** (fixed delay = 0)
 
 ## CALLBACK
 
 - Registration point: **immediately before `sendto()`**
 - Socket kept open until first callback (or timeout), then unset + close + teardown
 - First TX-done used; **no** fingerprint / payload match
-- Wait: 50 ms primary, extend to 100 ms total max
-- `callback_timeout` counted separately (VAL*: **0**)
+- Wait: 50 ms primary, extend to 100 ms total max (safety only)
 
-## RESULT
+## RESULT (baseline PRE=200)
 
 | Run | N | delivered | callback_seen | timeouts | connect_med | txdone_med | teardown_med | cycle_med | p90 | max |
 |-----|---|-----------|---------------|----------|-------------|------------|--------------|-----------|-----|-----|
@@ -39,32 +38,52 @@ Experiment only. Production `SendPreparedOnce` was **not** switched.
 | VAL100 | 100 | 100/100 | 100/100 | 0 | 127 | 2 | 86 | **420** | 490 | 680 |
 | VAL200 | 200 | 189/200 | 200/200 | 0 | 127 | 1 | 97 | **430** | 490 | 780 |
 
-Lifecycle on VAL200: wifi_ready=200, encode=200, sendto=200, nonce=200.
+## SHORT PRE SEARCH
 
-### PRE sweep (screen ×20, POST=callback only)
+Only PRE changed. Screens use **N=50**. Adaptive order: 25 → 10 → 0.
 
-| PRE | delivered | callback | cycle_med | note |
-|-----|-----------|----------|-----------|------|
-| 150 | 20/20 | 20/20 | 350 | ok |
-| 100 | 20/20 | 20/20 | 320 | ok |
-| 75 | 20/20 | 20/20 | 280 | ok |
-| 50 | 16/20 | 20/20 | 270 | weaker delivery |
-| **25** | **20/20** | **20/20** | **230** | **shortest practical** |
-| 0 | 3/20 | 20/20 | 220 | delivery collapsed — stop |
+| PRE | N | delivery | callback_seen | timeout | connect med | txdone med | cycle med | p90 | max | note |
+|-----|---|----------|---------------|---------|-------------|------------|-----------|-----|-----|------|
+| 200 | 200 | 189/200 | 200/200 | 0 | 127 | 1 | **430** | 490 | 780 | prior VAL200 |
+| **25** | **50** | **37/50** | **50/50** | **0** | 128 | 13 | **230** | 250 | 310 | screen |
+| **25** | **200** | **182/200** | **200/200** | **0** | 131 | 20 | **230** | 270 | 390 | **VAL200 winner** |
+| 10 | 50 | 21/50 | 50/50 | 0 | 143 | 0 | 220 | 250 | 290 | delivery too weak — no VAL200 |
+| 0 | 50 | 7/50 | 49/50 | 1 | 130 | 0 | 220 | 250 | 1060 | collapsed — no VAL200 |
 
-## COMPARE (VAL200 PRE=200)
+Lifecycle on PRE25 VAL200: wifi_ready=200, encode=200, sendto=200, nonce=200. missing=18, duplicates=0, ooo=0.
 
-| | cycle median | delivery |
-|--|--------------|----------|
-| OLD winner (PRE=200, POST=300, WPA2) | **710 ms** | 198/200 |
-| NEW callback (PRE=200, POST=cb) | **430 ms** | 189/200 |
+### Winner
 
-- Absolute saving: **280 ms**
-- Percent saving: **39.4%**
-- Speedup: **1.65×**
+**PRE = 25 ms**, POST = late TX-done callback.
 
-Callback path is the new fastest candidate at PRE=200. Shortest practical PRE from sweep: **25 ms** (screen 20/20 @ 230 ms cycle); not re-validated at N=200 in this campaign.
+- callback_seen stable (200/200)
+- callback_timeout = 0
+- Wi-Fi lifecycle intact
+- delivery at normal UDP level (182/200 ≈ 91%)
+
+PRE=0 and PRE=10 are faster by ~10 ms median but delivery collapses; not winners.
+
+## COMPARE
+
+| | PRE | cycle median | delivery |
+|--|-----|--------------|----------|
+| OLD fixed POST=300 | 200 | 710 ms | 198/200 |
+| Callback PRE=200 | 200 | **430 ms** | 189/200 |
+| **Callback PRE=25 (winner)** | **25** | **230 ms** | **182/200** |
+
+### vs prior callback PRE=200 (430 ms)
+
+- Absolute saving: **200 ms**
+- Percent saving: **46.5%**
+- Speedup: **1.87×**
+
+### vs original fixed-POST winner (710 ms)
+
+- Absolute saving: **480 ms**
+- Percent saving: **67.6%**
+- Speedup: **3.09×**
 
 ## Raw data
 
 See `experiments/prepared_wifi_callback_wpa2.tsv`.
+Orchestrator: `experiments/run_callback_short_pre.py`.
diff --git a/experiments/callback_short_pre_chat.txt b/experiments/callback_short_pre_chat.txt
new file mode 100644
index 0000000..0779199
--- /dev/null
+++ b/experiments/callback_short_pre_chat.txt
@@ -0,0 +1,68 @@
+[TEST 1/6] PRE25_N50
+delivery=37/50
+callback=50/50
+timeouts=0
+missing=13 dup=0 ooo=0
+connect_med=128
+txdone_med=13
+teardown_med=59
+cycle_med=230
+p90=250
+max=310
+result=PASS
+remaining=5
+BEST NOW:
+PRE=25 cycle=230ms del=37/50 cb=50
+NEXT: PRE25_VAL200 if OK
+
+[TEST 2/6] PRE25_VAL200
+delivery=182/200
+callback=200/200
+timeouts=0
+missing=18 dup=0 ooo=0
+connect_med=131
+txdone_med=20
+teardown_med=58
+cycle_med=230
+p90=270
+max=390
+result=PASS
+remaining=4
+BEST NOW:
+PRE=25 cycle=230ms del=37/50 cb=50
+NEXT: PRE10_N50
+
+[TEST 3/6] PRE10_N50
+delivery=21/50
+callback=50/50
+timeouts=0
+missing=29 dup=0 ooo=0
+connect_med=143
+txdone_med=0
+teardown_med=61
+cycle_med=220
+p90=250
+max=290
+result=FAIL
+remaining=3
+BEST NOW:
+PRE=25 cycle=230ms del=37/50 cb=50
+NEXT: PRE0_N50
+
+[TEST 4/6] PRE0_N50
+delivery=7/50
+callback=49/50
+timeouts=1
+missing=43 dup=0 ooo=0
+connect_med=130
+txdone_med=0
+teardown_med=85
+cycle_med=220
+p90=250
+max=1060
+result=FAIL
+remaining=2
+BEST NOW:
+PRE=25 cycle=230ms del=37/50 cb=50
+NEXT: VAL200 for best short PRE
+
diff --git a/experiments/callback_short_pre_state.json b/experiments/callback_short_pre_state.json
new file mode 100644
index 0000000..9564e61
--- /dev/null
+++ b/experiments/callback_short_pre_state.json
@@ -0,0 +1,166 @@
+{
+  "results": {
+    "PRE25_N50": {
+      "id": 201,
+      "n": 50,
+      "del": 37,
+      "plan": 50,
+      "conn": 128,
+      "cyc": 230,
+      "p90": 250,
+      "mx": 310,
+      "wr": 50,
+      "enc": 50,
+      "st": 50,
+      "nonce": 50,
+      "pre": 25,
+      "post": 0,
+      "assoc": 154,
+      "auth": 3,
+      "retry": 10,
+      "pm": 1,
+      "cba": 50,
+      "cbm": 0,
+      "cbt": 0,
+      "txd": 13,
+      "td": 59,
+      "miss": 13,
+      "dup": 0,
+      "ooo": 0,
+      "samp": 37
+    },
+    "PRE25_VAL200": {
+      "id": 202,
+      "n": 200,
+      "del": 182,
+      "plan": 200,
+      "conn": 131,
+      "cyc": 230,
+      "p90": 270,
+      "mx": 390,
+      "wr": 200,
+      "enc": 200,
+      "st": 200,
+      "nonce": 200,
+      "pre": 25,
+      "post": 0,
+      "assoc": 154,
+      "auth": 3,
+      "retry": 10,
+      "pm": 1,
+      "cba": 200,
+      "cbm": 0,
+      "cbt": 0,
+      "txd": 20,
+      "td": 58,
+      "miss": 18,
+      "dup": 0,
+      "ooo": 0,
+      "samp": 182
+    },
+    "PRE10_N50": {
+      "id": 203,
+      "n": 50,
+      "del": 21,
+      "plan": 50,
+      "conn": 143,
+      "cyc": 220,
+      "p90": 250,
+      "mx": 290,
+      "wr": 50,
+      "enc": 50,
+      "st": 50,
+      "nonce": 50,
+      "pre": 10,
+      "post": 0,
+      "assoc": 154,
+      "auth": 3,
+      "retry": 10,
+      "pm": 1,
+      "cba": 50,
+      "cbm": 0,
+      "cbt": 0,
+      "txd": 0,
+      "td": 61,
+      "miss": 29,
+      "dup": 0,
+      "ooo": 0,
+      "samp": 22
+    },
+    "PRE0_N50": {
+      "id": 204,
+      "n": 50,
+      "del": 7,
+      "plan": 50,
+      "conn": 130,
+      "cyc": 220,
+      "p90": 250,
+      "mx": 1060,
+      "wr": 50,
+      "enc": 50,
+      "st": 50,
+      "nonce": 50,
+      "pre": 0,
+      "post": 0,
+      "assoc": 154,
+      "auth": 3,
+      "retry": 10,
+      "pm": 1,
+      "cba": 49,
+      "cbm": 0,
+      "cbt": 1,
+      "txd": 0,
+      "td": 85,
+      "miss": 43,
+      "dup": 0,
+      "ooo": 0,
+      "samp": 8
+    }
+  },
+  "val200": {
+    "25": {
+      "id": 202,
+      "n": 200,
+      "del": 182,
+      "plan": 200,
+      "conn": 131,
+      "cyc": 230,
+      "p90": 270,
+      "mx": 390,
+      "wr": 200,
+      "enc": 200,
+      "st": 200,
+      "nonce": 200,
+      "pre": 25,
+      "post": 0,
+      "assoc": 154,
+      "auth": 3,
+      "retry": 10,
+      "pm": 1,
+      "cba": 200,
+      "cbm": 0,
+      "cbt": 0,
+      "txd": 20,
+      "td": 58,
+      "miss": 18,
+      "dup": 0,
+      "ooo": 0,
+      "samp": 182
+    }
+  },
+  "winner_pre": 25,
+  "best": {
+    "name": "PRE25_N50",
+    "pre": 25,
+    "cyc": 230,
+    "del": 37,
+    "plan": 50,
+    "cba": 50,
+    "cbt": 0,
+    "conn": 128,
+    "txd": 13,
+    "p90": 250,
+    "mx": 310
+  },
+  "old_pre200_cycle": 430
+}
\ No newline at end of file
diff --git a/experiments/prepared_wifi_callback_wpa2.tsv b/experiments/prepared_wifi_callback_wpa2.tsv
index dcf4545..27eaccc 100644
--- a/experiments/prepared_wifi_callback_wpa2.tsv
+++ b/experiments/prepared_wifi_callback_wpa2.tsv
@@ -1,10 +1,14 @@
-name	test_id	n	delivered	plan	connect_med	txdone_med	teardown_med	cycle_med	p90	max	wifi_ready	encode	sendto	nonce	pre	post_mode	auth	cb_seen	cb_timeout	samples
-VAL20_PRE200	101	20	19	20	127	1	77	420	440	490	20	20	20	20	200	1	3	20	0	19
-VAL100_PRE200	102	100	100	100	127	2	86	420	490	680	100	100	100	100	200	1	3	100	0	100
-VAL200_PRE200	103	200	189	200	127	1	97	430	490	780	200	200	200	200	200	1	3	200	0	189
-PRE150_S20	140	20	20	20	115	4	76	350	420	500	20	20	20	20	150	1	3	20	0	20
-PRE100_S20	130	20	20	20	128	0	85	320	350	390	20	20	20	20	100	1	3	20	0	20
-PRE75_S20	125	20	20	20	134	4	76	280	320	430	20	20	20	20	75	1	3	20	0	20
-PRE50_S20	120	20	16	20	133	6	71	270	330	390	20	20	20	20	50	1	3	20	0	16
-PRE25_S20	115	20	20	20	114	1	92	230	290	640	20	20	20	20	25	1	3	20	0	20
-PRE0_S20	110	20	3	20	126	0	59	220	230	240	20	20	20	20	0	1	3	20	0	4
+name	test_id	n	delivered	plan	connect_med	txdone_med	teardown_med	cycle_med	p90	max	wifi_ready	encode	sendto	nonce	pre	post_mode	auth	cb_seen	cb_timeout	missing	duplicates	ooo	samples
+VAL20_PRE200	101	20	19	20	127	1	77	420	440	490	20	20	20	20	200	1	3	20	0	1	0	0	19
+VAL100_PRE200	102	100	100	100	127	2	86	420	490	680	100	100	100	100	200	1	3	100	0	0	0	0	100
+VAL200_PRE200	103	200	189	200	127	1	97	430	490	780	200	200	200	200	200	1	3	200	0	11	0	0	189
+PRE150_S20	140	20	20	20	115	4	76	350	420	500	20	20	20	20	150	1	3	20	0	0	0	0	20
+PRE100_S20	130	20	20	20	128	0	85	320	350	390	20	20	20	20	100	1	3	20	0	0	0	0	20
+PRE75_S20	125	20	20	20	134	4	76	280	320	430	20	20	20	20	75	1	3	20	0	0	0	0	20
+PRE50_S20	120	20	16	20	133	6	71	270	330	390	20	20	20	20	50	1	3	20	0	4	0	0	16
+PRE25_S20	115	20	20	20	114	1	92	230	290	640	20	20	20	20	25	1	3	20	0	0	0	0	20
+PRE0_S20	110	20	3	20	126	0	59	220	230	240	20	20	20	20	0	1	3	20	0	17	0	0	4
+PRE25_N50	201	50	37	50	128	13	59	230	250	310	50	50	50	50	25	1	3	50	0	13	0	0	37
+PRE25_VAL200	202	200	182	200	131	20	58	230	270	390	200	200	200	200	25	1	3	200	0	18	0	0	182
+PRE10_N50	203	50	21	50	143	0	61	220	250	290	50	50	50	50	10	1	3	50	0	29	0	0	22
+PRE0_N50	204	50	7	50	130	0	85	220	250	1060	50	50	50	50	0	1	3	49	1	43	0	0	8
diff --git a/experiments/run_callback_short_pre.py b/experiments/run_callback_short_pre.py
new file mode 100644
index 0000000..36987a1
--- /dev/null
+++ b/experiments/run_callback_short_pre.py
@@ -0,0 +1,387 @@
+"""Short PRE search for late TX-done callback path (WPA2). N=50 screens + VAL200."""
+
+from __future__ import annotations
+
+import json
+import re
+import sys
+import time
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from run_fastest_path import (  # noqa: E402
+    BUILD,
+    ROOT,
+    RX_LOG,
+    cmake_configure,
+    ensure_receiver,
+    flash,
+    log,
+    ninja_build,
+)
+
+RESULTS = ROOT / "experiments" / "prepared_wifi_callback_wpa2.tsv"
+CHAT = ROOT / "experiments" / "callback_short_pre_chat.txt"
+STATE = ROOT / "experiments" / "callback_short_pre_state.json"
+PROGRESS = ROOT / "experiments" / "callback_short_pre_progress.log"
+
+RESULT_RE_EXT = re.compile(
+    r"TEST_RESULT test_id=(?P\d+) n=(?P\d+) delivered=(?P\d+)/(?P\d+) "
+    r"connect_med_ms=(?P\d+) cycle_med_ms=(?P\d+) p90_ms=(?P\d+) "
+    r"max_ms=(?P\d+) wifi_ready=(?P\d+) encode=(?P\d+) sendto=(?P\d+) "
+    r"nonce=(?P\d+) pre=(?P
\d+) post=(?P\d+) assoc=0x(?P[0-9a-fA-F]+) "
+    r"auth=(?P\d+) retry=(?P\d+) post_mode=(?P\d+) cb_any=(?P\d+) "
+    r"cb_match=(?P\d+)(?: cb_timeout=(?P\d+))?(?: txdone_med_ms=(?P\d+))?"
+    r"(?: teardown_med_ms=(?P\d+))?(?: missing=(?P\d+))?"
+    r"(?: duplicates=(?P\d+))?(?: ooo=(?P\d+))? samples=(?P\d+)"
+)
+
+BASE = {
+    "AE_EXP_FAST_USE_BSSID": "0",
+    "AE_EXP_FAST_FAST_SCAN": "0",
+    "AE_EXP_FAST_AUTH": "2",
+    "AE_EXP_FAST_RETRY": "10",
+    "AE_EXP_FAST_POST_MODE": "1",
+    "AE_EXP_FAST_POST_MS": "0",
+    "AE_EXP_FAST_AMPDU_TX_OFF": "0",
+    "AE_EXP_FAST_STORAGE_RAM": "0",
+    "AE_EXP_FAST_DISABLE_WPA3": "1",
+}
+
+OLD_PRE200_CYCLE = 430  # confirmed VAL200 callback PRE=200
+
+
+def force_wpa3_off() -> None:
+    sdk = BUILD / "sdkconfig"
+    if not sdk.exists():
+        return
+    text = sdk.read_text(encoding="utf-8")
+    text = text.replace(
+        "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y",
+        "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set",
+    )
+    text = text.replace(
+        "CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y",
+        "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set",
+    )
+    sdk.write_text(text, encoding="utf-8")
+
+
+def count_results() -> int:
+    if not RX_LOG.exists():
+        return 0
+    text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+    return len(list(RESULT_RE_EXT.finditer(text)))
+
+
+def wait_result_ext(
+    prev: int, timeout_s: int, expect_id: int | None, expect_n: int | None
+) -> dict:
+    deadline = time.time() + timeout_s
+    last_hb = 0.0
+    while time.time() < deadline:
+        now = time.time()
+        if now - last_hb >= 30:
+            last_hb = now
+            log(f"waiting TEST_RESULT prev={prev} left={int(deadline-now)}s")
+        if RX_LOG.exists():
+            text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+            matches = list(RESULT_RE_EXT.finditer(text))
+            if len(matches) > prev:
+                for m in reversed(matches[prev:]):
+                    gd = m.groupdict()
+                    d = {}
+                    for k, v in gd.items():
+                        if v is None:
+                            d[k] = 0
+                        elif k == "assoc":
+                            d[k] = int(v, 16)
+                        else:
+                            d[k] = int(v)
+                    if expect_id is not None and d["id"] != expect_id:
+                        continue
+                    if expect_n is not None and d["n"] != expect_n:
+                        continue
+                    return d
+        time.sleep(2)
+    raise TimeoutError("no TEST_RESULT")
+
+
+def judge(r: dict) -> str:
+    plan = r.get("plan", 0)
+    if r.get("auth", 0) != 3:
+        return "INVALID_AUTH"
+    if r.get("wr", 0) < plan or r.get("enc", 0) < plan or r.get("st", 0) < plan:
+        return "LIFECYCLE_FAIL"
+    if r.get("cba", 0) < plan - 1:  # allow 1 miss on cb accounting
+        return "CALLBACK_WEAK"
+    if r.get("cbt", 0) > max(1, plan // 25):
+        return "TIMEOUT_HIGH"
+    # UDP losses OK; catastrophic = <70%
+    if r["del"] < int(plan * 0.70):
+        return "FAIL"
+    return "PASS"
+
+
+def write_chat(
+    test_no: int, remaining: int, name: str, r: dict, best: dict | None, nxt: str
+) -> None:
+    result = judge(r)
+    best_s = "none"
+    if best:
+        best_s = (
+            f"PRE={best.get('pre')} cycle={best.get('cyc')}ms "
+            f"del={best.get('del')}/{best.get('plan')} cb={best.get('cba')}"
+        )
+    lines = [
+        f"[TEST {test_no}/{test_no + remaining}] {name}",
+        f"delivery={r['del']}/{r['plan']}",
+        f"callback={r.get('cba', 0)}/{r.get('wr', r['plan'])}",
+        f"timeouts={r.get('cbt', 0)}",
+        f"missing={r.get('miss', r['plan'] - r['del'])} dup={r.get('dup', 0)} ooo={r.get('ooo', 0)}",
+        f"connect_med={r['conn']}",
+        f"txdone_med={r.get('txd', 0)}",
+        f"teardown_med={r.get('td', 0)}",
+        f"cycle_med={r['cyc']}",
+        f"p90={r['p90']}",
+        f"max={r['mx']}",
+        f"result={result}",
+        f"remaining={remaining}",
+        "BEST NOW:",
+        best_s,
+        f"NEXT: {nxt}",
+        "",
+    ]
+    text = "\n".join(lines)
+    CHAT.parent.mkdir(parents=True, exist_ok=True)
+    with CHAT.open("a", encoding="utf-8") as f:
+        f.write(text + "\n")
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(text + "\n")
+    print(text, flush=True)
+
+
+def append_tsv(name: str, r: dict) -> None:
+    header = (
+        "name\ttest_id\tn\tdelivered\tplan\tconnect_med\ttxdone_med\tteardown_med\t"
+        "cycle_med\tp90\tmax\twifi_ready\tencode\tsendto\tnonce\tpre\tpost_mode\t"
+        "auth\tcb_seen\tcb_timeout\tmissing\tduplicates\tooo\tsamples\n"
+    )
+    need_header = not RESULTS.exists()
+    if RESULTS.exists():
+        first = RESULTS.read_text(encoding="utf-8").splitlines()[:1]
+        if first and "missing" not in first[0]:
+            # keep old file; append with extended header note via compatible columns
+            pass
+    if need_header:
+        RESULTS.write_text(header, encoding="utf-8")
+    line = (
+        f"{name}\t{r['id']}\t{r['n']}\t{r['del']}\t{r['plan']}\t{r['conn']}\t"
+        f"{r.get('txd',0)}\t{r.get('td',0)}\t{r['cyc']}\t{r['p90']}\t{r['mx']}\t"
+        f"{r['wr']}\t{r['enc']}\t{r['st']}\t{r['nonce']}\t{r['pre']}\t{r['pm']}\t"
+        f"{r['auth']}\t{r.get('cba',0)}\t{r.get('cbt',0)}\t"
+        f"{r.get('miss', r['plan']-r['del'])}\t{r.get('dup',0)}\t{r.get('ooo',0)}\t"
+        f"{r['samp']}\n"
+    )
+    with RESULTS.open("a", encoding="utf-8") as f:
+        f.write(line)
+
+
+def run_one(name: str, test_id: int, n: int, pre_ms: int, timeout_s: int) -> dict:
+    flags = {
+        **BASE,
+        "AE_EXP_FAST_TEST_ID": str(test_id),
+        "AE_EXP_FAST_N": str(n),
+        "AE_EXP_FAST_PRE_MS": str(pre_ms),
+    }
+    ensure_receiver()
+    cmake_configure(flags)
+    force_wpa3_off()
+    ninja_build()
+    force_wpa3_off()
+    prev = count_results()
+    flash()
+    r = wait_result_ext(prev, timeout_s, expect_id=test_id, expect_n=n)
+    r["pre"] = pre_ms
+    if "miss" not in r or r["miss"] == 0 and r["del"] < r["plan"]:
+        r["miss"] = r["plan"] - r["del"]
+    append_tsv(name, r)
+    return r
+
+
+def consider_best(best: dict | None, name: str, r: dict) -> dict:
+    if judge(r) not in ("PASS",):
+        return best if best else {
+            "name": name,
+            "pre": r["pre"],
+            "cyc": r["cyc"],
+            "del": r["del"],
+            "plan": r["plan"],
+            "cba": r.get("cba", 0),
+            "cbt": r.get("cbt", 0),
+            "conn": r["conn"],
+            "txd": r.get("txd", 0),
+            "p90": r["p90"],
+            "mx": r["mx"],
+        }
+    cand = {
+        "name": name,
+        "pre": r["pre"],
+        "cyc": r["cyc"],
+        "del": r["del"],
+        "plan": r["plan"],
+        "cba": r.get("cba", 0),
+        "cbt": r.get("cbt", 0),
+        "conn": r["conn"],
+        "txd": r.get("txd", 0),
+        "p90": r["p90"],
+        "mx": r["mx"],
+    }
+    if best is None:
+        return cand
+    # Prefer shorter PRE if cycle not worse by much and delivery OK; else faster cycle
+    if cand["pre"] < best["pre"] and cand["cyc"] <= best["cyc"] + 20:
+        return cand
+    if cand["cyc"] < best["cyc"]:
+        return cand
+    return best
+
+
+def screen_ok_for_val200(r: dict) -> bool:
+    return (
+        judge(r) == "PASS"
+        and r.get("auth", 0) == 3
+        and r.get("cba", 0) >= r["plan"] - 0
+        and r.get("cbt", 0) <= 1
+        and r["wr"] == r["plan"]
+        and r["enc"] == r["plan"]
+        and r["st"] == r["plan"]
+    )
+
+
+def main() -> int:
+    CHAT.write_text("", encoding="utf-8")
+    # Ensure TSV has extended header for new rows; keep prior history via append
+    if not RESULTS.exists():
+        RESULTS.write_text(
+            "name\ttest_id\tn\tdelivered\tplan\tconnect_med\ttxdone_med\tteardown_med\t"
+            "cycle_med\tp90\tmax\twifi_ready\tencode\tsendto\tnonce\tpre\tpost_mode\t"
+            "auth\tcb_seen\tcb_timeout\tmissing\tduplicates\tooo\tsamples\n",
+            encoding="utf-8",
+        )
+    else:
+        # Append a blank separator comment isn't valid TSV; just continue appending
+        pass
+
+    best: dict | None = None
+    results: dict = {}
+    test_no = 1
+    remaining = 6
+    val200_by_pre: dict[int, dict] = {}
+
+    # --- PRE25_N50 ---
+    name = "PRE25_N50"
+    log(f"=== {name} ===")
+    r25 = run_one(name, test_id=201, n=50, pre_ms=25, timeout_s=1800)
+    results[name] = r25
+    best = consider_best(best, name, r25)
+    write_chat(test_no, remaining - 1, name, r25, best, "PRE25_VAL200 if OK")
+    test_no += 1
+    remaining -= 1
+
+    if screen_ok_for_val200(r25):
+        name = "PRE25_VAL200"
+        log(f"=== {name} ===")
+        r = run_one(name, test_id=202, n=200, pre_ms=25, timeout_s=7200)
+        results[name] = r
+        val200_by_pre[25] = r
+        best = consider_best(best, name, r)
+        write_chat(test_no, remaining - 1, name, r, best, "PRE10_N50")
+        test_no += 1
+        remaining -= 1
+    else:
+        log("PRE25 screen not OK for VAL200 — skip VAL200")
+
+    # --- PRE10_N50 ---
+    name = "PRE10_N50"
+    log(f"=== {name} ===")
+    r10 = run_one(name, test_id=203, n=50, pre_ms=10, timeout_s=1800)
+    results[name] = r10
+    best = consider_best(best, name, r10)
+    write_chat(test_no, remaining - 1, name, r10, best, "PRE0_N50")
+    test_no += 1
+    remaining -= 1
+
+    # --- PRE0_N50 ---
+    name = "PRE0_N50"
+    log(f"=== {name} ===")
+    r0 = run_one(name, test_id=204, n=50, pre_ms=0, timeout_s=1800)
+    results[name] = r0
+    best = consider_best(best, name, r0)
+    write_chat(test_no, remaining - 1, name, r0, best, "VAL200 for best short PRE")
+    test_no += 1
+    remaining -= 1
+
+    # Pick best short PRE among 0/10/25 that passed screen
+    candidates = []
+    for pre, rr in ((0, r0), (10, r10), (25, r25)):
+        if screen_ok_for_val200(rr):
+            candidates.append((pre, rr))
+    # Prefer shortest PRE among passing screens
+    candidates.sort(key=lambda x: (x[0], x[1]["cyc"]))
+
+    winner_pre = 25
+    if candidates:
+        winner_pre = candidates[0][0]
+    else:
+        # fallback: least-bad by delivery then cycle
+        ranked = sorted(
+            [(0, r0), (10, r10), (25, r25)],
+            key=lambda x: (-x[1]["del"], x[1]["cyc"]),
+        )
+        winner_pre = ranked[0][0]
+
+    # VAL200 for winner if not already done (PRE25 may already have VAL200)
+    if winner_pre not in val200_by_pre and screen_ok_for_val200(
+        {0: r0, 10: r10, 25: r25}[winner_pre]
+    ):
+        name = f"PRE{winner_pre}_VAL200"
+        log(f"=== {name} ===")
+        r = run_one(
+            name, test_id=210 + winner_pre, n=200, pre_ms=winner_pre, timeout_s=7200
+        )
+        results[name] = r
+        val200_by_pre[winner_pre] = r
+        best = consider_best(best, name, r)
+        write_chat(test_no, remaining - 1, name, r, best, "report")
+        test_no += 1
+        remaining -= 1
+    elif winner_pre in val200_by_pre:
+        log(f"VAL200 already done for PRE={winner_pre}")
+        best = consider_best(best, f"PRE{winner_pre}_VAL200", val200_by_pre[winner_pre])
+    else:
+        log(f"No VAL200 for winner PRE={winner_pre} (screen not OK)")
+
+    # If PRE0 or PRE10 passed and is shorter than PRE25 and better, ensure VAL200
+    # (already handled by winner_pre)
+
+    STATE.write_text(
+        json.dumps(
+            {
+                "results": results,
+                "val200": {str(k): v for k, v in val200_by_pre.items()},
+                "winner_pre": winner_pre,
+                "best": best,
+                "old_pre200_cycle": OLD_PRE200_CYCLE,
+            },
+            indent=2,
+            default=str,
+        ),
+        encoding="utf-8",
+    )
+    log(f"DONE winner_pre={winner_pre} best={best}")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp
index baa6099..2d5e1da 100644
--- a/temperature_receiver/main.cpp
+++ b/temperature_receiver/main.cpp
@@ -35,6 +35,8 @@ struct TestStats {
   int planned{20};
   int delivered{0};
   int duplicates{0};
+  int out_of_order{0};
+  int max_idx_seen{0};
   std::vector got;
   std::vector cycle_us;
   std::vector connect_us;
@@ -97,6 +99,12 @@ void NotePrepared(int idx) {
   if (!seen) {
     seen = 1;
     ++g_st.delivered;
+    if (idx < g_st.max_idx_seen) {
+      ++g_st.out_of_order;
+    }
+    if (idx > g_st.max_idx_seen) {
+      g_st.max_idx_seen = idx;
+    }
   } else {
     ++g_st.duplicates;
   }
@@ -113,6 +121,10 @@ void PrintTestResult() {
   auto const conn_med = PercentileUs(g_st.connect_us, 50) / 1000;
   auto const txdone_med = PercentileUs(g_st.tx_done_wait_us, 50) / 1000;
   auto const teardown_med = PercentileUs(g_st.teardown_us, 50) / 1000;
+  int missing = g_st.planned - g_st.delivered;
+  if (missing < 0) {
+    missing = 0;
+  }
   std::cout << "TEST_RESULT"
             << " test_id=" << static_cast(g_st.test_id)
             << " n=" << g_st.planned << " delivered=" << g_st.delivered << "/"
@@ -132,6 +144,9 @@ void PrintTestResult() {
             << " cb_timeout=" << g_st.cb_timeout
             << " txdone_med_ms=" << txdone_med
             << " teardown_med_ms=" << teardown_med
+            << " missing=" << missing
+            << " duplicates=" << g_st.duplicates
+            << " ooo=" << g_st.out_of_order
             << " samples=" << g_st.cycle_us.size() << "\n";
   std::cout << "BENCH_DONE test_id=" << static_cast(g_st.test_id) << "\n";
   std::cout.flush();

From 5b878e10690ada5a08a027e851cef89951677f2a Mon Sep 17 00:00:00 2001
From: aethernet-io 
Date: Sat, 29 Aug 2026 12:06:33 -0700
Subject: [PATCH 27/32] Add deep-sleep 5x50 prepared E2E with RTC cache and
 250ms HOT median.

Silent WPA2 late-callback path survives deep sleep via RTC Wi-Fi cache;
report 5 FULL + 249 HOT records (FINAL Aether flush incomplete).

Co-authored-by: Cursor 
---
 CMakeLists.txt                                |   8 +
 experiments/PREPARED_DEEPSLEEP_5X50_REPORT.md | 139 +++
 experiments/deepsleep_5x50_chat.txt           |  42 +
 experiments/prepared_deepsleep_5x50.tsv       | 255 +++++
 experiments/run_deepsleep_5x50.py             | 280 ++++++
 main/CMakeLists.txt                           |   9 +
 main/bench_payload.h                          |  72 ++
 main/experiment_early_entry.cpp               |  34 +
 main/experiment_early_entry.h                 |  32 +
 main/main.cpp                                 |   4 +
 main/prepared_deepsleep_5x50_bench.cpp        | 916 ++++++++++++++++++
 main/prepared_send/prepared_send.cpp          | 126 ++-
 main/prepared_send/prepared_send.h            |  28 +-
 sdkconfig.defaults.deepsleep_5x50             |  21 +
 temperature_receiver/main.cpp                 | 437 +++++----
 15 files changed, 2187 insertions(+), 216 deletions(-)
 create mode 100644 experiments/PREPARED_DEEPSLEEP_5X50_REPORT.md
 create mode 100644 experiments/deepsleep_5x50_chat.txt
 create mode 100644 experiments/prepared_deepsleep_5x50.tsv
 create mode 100644 experiments/run_deepsleep_5x50.py
 create mode 100644 main/experiment_early_entry.cpp
 create mode 100644 main/experiment_early_entry.h
 create mode 100644 main/prepared_deepsleep_5x50_bench.cpp
 create mode 100644 sdkconfig.defaults.deepsleep_5x50

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6333f9e..f084591 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -35,6 +35,8 @@ set(AE_EXP_PREPARED_WIFI_BISECT "" CACHE STRING
     "Silent single-factor prepared Wi-Fi bisect (set to 1)")
 set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING
     "Silent fastest-path prepared Wi-Fi campaign (set to 1)")
+set(AE_EXP_PREPARED_DEEPSLEEP_5X50 "" CACHE STRING
+    "Silent deep-sleep 5x50 prepared E2E (set to 1)")
 set(AE_EXP_FAST_DISABLE_WPA3 "" CACHE STRING
     "Benchmark-only: disable CONFIG_ESP_WIFI_ENABLE_WPA3_SAE (set to 1)")
 set(AE_EXP_BISECT_CONSOLE "" CACHE STRING
@@ -45,6 +47,12 @@ if(AE_EXP_BISECT_CONSOLE STREQUAL "1" AND
    AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1")
   list(APPEND SDKCONFIG_DEFAULTS
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.bench")
+elseif(AE_EXP_PREPARED_DEEPSLEEP_5X50 STREQUAL "1")
+  list(APPEND SDKCONFIG_DEFAULTS
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
 elseif(AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1")
   list(APPEND SDKCONFIG_DEFAULTS
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
diff --git a/experiments/PREPARED_DEEPSLEEP_5X50_REPORT.md b/experiments/PREPARED_DEEPSLEEP_5X50_REPORT.md
new file mode 100644
index 0000000..b90fb3f
--- /dev/null
+++ b/experiments/PREPARED_DEEPSLEEP_5X50_REPORT.md
@@ -0,0 +1,139 @@
+# Prepared deep-sleep 5×50 E2E report
+
+Experiment only. Production `SendPreparedOnce` was **not** switched.
+
+## Pins
+
+| Repo | Branch | SHA |
+|------|--------|-----|
+| temperature-sensor | `thermometer-prepared-send-v0` | *(this commit)* |
+| aether-client-cpp | `exp/esp32c6-wifi-lifecycle-diag` | `157aadbec8e7b852d0f89274307ff7cb8103e5f7` **unchanged** |
+
+## Effective sdkconfig (verified before flash)
+
+```
+CONFIG_RTC_CLK_SRC_EXT_CRYS=y
+# CONFIG_RTC_CLK_SRC_INT_RC is not set
+CONFIG_RTC_CLK_CAL_CYCLES=1024
+CONFIG_ESP_BROWNOUT_DET=y
+CONFIG_ESP_BROWNOUT_DET_LVL_SEL_7=y   (from base defaults)
+# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set
+CONFIG_ESP_CONSOLE_NONE=y
+CONFIG_LOG_DEFAULT_LEVEL_NONE=y
+CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y
+# CONFIG_PM_ENABLE is not set
+CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y
+```
+
+## Fast Wi-Fi / HOT path
+
+- WPA2-PSK, negotiated authmode=3 on all recovered HOT samples
+- Cached channel; **no** BSSID reconnect
+- Static IPv4 / netmask / gateway + cached GW MAC + static ARP (RTC `PreparedWifiRtcCache`)
+- Wi-Fi 4 (b/g/n), auto PHY, `WIFI_PS_NONE`, max TX, retry=10
+- PRE=25 ms, POST=0, late TX-done callback immediately before `sendto`, socket held until callback
+- Safety callback wait ≤100 ms; no fingerprint matching
+
+## RTC state
+
+- `RtcState` (magic `DS3P`) + CRC: phase, outer 1..5, hot 1..50, attempts, pending measurement, brownout/unexpected counters, sleep arm timestamp
+- `PreparedWifiRtcCache` (magic `WCF1`) + CRC: channel, IP, netmask, gateway, gw_mac (BSSID diagnostic only)
+- Prepared nonce block remains `RTC_NOINIT` `PreparedSendMessageBlock` (existing aether layout)
+- Early `app_main` hook: `ExperimentEarlyAppEntry()` before WDT/`setup`
+
+## Campaign shape
+
+- Client id: `prepared_deepsleep_5x50_v1`
+- 5 measured FULL Æther cycles
+- Per FULL: `PrepareSendMessageBlock(50)` → 50 deep-sleep HOT wakes (3 s) → next FULL
+- Target: 250 prepared `sendto`; FINAL Aether report after last HOT
+
+## Delivery / lifecycle
+
+| Item | Result |
+|------|--------|
+| FULL messages recovered | **5/5** |
+| HOT measurement records | **249/250** (outer 5 missing last pending = HOT#50 flush via FINAL) |
+| FINAL message | **not received** (FINAL Aether phase stuck/retried; campaign timed out) |
+| callback_seen (HOT) | **246** |
+| callback_timeout (HOT) | **3** |
+| authmode=3 | **249/249** HOT |
+| brownout boots | **0** |
+| unexpected resets (payload) | **0** |
+
+Prepared `sendto` lifecycle on device is independent of UDP delivery; UDP losses are expected.
+
+## FULL USER CYCLE (n=5)
+
+Raw (ms): 12670, 3640, 4310, 3600, 3620
+
+| | ms |
+|--|-----|
+| min | 3600 |
+| **median** | **3640** |
+| p90 | 12670 |
+| max | 12670 |
+
+(First FULL includes colder registration-adjacent work; later FULLs ~3.6–4.3 s.)
+
+## HOT USER CYCLE (n=249)
+
+| | ms |
+|--|-----|
+| min | ~(from samples) |
+| **median** | **250** |
+| p90 | 320 |
+| p99 | 1090 |
+| max | 1370 |
+
+## HOT WIFI CYCLE (n=249)
+
+| | ms |
+|--|-----|
+| **median** | **239** |
+| p90 | 309 |
+| p99 | 1079 |
+| max | 1359 |
+
+## CONNECT / TX-DONE / TEARDOWN (HOT)
+
+| | med ms | p90 ms | max ms |
+|--|--------|--------|--------|
+| connect | 133 | 188 | 1293 |
+| tx-done wait | 11 | 23 | 100 |
+| teardown | 77 | 97 | 967 |
+
+## SLEEP → APP overhead
+
+`sleep_elapsed_to_app_us` median ≈ **3040.7 ms** (requested 3000 ms)
+
+| | med ms | p90 | p99 | max |
+|--|--------|------|-----|-----|
+| sleep_to_app_overhead | **40.7** | 40.8 | 40.8 | 40.9 |
+| app_entry_esp_timer_us | **5424** | 5424 | — | 5425 |
+
+## Brownout / reset
+
+| | count |
+|--|-------|
+| ESP_RST_BROWNOUT boots (payload flag) | 0 |
+| unexpected_reset_count in payloads | 0 |
+| recovery FULL (not observed in TSV) | 0 |
+
+## Confirmation
+
+- Five FULL blocks: **yes** (records for outer 1..5)
+- 50 prepared nonces per block: **yes** (HOT counts 50/50/50/50/49 recovered; last pending needs FINAL)
+- Total prepared sends targeted 250: device completed through HOT#46–50 of outer 5; **249** timing records recovered via pending chain
+
+## Artifacts
+
+- `experiments/prepared_deepsleep_5x50.tsv`
+- `experiments/run_deepsleep_5x50.py`
+- `main/prepared_deepsleep_5x50_bench.cpp`
+- `sdkconfig.defaults.deepsleep_5x50`
+
+## Note on FINAL
+
+FINAL Æther write after HOT#50 outer 5 did not reach the receiver before the orchestrator timeout. A fail-counter fallback to DONE was added for subsequent runs. Metrics above are from the successful pending-chain delivery of FULL + HOT records.
diff --git a/experiments/deepsleep_5x50_chat.txt b/experiments/deepsleep_5x50_chat.txt
new file mode 100644
index 0000000..682c328
--- /dev/null
+++ b/experiments/deepsleep_5x50_chat.txt
@@ -0,0 +1,42 @@
+[OUTER 1/5]
+full_user_ms=12669
+hot_sendto=50/50
+receiver_hot=49/50
+hot_user_median_ms=260
+wake_overhead_median_ms=40
+callback_seen_sum=49 timeouts_sum=0
+brownout=0
+unexpected_reset=0
+remaining=4
+NEXT:
+FULL 2/5
+
+
+[OUTER 3/5]
+full_user_ms=4309
+hot_sendto=50/50
+receiver_hot=49/50
+hot_user_median_ms=250
+wake_overhead_median_ms=40
+callback_seen_sum=48 timeouts_sum=1
+brownout=0
+unexpected_reset=0
+remaining=2
+NEXT:
+FULL 4/5
+
+
+[OUTER 4/5]
+full_user_ms=3599
+hot_sendto=50/50
+receiver_hot=43/50
+hot_user_median_ms=250
+wake_overhead_median_ms=40
+callback_seen_sum=43 timeouts_sum=0
+brownout=0
+unexpected_reset=0
+remaining=1
+NEXT:
+FULL 5/5
+
+  17:[18:26:09.012827]:kInfo:kAeActions:ping.cpp:71:kPing:Ping action created to server id: 21, interval: 0:00:01.000000s, rx_window: 0:00:0
diff --git a/experiments/prepared_deepsleep_5x50.tsv b/experiments/prepared_deepsleep_5x50.tsv
new file mode 100644
index 0000000..02e354a
--- /dev/null
+++ b/experiments/prepared_deepsleep_5x50.tsv
@@ -0,0 +1,255 @@
+record_id	kind	outer	hot	user_us	wifi_us	connect_us	txdone_us	teardown_us	sleep_elapsed_us	sleep_overhead_us	app_entry_us	cb_seen	cb_timeout	brownout	auth	seq
+1	1	1	0	12669799	12669799	0	0	0	3040747	40747	5424	0	0	0	0	2
+2	2	1	1	390271	378793	213437	70941	74685	3040704	40704	5424	1	0	0	3	3
+3	2	1	2	250270	238789	120887	3882	93147	3040709	40709	5424	1	0	0	3	4
+4	2	1	3	250270	238786	118646	21314	75711	3040690	40690	5424	1	0	0	3	5
+5	2	1	4	240272	228785	114186	21320	75706	3040699	40699	5424	1	0	0	3	6
+6	2	1	5	230275	218784	118151	7158	79884	3040700	40700	5424	1	0	0	3	7
+7	2	1	6	260275	248781	132523	3315	93726	3040685	40685	5424	1	0	0	3	8
+8	2	1	7	240270	228774	128190	3664	83365	3040679	40679	5424	1	0	0	3	9
+9	2	1	8	260277	248778	146583	8401	78643	3040680	40680	5424	1	0	0	3	10
+10	2	1	9	290271	278770	169141	8263	78735	3040737	40737	5424	1	0	0	3	11
+11	2	1	10	260274	248769	136564	19629	77373	3040693	40693	5424	1	0	0	3	12
+12	2	1	11	250270	238763	154683	5448	61584	3040681	40681	5424	1	0	0	3	13
+13	2	1	12	270277	258766	151717	3400	83646	3040729	40729	5424	1	0	0	3	14
+14	2	1	13	300277	288764	187893	11605	75119	3040736	40736	5424	1	0	0	3	15
+15	2	1	14	240276	228759	137728	15516	61284	3040667	40667	5424	1	0	0	3	16
+16	2	1	15	350272	338752	143274	85645	89994	3040724	40724	5424	1	0	0	3	17
+17	2	1	16	280280	268758	174425	361	76712	3040717	40717	5424	1	0	0	3	18
+18	2	1	17	270277	258751	145698	123	96909	3040737	40737	5424	1	0	0	3	19
+19	2	1	18	320275	308748	190673	17514	79448	3040710	40710	5424	1	0	0	3	20
+20	2	1	19	280276	268747	117796	35867	101071	3040715	40715	5424	1	0	0	3	21
+21	2	1	20	310271	298738	192100	12041	73674	3040724	40724	5424	1	0	0	3	22
+23	2	1	22	290278	278738	165958	18234	78569	3040676	40676	5424	1	0	0	3	24
+24	2	1	23	240276	228733	118761	5593	80109	3040720	40720	5424	1	0	0	3	25
+25	2	1	24	330276	318731	159784	130	136908	3040729	40729	5424	1	0	0	3	26
+26	2	1	25	270274	258725	145056	24156	72632	3040737	40737	5424	1	0	0	3	27
+27	2	1	26	230278	218727	124751	14863	61950	3040736	40736	5424	1	0	0	3	28
+28	2	1	27	420270	408717	299717	14679	72350	3040759	40759	5424	1	0	0	3	29
+29	2	1	28	280278	268722	131241	12224	104822	3040712	40712	5424	1	0	0	3	30
+30	2	1	29	240277	228854	146012	9444	57508	3040705	40705	5424	1	0	0	3	31
+31	2	1	30	440272	428849	363135	984	46045	3040773	40773	5424	1	0	0	3	32
+32	2	1	31	280274	268722	172298	3746	73261	3040711	40711	5424	1	0	0	3	33
+33	2	1	32	310277	298712	129790	5046	140765	3040758	40758	5424	1	0	0	3	34
+34	2	1	33	300274	288706	175126	12651	84298	3040737	40737	5424	1	0	0	3	35
+35	2	1	34	390271	378701	226430	25126	111802	3040752	40752	5424	1	0	0	3	36
+36	2	1	35	280279	268705	182186	13011	53873	3040764	40764	5424	1	0	0	3	37
+37	2	1	36	230278	218701	121716	645	76395	3040713	40713	5424	1	0	0	3	38
+38	2	1	37	290270	278690	123816	14280	122748	3040694	40694	5424	1	0	0	3	39
+39	2	1	38	240277	228694	123274	15322	71563	3040721	40721	5424	1	0	0	3	40
+40	2	1	39	260278	248692	140790	1242	85657	3040733	40733	5424	1	0	0	3	41
+41	2	1	40	240271	228682	123450	6319	80673	3040750	40750	5424	1	0	0	3	42
+42	2	1	41	220273	208681	124833	10993	55839	3040696	40696	5424	1	0	0	3	43
+43	2	1	42	280259	268665	144574	47225	59687	3040753	40753	5424	1	0	0	3	44
+44	2	1	43	230278	218681	118721	10817	64818	3040681	40681	5424	1	0	0	3	45
+45	2	1	44	240272	228672	133232	13735	63213	3040701	40701	5424	1	0	0	3	46
+46	2	1	45	250257	238654	138375	12658	63576	3040689	40689	5424	1	0	0	3	47
+47	2	1	46	230273	218668	132611	11842	55103	3040703	40703	5424	1	0	0	3	48
+48	2	1	47	270275	258666	149979	6058	80949	3040741	40741	5424	1	0	0	3	49
+49	2	1	48	250277	238665	126814	14677	82147	3040749	40749	5424	1	0	0	3	50
+50	2	1	49	230277	218662	119817	12076	64876	3040724	40724	5424	1	0	0	3	51
+51	2	1	50	240277	228659	140980	8264	58739	3040710	40710	5424	1	0	0	3	52
+52	1	2	0	3639803	3639803	0	0	0	3040781	40781	5424	0	0	0	0	53
+53	2	2	1	230263	218640	140568	546	56445	3040708	40708	5424	1	0	0	3	54
+54	2	2	2	310276	298650	131453	125	146909	3040747	40747	5424	1	0	0	3	55
+55	2	2	3	310275	298646	188526	6246	79498	3040760	40760	5424	1	0	0	3	56
+56	2	2	4	250277	238645	133688	6618	79029	3040737	40737	5424	1	0	0	3	57
+57	2	2	5	250273	238638	123838	17539	79251	3040723	40723	5424	1	0	0	3	58
+58	2	2	6	220275	208637	124152	12230	54725	3040696	40696	5424	1	0	0	3	59
+59	2	2	7	270263	258624	144756	17661	79254	3040724	40724	5424	1	0	0	3	60
+60	2	2	8	240277	228633	122446	14140	72698	3040717	40717	5424	1	0	0	3	61
+61	2	2	9	250277	238630	131405	10028	76925	3040742	40742	5424	1	0	0	3	62
+62	2	2	10	250279	238630	129248	11236	74458	3040703	40703	5424	1	0	0	3	63
+63	2	2	11	240263	228612	123207	17667	69140	3040633	40633	5424	1	0	0	3	64
+64	2	2	12	230275	218620	123487	12321	64629	3040710	40710	5424	1	0	0	3	65
+65	2	2	13	260275	248617	140899	11375	74329	3040709	40709	5424	1	0	0	3	66
+66	2	2	14	240277	228616	125233	13865	72946	3040717	40717	5424	1	0	0	3	67
+67	2	2	15	300273	288609	129551	8672	126963	3040730	40730	5424	1	0	0	3	68
+68	2	2	16	240274	228746	130403	533	76508	3040724	40724	5424	1	0	0	3	69
+69	2	2	17	250274	238741	132630	556	86485	3040731	40731	5424	1	0	0	3	70
+70	2	2	18	290275	278739	184773	13481	63324	3040723	40723	5424	1	0	0	3	71
+71	2	2	19	260273	248734	141919	10008	76921	3040735	40735	5424	1	0	0	3	72
+72	2	2	20	230273	218732	129083	7594	58110	3040744	40744	5424	1	0	0	3	73
+73	2	2	21	250276	238733	122482	126	96918	3040724	40724	5424	1	0	0	3	74
+74	2	2	22	270275	258728	158340	1293	75710	3040753	40753	5424	1	0	0	3	75
+75	2	2	23	220277	208727	121934	12195	54758	3040670	40670	5424	1	0	0	3	76
+76	2	2	24	250275	238722	147248	14159	62645	3040738	40738	5424	1	0	0	3	77
+77	2	2	25	300275	288719	144260	16166	110720	3040773	40773	5424	1	0	0	3	78
+78	2	2	26	300277	288718	193419	11384	65438	3040763	40763	5424	1	0	0	3	79
+79	2	2	27	240273	228711	143914	10779	56164	3040767	40767	5424	1	0	0	3	80
+80	2	2	28	270273	258710	151563	8908	76773	3040715	40715	5424	1	0	0	3	81
+81	2	2	29	240275	228707	125173	13280	73524	3040753	40753	5424	1	0	0	3	82
+82	2	2	30	240277	228707	132767	13403	63406	3040723	40723	5424	1	0	0	3	83
+83	2	2	31	240275	228703	140902	3867	61772	3040708	40708	5424	1	0	0	3	84
+84	2	2	32	250275	238699	146435	15327	61386	3040724	40724	5424	1	0	0	3	85
+86	2	2	34	1090297	1078716	233982	100004	726942	3040815	40815	5424	0	1	0	3	87
+88	2	2	36	260273	248685	137716	21258	75677	3040724	40724	5424	1	0	0	3	89
+89	2	2	37	250266	238675	141839	4209	72786	3040731	40731	5424	1	0	0	3	90
+90	2	2	38	240281	228687	119324	8775	76938	3040780	40780	5424	1	0	0	3	91
+91	2	2	39	230277	218681	128921	9464	57488	3040730	40730	5424	1	0	0	3	92
+92	2	2	40	260275	248677	146891	15193	71726	3040737	40737	5424	1	0	0	3	93
+93	2	2	41	270273	258671	133349	22546	84246	3040692	40692	5424	1	0	0	3	94
+94	2	2	42	330277	318672	214803	11605	75349	3040759	40759	5424	1	0	0	3	95
+95	2	2	43	260277	248669	130950	10860	86088	3040726	40726	5424	1	0	0	3	96
+96	2	2	44	280271	268660	123140	8609	118329	3040759	40759	5424	1	0	0	3	97
+97	2	2	45	310275	298662	189280	11111	75840	3040751	40751	5424	1	0	0	3	98
+98	2	2	46	250273	238656	127953	18060	78738	3040699	40699	5424	1	0	0	3	99
+99	2	2	47	250275	238655	120096	9218	86547	3040709	40709	5424	1	0	0	3	100
+103	1	3	0	4309804	4309804	0	0	0	3040875	40875	5424	0	0	0	0	104
+104	2	3	1	230270	218636	121576	4004	73025	3040744	40744	5424	1	0	0	3	105
+105	2	3	2	240274	228637	127416	220	86815	3040758	40758	5424	1	0	0	3	106
+106	2	3	3	250251	238611	132674	2411	84605	3040736	40736	5424	1	0	0	3	107
+107	2	3	4	250274	238631	130160	188	86853	3040709	40709	5424	1	0	0	3	108
+108	2	3	5	240274	228628	143671	3635	63390	3040744	40744	5424	1	0	0	3	109
+109	2	3	6	340275	328626	177858	21621	115303	3040742	40742	5424	1	0	0	3	110
+110	2	3	7	260274	248622	132215	10277	86765	3040717	40717	5424	1	0	0	3	111
+111	2	3	8	250274	238620	131650	203	86838	3040724	40724	5424	1	0	0	3	112
+112	2	3	9	230270	218613	123849	4235	72794	3040765	40765	5424	1	0	0	3	113
+113	2	3	10	260274	248614	139627	135	86906	3040767	40767	5424	1	0	0	3	114
+114	2	3	11	310277	298614	185450	18384	78544	3040758	40758	5424	1	0	0	3	115
+115	2	3	12	230281	218615	126423	12344	64462	3040756	40756	5424	1	0	0	3	116
+116	2	3	13	260277	248608	135076	18761	78167	3040740	40740	5424	1	0	0	3	117
+117	2	3	14	230275	218603	128101	3799	61925	3040741	40741	5424	1	0	0	3	118
+118	2	3	15	260262	248587	154729	11707	65278	3040720	40720	5424	1	0	0	3	119
+119	2	3	16	260275	248597	132881	17700	79244	3040772	40772	5424	1	0	0	3	120
+121	2	3	18	1310300	1298616	210088	100002	966983	3040825	40825	5424	0	1	0	3	122
+122	2	3	19	230256	218569	142061	375	56650	3040734	40734	5424	1	0	0	3	123
+123	2	3	20	330278	318589	209785	7170	78537	3040780	40780	5424	1	0	0	3	124
+124	2	3	21	250279	238588	131115	6538	79164	3040738	40738	5424	1	0	0	3	125
+125	2	3	22	250257	238563	131389	16367	70620	3040737	40737	5424	1	0	0	3	126
+126	2	3	23	240273	228575	120336	11299	75594	3040747	40747	5424	1	0	0	3	127
+127	2	3	24	250280	238579	129844	7922	77720	3040722	40722	5424	1	0	0	3	128
+128	2	3	25	300274	288596	178108	125	86922	3040788	40788	5424	1	0	0	3	129
+129	2	3	26	250274	238741	137761	128	86913	3040712	40712	5424	1	0	0	3	130
+130	2	3	27	300279	288743	173848	17638	79294	3040742	40742	5424	1	0	0	3	131
+131	2	3	28	330270	318732	196577	3700	103321	3040781	40781	5424	1	0	0	3	132
+132	2	3	29	1370258	1358717	1293093	1323	45723	3040859	40859	5424	1	0	0	3	133
+133	2	3	30	280272	268696	142366	126	106915	3040719	40719	5424	1	0	0	3	134
+134	2	3	31	240276	228661	122472	12296	74540	3040751	40751	5424	1	0	0	3	135
+135	2	3	32	240273	228656	130570	14056	62551	3040748	40748	5424	1	0	0	3	136
+136	2	3	33	260265	248645	138822	6583	79096	3040726	40726	5424	1	0	0	3	137
+137	2	3	34	250278	238654	128801	8549	77110	3040732	40732	5424	1	0	0	3	138
+138	2	3	35	240276	228649	125411	15308	71492	3040667	40667	5424	1	0	0	3	139
+139	2	3	36	230276	218647	123751	13216	63589	3040758	40758	5424	1	0	0	3	140
+140	2	3	37	280278	268646	151513	14066	82936	3040731	40731	5424	1	0	0	3	141
+141	2	3	38	240275	228640	141346	127	66906	3040765	40765	5424	1	0	0	3	142
+142	2	3	39	240272	228634	121244	14541	72075	3040730	40730	5424	1	0	0	3	143
+143	2	3	40	230258	218617	125377	127	76890	3040727	40727	5424	1	0	0	3	144
+144	2	3	41	340276	328632	141141	79052	87741	3040779	40779	5424	1	0	0	3	145
+145	2	3	42	270277	258631	156626	15887	71033	3040734	40734	5424	1	0	0	3	146
+146	2	3	43	260277	248628	128394	16115	80696	3040731	40731	5424	1	0	0	3	147
+147	2	3	44	240278	228625	119164	8275	78679	3040747	40747	5424	1	0	0	3	148
+148	2	3	45	230276	218622	128045	2837	74207	3040755	40755	5424	1	0	0	3	149
+149	2	3	46	220275	208617	121258	125	66916	3040751	40751	5424	1	0	0	3	150
+150	2	3	47	240275	228614	124824	1138	85904	3040683	40683	5424	1	0	0	3	151
+151	2	3	48	250270	238607	142738	5138	71893	3040785	40785	5424	1	0	0	3	152
+152	2	3	49	250270	238604	128270	14754	72276	3040727	40727	5424	1	0	0	3	153
+153	2	3	50	240275	228605	125544	545	86495	3040723	40723	5424	1	0	0	3	154
+154	1	4	0	3599804	3599804	0	0	0	3040743	40743	5424	0	0	0	0	155
+155	2	4	1	330274	318599	219698	7892	68716	3040780	40780	5424	1	0	0	3	156
+156	2	4	2	280260	268581	157703	879	96138	3040736	40736	5424	1	0	0	3	157
+157	2	4	3	300276	288594	193637	12696	64022	3040702	40702	5424	1	0	0	3	158
+158	2	4	4	260276	248593	136570	17074	79863	3040748	40748	5424	1	0	0	3	159
+159	2	4	5	250264	238577	125102	16108	80803	3040747	40747	5424	1	0	0	3	160
+160	2	4	6	290277	278587	123568	13581	123339	3040786	40786	5424	1	0	0	3	161
+161	2	4	7	250277	238584	133275	250	86796	3040741	40741	5424	1	0	0	3	162
+162	2	4	8	360252	348581	239275	17926	69091	3040737	40737	5424	1	0	0	3	163
+163	2	4	9	260272	248851	136106	11038	85989	3040752	40752	5424	1	0	0	3	164
+164	2	4	10	280277	268855	162473	11854	74974	3040734	40734	5424	1	0	0	3	165
+165	2	4	11	250277	238855	130558	12111	73552	3040723	40723	5424	1	0	0	3	166
+166	2	4	12	250275	238852	125505	19219	77706	3040719	40719	5424	1	0	0	3	167
+170	2	4	16	250277	238850	129439	10975	75834	3040719	40719	5424	1	0	0	3	171
+171	2	4	17	330279	318852	128984	79330	86342	3040787	40787	5424	1	0	0	3	172
+172	2	4	18	220272	208844	117886	12151	64876	3040744	40744	5425	1	0	0	3	173
+173	2	4	19	240273	228845	128447	232	76808	3040730	40730	5424	1	0	0	3	174
+174	2	4	20	410275	398845	182873	13281	183522	3040761	40761	5424	1	0	0	3	175
+175	2	4	21	220267	208836	124605	864	66152	3040737	40737	5424	1	0	0	3	176
+176	2	4	22	320275	308844	148278	22348	124576	3040779	40779	5424	1	0	0	3	177
+178	2	4	24	280279	268846	162134	5943	81064	3040741	40741	5424	1	0	0	3	179
+179	2	4	25	240275	228841	118465	10761	76189	3040666	40666	5424	1	0	0	3	180
+180	2	4	26	300273	288839	119039	71516	75296	3040734	40734	5424	1	0	0	3	181
+181	2	4	27	250272	238836	133939	9906	77117	3040759	40759	5424	1	0	0	3	182
+182	2	4	28	270274	258838	167635	695	76344	3040740	40740	5424	1	0	0	3	183
+183	2	4	29	270280	258842	149743	11183	74457	3040718	40718	5424	1	0	0	3	184
+184	2	4	30	240277	228839	119986	14769	72034	3040743	40743	5424	1	0	0	3	185
+185	2	4	31	260282	248843	132906	40913	54788	3040762	40762	5424	1	0	0	3	186
+186	2	4	32	240274	228834	120704	11553	74091	3040725	40725	5424	1	0	0	3	187
+187	2	4	33	240277	228836	125850	12506	74308	3040724	40724	5424	1	0	0	3	188
+188	2	4	34	250277	238835	128382	5322	80367	3040750	40750	5424	1	0	0	3	189
+189	2	4	35	330270	318828	172681	41360	85673	3040741	40741	5424	1	0	0	3	190
+193	2	4	39	300284	288838	180269	6345	79367	3040780	40780	5424	1	0	0	3	194
+194	2	4	40	220274	208827	125547	2881	64158	3040746	40746	5424	1	0	0	3	195
+195	2	4	41	250272	238824	123230	11825	85211	3040644	40644	5424	1	0	0	3	196
+196	2	4	42	270271	258823	140274	11770	84952	3040744	40744	5424	1	0	0	3	197
+197	2	4	43	240273	228825	118829	8757	78118	3040711	40711	5424	1	0	0	3	198
+198	2	4	44	250275	238825	123677	24211	72729	3040725	40725	5424	1	0	0	3	199
+199	2	4	45	240277	228826	117131	19254	77673	3040737	40737	5424	1	0	0	3	200
+200	2	4	46	250275	238823	134544	5536	80114	3040731	40731	5424	1	0	0	3	201
+201	2	4	47	240275	228823	119772	15291	71511	3040717	40717	5424	1	0	0	3	202
+202	2	4	48	320272	308819	161439	39044	87984	3040766	40766	5424	1	0	0	3	203
+203	2	4	49	250272	238818	123520	11579	85450	3040786	40786	5424	1	0	0	3	204
+204	2	4	50	250274	238819	155909	2421	64620	3040743	40743	5424	1	0	0	3	205
+205	1	5	0	3619804	3619804	0	0	0	3040740	40740	5424	0	0	0	0	206
+206	2	5	1	240270	228813	126090	9345	77690	3040765	40765	5424	1	0	0	3	207
+207	2	5	2	290261	278804	166622	28103	68801	3040737	40737	5424	1	0	0	3	208
+208	2	5	3	260273	248816	128562	7977	88685	3040729	40729	5424	1	0	0	3	209
+209	2	5	4	220277	208818	126516	3491	63516	3040748	40748	5424	1	0	0	3	210
+210	2	5	5	260275	248815	137447	18096	78830	3040758	40758	5424	1	0	0	3	211
+212	2	5	7	250277	238815	144582	2232	74711	3040751	40751	5424	1	0	0	3	213
+213	2	5	8	250275	238814	128747	4915	80723	3040751	40751	5424	1	0	0	3	214
+214	2	5	9	220263	208801	124346	15244	51670	3040736	40736	5424	1	0	0	3	215
+215	2	5	10	230276	218812	125444	126	76913	3040757	40757	5424	1	0	0	3	216
+216	2	5	11	300275	288810	166141	26352	80570	3040688	40688	5424	1	0	0	3	217
+217	2	5	12	250278	238813	155919	2516	64520	3040748	40748	5424	1	0	0	3	218
+218	2	5	13	330275	318808	209693	13116	73711	3040726	40726	5424	1	0	0	3	219
+219	2	5	14	250269	238801	143207	1664	75267	3040759	40759	5424	1	0	0	3	220
+220	2	5	15	240261	228794	124935	20411	66498	3040765	40765	5424	1	0	0	3	221
+221	2	5	16	250279	238811	142604	4205	71439	3040732	40732	5424	1	0	0	3	222
+222	2	5	17	250277	238807	129185	10616	76187	3040753	40753	5424	1	0	0	3	223
+223	2	5	18	240276	228805	130604	12100	63572	3040732	40732	5424	1	0	0	3	224
+224	2	5	19	260275	248803	136212	23079	73841	3040718	40718	5424	1	0	0	3	225
+225	2	5	20	320270	308823	191929	6002	91038	3040737	40737	5424	1	0	0	3	226
+226	2	5	21	250278	238856	121095	4025	93015	3040765	40765	5424	1	0	0	3	227
+227	2	5	22	290277	278855	146066	17894	99032	3040737	40737	5424	1	0	0	3	228
+228	2	5	23	340279	328856	209744	6947	88748	3040775	40775	5424	1	0	0	3	229
+229	2	5	24	240277	228853	116772	23613	73313	3040738	40738	5424	1	0	0	3	230
+230	2	5	25	250263	238839	121636	18445	78478	3040744	40744	5424	1	0	0	3	231
+231	2	5	26	300274	288849	171045	1303	95736	3040748	40748	5424	1	0	0	3	232
+232	2	5	27	300271	288845	179249	6302	80622	3040758	40758	5424	1	0	0	3	233
+233	2	5	28	350275	338848	220106	13843	82955	3040751	40751	5424	1	0	0	3	234
+234	2	5	29	250271	238843	122074	15759	81045	3040750	40750	5424	1	0	0	3	235
+235	2	5	30	230276	218847	113987	1821	85221	3040719	40719	5424	1	0	0	3	236
+236	2	5	31	250273	238843	128890	13631	73168	3040765	40765	5424	1	0	0	3	237
+237	2	5	32	250275	238845	130668	12835	73767	3040745	40745	5424	1	0	0	3	238
+238	2	5	33	230277	218846	133217	4341	61328	3040764	40764	5424	1	0	0	3	239
+239	2	5	34	240274	228842	137232	555	76486	3040760	40760	5424	1	0	0	3	240
+240	2	5	35	240272	228839	115035	12454	84577	3040731	40731	5424	1	0	0	3	241
+241	2	5	36	280276	268843	130251	626	116413	3040731	40731	5424	1	0	0	3	242
+242	2	5	37	240276	228842	132048	10183	66729	3040744	40744	5424	1	0	0	3	243
+243	2	5	38	250277	238842	127311	21239	75689	3040739	40739	5424	1	0	0	3	244
+244	2	5	39	240263	228828	124588	23722	63209	3040744	40744	5424	1	0	0	3	245
+245	2	5	40	260278	248841	140426	7506	78197	3040751	40751	5424	1	0	0	3	246
+246	2	5	41	450284	438847	219333	4905	192132	3040771	40771	5424	1	0	0	3	247
+247	2	5	42	300281	288842	132208	41165	94512	3040747	40747	5424	1	0	0	3	248
+252	2	5	47	350277	338836	224828	17020	79927	3040773	40773	5424	1	0	0	3	253
+253	2	5	48	260277	248835	124756	17313	89613	3040739	40739	5424	1	0	0	3	254
+254	2	5	49	280274	268829	160093	129	86906	3040736	40736	5424	1	0	0	3	255
+22	2	1	21	250282	238818	134667	9071	76633	3040776	40776	5424	1	0	0	3	23
+85	2	2	33	250270	238715	117085	20491	86537	3040740	40740	5424	1	0	0	3	86
+87	2	2	35	260274	248713	136592	3308	93733	3040681	40681	5424	1	0	0	3	88
+100	2	2	48	250271	238672	119246	6797	88876	3040751	40751	5424	1	0	0	3	101
+101	2	2	49	220275	208674	116943	15615	61325	3040733	40733	5424	1	0	0	3	102
+102	2	2	50	250277	238673	135726	19338	67584	3040725	40725	5424	1	0	0	3	103
+120	2	3	17	240275	228618	126910	21471	65468	3040724	40724	5424	1	0	0	3	121
+168	2	4	14	1270298	1258726	182271	100005	956947	3040815	40815	5424	0	1	0	3	169
+169	2	4	15	260273	248698	139840	13711	73227	3040743	40743	5424	1	0	0	3	170
+177	2	4	23	250261	238664	134126	14504	72481	3040738	40738	5424	1	0	0	3	178
+190	2	4	36	270276	258821	128215	5863	111181	3040730	40730	5424	1	0	0	3	191
+191	2	4	37	230270	218811	117599	4822	82207	3040731	40731	5424	1	0	0	3	192
+192	2	4	38	280277	268815	134606	39777	77140	3040761	40761	5424	1	0	0	3	193
+211	2	5	6	250262	238745	141789	12793	64145	3040732	40732	5424	1	0	0	3	212
+248	2	5	43	300274	288650	190128	3367	73658	3040704	40704	5424	1	0	0	3	249
+249	2	5	44	290277	278650	184908	11749	65087	3040769	40769	5424	1	0	0	3	250
+250	2	5	45	260275	248645	144301	25005	61813	3040753	40753	5424	1	0	0	3	251
+251	2	5	46	310280	298648	139253	15607	121407	3040775	40775	5424	1	0	0	3	252
+167	2	4	13	250275	238686	134105	23080	63839	3040767	40767	5424	1	0	0	3	168
diff --git a/experiments/run_deepsleep_5x50.py b/experiments/run_deepsleep_5x50.py
new file mode 100644
index 0000000..175e16c
--- /dev/null
+++ b/experiments/run_deepsleep_5x50.py
@@ -0,0 +1,280 @@
+"""Build/flash/monitor prepared deep-sleep 5x50 E2E (silent ESP, Æther receiver)."""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+# Reuse existing IDF-configured ESP32-C6 build tree (do not create a desktop cmake dir).
+BUILD = ROOT / "build-esp32c6-save-bench-smoke"
+AETHER = r"C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0"
+PY = Path(r"C:\Espressif\python_env\idf6.0_py3.11_env\Scripts\python.exe")
+CMAKE = Path(r"C:\Espressif\tools\cmake\3.30.2\bin\cmake.exe")
+NINJA = Path(r"C:\Espressif\tools\ninja\1.12.1\ninja.exe")
+RX_EXE = ROOT / "temperature_receiver" / "build-bisect" / "temperature_receiver.exe"
+RX_SESSION = ROOT / "experiments" / "prepared_wifi_cache_rx_session"
+RX_LOG = ROOT / "experiments" / "prepared_deepsleep_5x50_rx.log"
+TSV = ROOT / "experiments" / "prepared_deepsleep_5x50.tsv"
+PROGRESS = ROOT / "experiments" / "deepsleep_5x50_progress.log"
+CHAT = ROOT / "experiments" / "deepsleep_5x50_chat.txt"
+
+IDF_PATH = r"C:\Espressif\frameworks\esp-idf-v6.0.2"
+CCACHE = r"C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64"
+
+RESULT_RE = re.compile(r"TEST_RESULT .* BENCH_DONE deepsleep_5x50|BENCH_DONE deepsleep_5x50")
+OUTER_RE = re.compile(r"\[OUTER (?P\d+)/5\]")
+
+
+def env() -> dict:
+    e = os.environ.copy()
+    e["IDF_PATH"] = IDF_PATH
+    e["IDF_TOOLS_PATH"] = r"C:\Espressif"
+    extra = [
+        CCACHE,
+        r"C:\Espressif\tools\ninja\1.12.1",
+        r"C:\Espressif\tools\cmake\3.30.2\bin",
+        r"C:\msys64\ucrt64\bin",
+    ]
+    e["Path"] = ";".join(extra) + ";" + e.get("Path", "")
+    e.pop("CCACHE_DISABLE", None)
+    return e
+
+
+def log(msg: str) -> None:
+    line = time.strftime("%H:%M:%S") + " " + msg
+    print(line, flush=True)
+    PROGRESS.parent.mkdir(parents=True, exist_ok=True)
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(line + "\n")
+
+
+def force_sdk_fixes() -> None:
+    sdk = BUILD / "sdkconfig"
+    if not sdk.exists():
+        return
+    text = sdk.read_text(encoding="utf-8")
+    reps = [
+        ("CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_RTC_CLK_SRC_INT_RC=y", "# CONFIG_RTC_CLK_SRC_INT_RC is not set"),
+        ("# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set", "CONFIG_RTC_CLK_SRC_EXT_CRYS=y"),
+        ("CONFIG_ESP_BROWNOUT_DET=n", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("# CONFIG_ESP_BROWNOUT_DET is not set", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("CONFIG_PM_ENABLE=y", "# CONFIG_PM_ENABLE is not set"),
+    ]
+    for a, b in reps:
+        text = text.replace(a, b)
+    if "CONFIG_RTC_CLK_SRC_EXT_CRYS=y" not in text:
+        text += "\nCONFIG_RTC_CLK_SRC_EXT_CRYS=y\n"
+    sdk.write_text(text, encoding="utf-8")
+
+
+def show_effective() -> None:
+    sdk = BUILD / "sdkconfig"
+    keys = [
+        "CONFIG_RTC_CLK_SRC_EXT_CRYS",
+        "CONFIG_RTC_CLK_SRC_INT_RC",
+        "CONFIG_ESP_BROWNOUT_DET",
+        "CONFIG_ESP_BROWNOUT_DET_LVL_SEL_7",
+        "CONFIG_ESP_WIFI_ENABLE_WPA3_SAE",
+        "CONFIG_ESP_CONSOLE_NONE",
+        "CONFIG_LOG_DEFAULT_LEVEL_NONE",
+        "CONFIG_BOOTLOADER_LOG_LEVEL_NONE",
+        "CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160",
+        "CONFIG_PM_ENABLE",
+        "CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP",
+        "CONFIG_RTC_CLK_CAL_CYCLES",
+    ]
+    log("=== effective sdkconfig ===")
+    text = sdk.read_text(encoding="utf-8") if sdk.exists() else ""
+    for k in keys:
+        lines = [ln for ln in text.splitlines() if k in ln and not ln.strip().startswith("# ") or ln.startswith(f"# {k}")]
+        # simpler: grep style
+        matched = [ln for ln in text.splitlines() if k in ln]
+        for ln in matched[:3]:
+            log(ln)
+
+
+def cmake_configure() -> None:
+    args = [
+        str(CMAKE),
+        "-S",
+        str(ROOT),
+        "-B",
+        str(BUILD),
+        "-G",
+        "Ninja",
+        f"-DCPM_aether-client-cpp_SOURCE={AETHER}",
+        "-DAE_EXP_PREPARED_DEEPSLEEP_5X50=1",
+        "-DAE_EXP_PREPARED_WIFI_FASTEST=",
+        "-DAE_EXP_PREPARED_WIFI_BISECT=",
+        "-DAE_EXP_BISECT_CONSOLE=",
+        "-DAE_EXP_BISECT_SMOKE=",
+        "-DAE_EXP_SKIP_DTOR_SAVE=1",
+        "-DSERVICE_UID=5aade50f-00d9-4624-b097-e203cdcf1e38",
+        "-DBENCH_CLIENT_ID=prepared_deepsleep_5x50_v1",
+        "-DWIFI_SSID=chirkov",
+        "-DWIFI_PASSWORD=kcdjepWz51",
+        "-DCMAKE_BUILD_TYPE=Release",
+    ]
+    log("cmake configure deepsleep_5x50")
+    r = subprocess.run(args, cwd=ROOT, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "deepsleep_cmake.err").write_text(
+            r.stdout + "\n" + r.stderr, encoding="utf-8"
+        )
+        raise RuntimeError("cmake failed")
+    log("cmake ok")
+
+
+def ninja_build() -> None:
+    r = subprocess.run(
+        [str(NINJA), "-C", str(BUILD)], env=env(), capture_output=True, text=True
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "deepsleep_build.err").write_text(
+            r.stdout[-12000:] + "\n" + r.stderr[-12000:], encoding="utf-8"
+        )
+        raise RuntimeError("ninja failed")
+    log("build ok")
+
+
+def flash() -> None:
+    cmd = [
+        str(PY),
+        "-m",
+        "esptool",
+        "--chip",
+        "esp32c6",
+        "-p",
+        "COM7",
+        "-b",
+        "460800",
+        "write-flash",
+        "--flash-size",
+        "4MB",
+        "0x0",
+        str(BUILD / "bootloader" / "bootloader.bin"),
+        "0x8000",
+        str(BUILD / "partition_table" / "partition-table.bin"),
+        "0x10000",
+        str(BUILD / "temperature_sensor.bin"),
+    ]
+    r = subprocess.run(cmd, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "deepsleep_flash.err").write_text(
+            r.stdout + "\n" + r.stderr, encoding="utf-8"
+        )
+        raise RuntimeError("flash failed")
+    log("flash ok")
+
+
+def ensure_receiver() -> None:
+    out = subprocess.run(
+        ["tasklist", "/FI", "IMAGENAME eq temperature_receiver.exe"],
+        capture_output=True,
+        text=True,
+    ).stdout
+    if "temperature_receiver.exe" in out:
+        log("receiver already running")
+        return
+    RX_SESSION.mkdir(parents=True, exist_ok=True)
+    env2 = env()
+    env2["AE_RECEIVER_SESSION_DIR"] = str(RX_SESSION)
+    env2["AE_DS_TSV"] = str(TSV)
+    RX_LOG.parent.mkdir(parents=True, exist_ok=True)
+    with RX_LOG.open("a", encoding="utf-8") as outf, (
+        ROOT / "experiments" / "prepared_deepsleep_5x50_rx.log.err"
+    ).open("a", encoding="utf-8") as errf:
+        subprocess.Popen(
+            [str(RX_EXE)],
+            cwd=str(RX_SESSION),
+            env=env2,
+            stdout=outf,
+            stderr=errf,
+        )
+    time.sleep(4)
+    log("receiver started")
+
+
+def tsv_stats() -> dict:
+    if not TSV.exists():
+        return {}
+    rows = list(TSV.read_text(encoding="utf-8").splitlines())
+    if len(rows) < 2:
+        return {}
+    full = hot = 0
+    outers_full = set()
+    for line in rows[1:]:
+        parts = line.split("\t")
+        if len(parts) < 3:
+            continue
+        kind = parts[1]
+        outer = parts[2]
+        if kind == "1":
+            full += 1
+            outers_full.add(outer)
+        elif kind == "2":
+            hot += 1
+    return {"full": full, "hot": hot, "outers": outers_full}
+
+
+def wait_done(timeout_s: int = 1800) -> str:
+    deadline = time.time() + timeout_s
+    last_len = 0
+    last_outer = 0
+    while time.time() < deadline:
+        if RX_LOG.exists():
+            text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+            if len(text) != last_len:
+                last_len = len(text)
+                for m in OUTER_RE.finditer(text):
+                    o = int(m.group("o"))
+                    if o > last_outer:
+                        last_outer = o
+                        idx = m.start()
+                        block = text[idx : idx + 350]
+                        with CHAT.open("a", encoding="utf-8") as f:
+                            f.write(block + "\n")
+                        print(block, flush=True)
+                if "BENCH_DONE deepsleep_5x50" in text:
+                    for line in reversed(text.splitlines()):
+                        if line.startswith("TEST_RESULT"):
+                            return line
+                    return "BENCH_DONE"
+        st = tsv_stats()
+        # Complete enough without FINAL: 5 FULL + >=245 HOT
+        if st.get("full", 0) >= 5 and st.get("hot", 0) >= 245:
+            log(f"TSV complete enough full={st['full']} hot={st['hot']}")
+            return f"TSV_COMPLETE full={st['full']} hot={st['hot']}"
+        time.sleep(5)
+    raise TimeoutError("no BENCH_DONE")
+
+
+def main() -> int:
+    CHAT.write_text("", encoding="utf-8")
+    if TSV.exists():
+        TSV.unlink()
+    ensure_receiver()
+    cmake_configure()
+    force_sdk_fixes()
+    show_effective()
+    ninja_build()
+    force_sdk_fixes()
+    show_effective()
+    flash()
+    log("waiting for 5x50 deepsleep run (~15-25 min)...")
+    result = wait_done(2400)
+    log("RESULT " + result)
+    with CHAT.open("a", encoding="utf-8") as f:
+        f.write(result + "\n")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 7612562..e699a0a 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -33,6 +33,12 @@ elseif(AE_EXP_PREPARED_WIFI_FASTEST)
     "prepared_wifi_fastest_path_bench.cpp"
     "prepared_send/prepared_send.cpp"
   )
+elseif(AE_EXP_PREPARED_DEEPSLEEP_5X50)
+  list(APPEND src_list
+    "prepared_deepsleep_5x50_bench.cpp"
+    "experiment_early_entry.cpp"
+    "prepared_send/prepared_send.cpp"
+  )
 elseif(AE_EXP_PREPARED_WIFI_BISECT)
   list(APPEND src_list
     "prepared_wifi_single_factor_bisect_bench.cpp"
@@ -172,6 +178,7 @@ set(AE_EXP_PREPARED_WIFI_CACHE_5X20 "" CACHE STRING "Silent 5x20 prepared Wi-Fi
 set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 "" CACHE STRING "Silent 5x20 keep-Wi-Fi-up prepared bench (set to 1)")
 set(AE_EXP_PREPARED_WIFI_BISECT "" CACHE STRING "Silent single-factor prepared Wi-Fi bisect (set to 1)")
 set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING "Silent fastest-path prepared campaign (set to 1)")
+set(AE_EXP_PREPARED_DEEPSLEEP_5X50 "" CACHE STRING "Silent deep-sleep 5x50 prepared E2E (set to 1)")
 set(AE_EXP_FAST_N "" CACHE STRING "Fastest-path prepared count")
 set(AE_EXP_FAST_TEST_ID "" CACHE STRING "Fastest-path test id")
 set(AE_EXP_FAST_PRE_MS "" CACHE STRING "Fastest-path pre-send delay ms")
@@ -225,6 +232,7 @@ ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_CACHE_5X20)
 ae_exp_define_if_set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20)
 ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_BISECT)
 ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_FASTEST)
+ae_exp_define_if_set(AE_EXP_PREPARED_DEEPSLEEP_5X50)
 ae_exp_define_if_set(AE_EXP_FAST_N)
 ae_exp_define_if_set(AE_EXP_FAST_TEST_ID)
 ae_exp_define_if_set(AE_EXP_FAST_PRE_MS)
@@ -241,6 +249,7 @@ ae_exp_define_if_set(AE_EXP_BISECT_SMOKE)
 if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR
    AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1" OR
    AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1" OR
+   AE_EXP_PREPARED_DEEPSLEEP_5X50 STREQUAL "1" OR
    (AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1" AND
     NOT AE_EXP_BISECT_CONSOLE STREQUAL "1"))
   target_compile_definitions(aether PUBLIC "AE_EXP_SILENT=1")
diff --git a/main/bench_payload.h b/main/bench_payload.h
index 2c1b28d..2ae038f 100644
--- a/main/bench_payload.h
+++ b/main/bench_payload.h
@@ -288,6 +288,78 @@ inline bool DecodeFast(Buffer const& data, FastPayload& out) {
   return out.magic == kFastMagic;
 }
 
+static constexpr std::uint8_t kDsMagic = 0xD5;
+
+enum class DsMsgType : std::uint8_t {
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+  kRecovery = 4,
+};
+
+enum class DsFlags : std::uint8_t {
+  kBrownout = 1 << 0,
+  kCallbackSeen = 1 << 1,
+  kCallbackTimeout = 1 << 2,
+  kCacheValid = 1 << 3,
+  kStateValid = 1 << 4,
+};
+
+enum class DsPendingKind : std::uint8_t {
+  kNone = 0,
+  kFull = 1,
+  kHot = 2,
+};
+
+#pragma pack(push, 1)
+struct DsPayload {
+  std::uint8_t magic{kDsMagic};
+  std::uint8_t type{0};
+  std::uint8_t outer_cycle{0};
+  std::uint8_t hot_index{0};
+  std::uint16_t sequence_global{0};
+  std::uint16_t record_id{0};
+  std::uint8_t reset_reason{0};
+  std::uint8_t wake_cause{0};
+  std::uint8_t flags{0};
+  std::uint8_t brownout_count{0};
+  std::uint8_t unexpected_reset_count{0};
+  std::uint8_t negotiated_auth{0};
+  std::uint32_t requested_sleep_us{0};
+  std::uint32_t sleep_elapsed_to_app_us{0};
+  std::uint32_t sleep_to_app_overhead_us{0};
+  std::uint32_t app_entry_esp_timer_us{0};
+  std::uint32_t pending_user_cycle_us{0};
+  std::uint32_t pending_wifi_cycle_us{0};
+  std::uint32_t connect_us{0};
+  std::uint32_t tx_done_wait_us{0};
+  std::uint32_t teardown_us{0};
+  std::uint16_t prepared_message_left{0};
+  std::uint8_t pending_kind{0};
+  std::uint8_t pending_outer{0};
+  std::uint8_t pending_hot_index{0};
+  std::uint8_t reserved{0};
+};
+#pragma pack(pop)
+
+static_assert(sizeof(DsPayload) == 56, "ds payload size");
+
+template 
+inline Buffer EncodeDs(DsPayload const& p) {
+  Buffer out(sizeof(DsPayload));
+  std::memcpy(out.data(), &p, sizeof(DsPayload));
+  return out;
+}
+
+template 
+inline bool DecodeDs(Buffer const& data, DsPayload& out) {
+  if (data.size() < sizeof(DsPayload)) {
+    return false;
+  }
+  std::memcpy(&out, data.data(), sizeof(DsPayload));
+  return out.magic == kDsMagic;
+}
+
 }  // namespace temp_sensor::bench
 
 #endif  // TEMP_SENSOR_BENCH_PAYLOAD_H_
diff --git a/main/experiment_early_entry.cpp b/main/experiment_early_entry.cpp
new file mode 100644
index 0000000..1ab4c39
--- /dev/null
+++ b/main/experiment_early_entry.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Aethernet Inc.
+ *
+ * Early app_main capture for AE_EXP_PREPARED_DEEPSLEEP_5X50.
+ */
+
+#include "experiment_early_entry.h"
+
+#if defined(ESP_PLATFORM) && defined(AE_EXP_PREPARED_DEEPSLEEP_5X50)
+
+#  include 
+#  include 
+#  include 
+
+extern "C" std::uint64_t esp_rtc_get_time_us(void);
+
+namespace {
+ExperimentEarlyEntrySnapshot g_early{};
+}
+
+extern "C" void ExperimentEarlyAppEntry() {
+  g_early.app_entry_esp_timer_us = esp_timer_get_time();
+  g_early.app_entry_rtc_us = esp_rtc_get_time_us();
+  g_early.reset_reason = static_cast(esp_reset_reason());
+  g_early.wakeup_cause =
+      static_cast(esp_sleep_get_wakeup_cause());
+  g_early.valid = 1;
+}
+
+ExperimentEarlyEntrySnapshot const& GetExperimentEarlyEntrySnapshot() {
+  return g_early;
+}
+
+#endif
diff --git a/main/experiment_early_entry.h b/main/experiment_early_entry.h
new file mode 100644
index 0000000..f6af911
--- /dev/null
+++ b/main/experiment_early_entry.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2026 Aethernet Inc.
+ *
+ * First-instruction app_main hook for deep-sleep timing experiments.
+ * No heap, no logging, no formatting.
+ */
+
+#ifndef TEMP_SENSOR_EXPERIMENT_EARLY_ENTRY_H_
+#define TEMP_SENSOR_EXPERIMENT_EARLY_ENTRY_H_
+
+#include 
+
+struct ExperimentEarlyEntrySnapshot {
+  std::int64_t app_entry_esp_timer_us{0};
+  std::uint64_t app_entry_rtc_us{0};
+  std::uint8_t reset_reason{0};
+  std::uint8_t wakeup_cause{0};
+  std::uint8_t valid{0};
+};
+
+#if defined(ESP_PLATFORM) && defined(AE_EXP_PREPARED_DEEPSLEEP_5X50)
+extern "C" void ExperimentEarlyAppEntry();
+ExperimentEarlyEntrySnapshot const& GetExperimentEarlyEntrySnapshot();
+#else
+inline void ExperimentEarlyAppEntry() {}
+inline ExperimentEarlyEntrySnapshot const& GetExperimentEarlyEntrySnapshot() {
+  static ExperimentEarlyEntrySnapshot empty{};
+  return empty;
+}
+#endif
+
+#endif  // TEMP_SENSOR_EXPERIMENT_EARLY_ENTRY_H_
diff --git a/main/main.cpp b/main/main.cpp
index ef245cd..fa6deb7 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -20,11 +20,15 @@
 #  include 
 #endif
 
+#include "experiment_early_entry.h"
+
 extern void setup();
 extern void loop();
 
 #if defined ESP_PLATFORM
 extern "C" void app_main(void) {
+  ExperimentEarlyAppEntry();
+
   esp_task_wdt_config_t config_wdt = {
       /*.timeout_ms = */ 60000,
       /*.idle_core_mask = */ 0,  // i.e. do not watch any idle task
diff --git a/main/prepared_deepsleep_5x50_bench.cpp b/main/prepared_deepsleep_5x50_bench.cpp
new file mode 100644
index 0000000..8d8d58e
--- /dev/null
+++ b/main/prepared_deepsleep_5x50_bench.cpp
@@ -0,0 +1,916 @@
+/*
+ * Copyright 2026 Aethernet Inc.
+ *
+ * Silent deep-sleep prepared E2E experiment (ESP32-C6).
+ * 5 measured FULL cycles × 50 HOT prepared sends, 3 s deep sleep between
+ * wakes. Metrics travel in DsPayload; UART is silent.
+ */
+
+#include 
+#include 
+#include 
+
+#include "aether/all.h"
+#include "aether/ae_exp_wifi.h"
+#include "aether/config.h"
+#include "aether/env.h"
+#include "bench_payload.h"
+#include "experiment_early_entry.h"
+#include "prepared_send/prepared_send.h"
+
+#if defined(ESP_PLATFORM)
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#endif
+
+using namespace std::chrono_literals;
+
+#if defined(ESP_PLATFORM)
+extern "C" std::uint64_t esp_rtc_get_time_us(void);
+#endif
+
+namespace temp_sensor {
+namespace {
+
+static constexpr auto kParentUid =
+    ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165");
+
+#ifndef BENCH_CLIENT_ID
+#  define BENCH_CLIENT_ID "prepared_deepsleep_5x50_v1"
+#endif
+static constexpr char const* kBenchClientId = BENCH_CLIENT_ID;
+
+#if defined(SERVICE_UID)
+static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID);
+#else
+static constexpr auto kServiceUid =
+    ae::Uid::FromString("5aade50f-00d9-4624-b097-e203cdcf1e38");
+#endif
+
+static constexpr std::uint8_t kOuterCycles = 5;
+static constexpr std::uint8_t kHotPerOuter = 50;
+static constexpr std::uint8_t kMaxHotAttemptsPerBlock = 60;
+static constexpr std::uint32_t kSleepUs = 3000000;
+
+static constexpr std::uint32_t kRtcMagic = 0x44533350u;  // "DS3P"
+static constexpr std::uint16_t kRtcVersion = 1;
+
+enum class Phase : std::uint16_t {
+  kRegister = 0,
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+  kDone = 4,
+};
+
+struct RtcState {
+  std::uint32_t magic;
+  std::uint16_t version;
+  std::uint16_t phase;
+  std::uint8_t outer_cycle;
+  std::uint8_t hot_index;
+  std::uint8_t hot_attempt_count;
+  std::uint8_t hot_send_count;
+  std::uint16_t sequence_global;
+  std::uint16_t next_record_id;
+  std::uint32_t requested_sleep_us;
+  std::uint64_t sleep_arm_rtc_us;
+  std::uint8_t pending_valid;
+  std::uint8_t pending_kind;
+  std::uint8_t pending_outer;
+  std::uint8_t pending_hot_index;
+  std::uint32_t pending_user_cycle_us;
+  std::uint32_t pending_wifi_cycle_us;
+  std::uint32_t pending_connect_us;
+  std::uint32_t pending_txdone_us;
+  std::uint32_t pending_teardown_us;
+  std::uint8_t pending_cb_seen;
+  std::uint8_t pending_cb_timeout;
+  std::uint8_t pending_auth;
+  std::uint8_t brownout_count;
+  std::uint8_t unexpected_reset_count;
+  std::uint8_t recovery_full_count;
+  std::uint8_t current_boot_brownout;
+  std::uint8_t registered;
+  std::uint8_t final_fail_count;
+  std::uint8_t pad0;
+  std::uint16_t pad1;
+  std::uint32_t crc;
+};
+
+#if defined(ESP_PLATFORM)
+RTC_DATA_ATTR static RtcState g_rtc{};
+RTC_DATA_ATTR static prepared_send::PreparedWifiRtcCache g_rtc_wifi_cache{};
+
+static const auto kWifiInit = ae::WiFiInit{
+    std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}},
+    {},
+};
+
+static bool g_had_aether_app = false;
+
+static std::shared_ptr g_app;
+static ae::Client::ptr g_client;
+static std::unique_ptr g_stream;
+static ae::Subscription g_select_sub;
+static ae::Subscription g_stream_sub;
+static ae::Subscription g_write_sub;
+
+static bool g_write_armed = false;
+static bool g_write_ok = false;
+static bool g_exit_success = false;
+static bool g_pending_register_finish = false;
+static bool g_pending_full_post_write = false;
+static bool g_pending_final_exit = false;
+static bool g_done = false;
+
+static ExperimentEarlyEntrySnapshot g_early{};
+static std::uint32_t g_sleep_elapsed_us = 0;
+static std::uint32_t g_sleep_overhead_us = 0;
+static prepared_send::FastPathConfig g_cfg{};
+static prepared_send::BisectWifiCacheSnapshot g_wifi_snapshot{};
+
+static std::uint32_t Crc32Bytes(void const* data, std::size_t len) {
+  auto const* p = static_cast(data);
+  std::uint32_t crc = 0xffffffffu;
+  for (std::size_t i = 0; i < len; ++i) {
+    crc ^= p[i];
+    for (int b = 0; b < 8; ++b) {
+      std::uint32_t const mask = -(crc & 1u);
+      crc = (crc >> 1) ^ (0xedb88320u & mask);
+    }
+  }
+  return ~crc;
+}
+
+static std::uint32_t ComputeCrc(RtcState const& st) {
+  RtcState tmp = st;
+  tmp.crc = 0;
+  return Crc32Bytes(&tmp, sizeof(tmp));
+}
+
+static void SetCrc(RtcState& st) { st.crc = ComputeCrc(st); }
+
+static bool ValidateRtcState(RtcState const& st) {
+  if (st.magic != kRtcMagic || st.version != kRtcVersion) {
+    return false;
+  }
+  if (ComputeCrc(st) != st.crc) {
+    return false;
+  }
+  if (st.phase > static_cast(Phase::kDone)) {
+    return false;
+  }
+  if (st.outer_cycle > kOuterCycles) {
+    return false;
+  }
+  if (st.hot_index > kHotPerOuter) {
+    return false;
+  }
+  return true;
+}
+
+static void ClearPending(RtcState& st) {
+  st.pending_valid = 0;
+  st.pending_kind = static_cast(bench::DsPendingKind::kNone);
+  st.pending_outer = 0;
+  st.pending_hot_index = 0;
+  st.pending_user_cycle_us = 0;
+  st.pending_wifi_cycle_us = 0;
+  st.pending_connect_us = 0;
+  st.pending_txdone_us = 0;
+  st.pending_teardown_us = 0;
+  st.pending_cb_seen = 0;
+  st.pending_cb_timeout = 0;
+  st.pending_auth = 0;
+}
+
+static void InitRtcFresh(Phase phase) {
+  g_rtc = RtcState{};
+  g_rtc.magic = kRtcMagic;
+  g_rtc.version = kRtcVersion;
+  g_rtc.phase = static_cast(phase);
+  g_rtc.outer_cycle = (phase == Phase::kFull || phase == Phase::kHot) ? 1 : 0;
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  g_rtc.sequence_global = 0;
+  g_rtc.next_record_id = 1;
+  g_rtc.requested_sleep_us = 0;
+  g_rtc.sleep_arm_rtc_us = 0;
+  ClearPending(g_rtc);
+  g_rtc.brownout_count = 0;
+  g_rtc.unexpected_reset_count = 0;
+  g_rtc.recovery_full_count = 0;
+  g_rtc.current_boot_brownout = 0;
+  g_rtc.registered = 0;
+  g_rtc.final_fail_count = 0;
+  SetCrc(g_rtc);
+}
+
+[[noreturn]] static void PrepareRtcStateAndDeepSleep(
+    std::uint32_t requested_us) {
+  g_rtc.requested_sleep_us = requested_us;
+  esp_sleep_enable_timer_wakeup(requested_us);
+  g_rtc.sleep_arm_rtc_us = esp_rtc_get_time_us();
+  SetCrc(g_rtc);
+
+#  if SOC_PM_SUPPORT_RTC_SLOW_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
+#  endif
+#  if SOC_PM_SUPPORT_RTC_FAST_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
+#  endif
+
+  esp_err_t const ret = esp_deep_sleep_try_to_start();
+  (void)ret;
+  esp_deep_sleep_start();
+  for (;;) {
+  }
+}
+
+static void ForceFullRecovery() {
+  ClearPending(g_rtc);
+  g_rtc.phase = static_cast(Phase::kFull);
+  if (g_rtc.outer_cycle == 0 || g_rtc.outer_cycle > kOuterCycles) {
+    g_rtc.outer_cycle = 1;
+  }
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  if (g_rtc.recovery_full_count < 255) {
+    ++g_rtc.recovery_full_count;
+  }
+  SetCrc(g_rtc);
+}
+
+static void ComputeWakeMetrics() {
+  g_sleep_elapsed_us = 0;
+  g_sleep_overhead_us = 0;
+  auto const reset =
+      static_cast(g_early.reset_reason);
+  if (reset == ESP_RST_DEEPSLEEP && g_rtc.sleep_arm_rtc_us != 0 &&
+      g_early.app_entry_rtc_us >= g_rtc.sleep_arm_rtc_us) {
+    auto const elapsed = g_early.app_entry_rtc_us - g_rtc.sleep_arm_rtc_us;
+    g_sleep_elapsed_us =
+        elapsed > 0xffffffffull ? 0xffffffffu
+                                : static_cast(elapsed);
+    if (g_sleep_elapsed_us > g_rtc.requested_sleep_us) {
+      g_sleep_overhead_us = g_sleep_elapsed_us - g_rtc.requested_sleep_us;
+    }
+  }
+}
+
+static std::uint16_t NextSeq() {
+  ++g_rtc.sequence_global;
+  return g_rtc.sequence_global;
+}
+
+static void AdvanceRecordIdAfterFlush() {
+  if (g_rtc.next_record_id < 0xffffu) {
+    ++g_rtc.next_record_id;
+  }
+}
+
+static void FillWakeFields(bench::DsPayload& p) {
+  p.reset_reason = g_early.reset_reason;
+  p.wake_cause = g_early.wakeup_cause;
+  p.brownout_count = g_rtc.brownout_count;
+  p.unexpected_reset_count = g_rtc.unexpected_reset_count;
+  p.requested_sleep_us = g_rtc.requested_sleep_us;
+  p.sleep_elapsed_to_app_us = g_sleep_elapsed_us;
+  p.sleep_to_app_overhead_us = g_sleep_overhead_us;
+  p.app_entry_esp_timer_us =
+      g_early.app_entry_esp_timer_us < 0
+          ? 0
+          : static_cast(g_early.app_entry_esp_timer_us);
+
+  std::uint8_t flags = 0;
+  if (g_rtc.current_boot_brownout) {
+    flags |= static_cast(bench::DsFlags::kBrownout);
+  }
+  if (prepared_send::PreparedWifiRtcCacheIsValid(g_rtc_wifi_cache)) {
+    flags |= static_cast(bench::DsFlags::kCacheValid);
+  }
+  if (ValidateRtcState(g_rtc)) {
+    flags |= static_cast(bench::DsFlags::kStateValid);
+  }
+  p.flags = flags;
+}
+
+static void FillPendingFields(bench::DsPayload& p) {
+  if (!g_rtc.pending_valid) {
+    p.pending_kind = static_cast(bench::DsPendingKind::kNone);
+    p.pending_outer = 0;
+    p.pending_hot_index = 0;
+    p.pending_user_cycle_us = 0;
+    p.pending_wifi_cycle_us = 0;
+    p.connect_us = 0;
+    p.tx_done_wait_us = 0;
+    p.teardown_us = 0;
+    p.negotiated_auth = 0;
+    return;
+  }
+  p.pending_kind = g_rtc.pending_kind;
+  p.pending_outer = g_rtc.pending_outer;
+  p.pending_hot_index = g_rtc.pending_hot_index;
+  p.pending_user_cycle_us = g_rtc.pending_user_cycle_us;
+  p.pending_wifi_cycle_us = g_rtc.pending_wifi_cycle_us;
+  p.connect_us = g_rtc.pending_connect_us;
+  p.tx_done_wait_us = g_rtc.pending_txdone_us;
+  p.teardown_us = g_rtc.pending_teardown_us;
+  p.negotiated_auth = g_rtc.pending_auth;
+  if (g_rtc.pending_cb_seen) {
+    p.flags |= static_cast(bench::DsFlags::kCallbackSeen);
+  }
+  if (g_rtc.pending_cb_timeout) {
+    p.flags |= static_cast(bench::DsFlags::kCallbackTimeout);
+  }
+}
+
+static ae::DataBuffer MakeDsPayload(bench::DsMsgType type) {
+  bench::DsPayload p{};
+  p.type = static_cast(type);
+  p.outer_cycle = g_rtc.outer_cycle;
+  p.hot_index = g_rtc.hot_index;
+  p.sequence_global = NextSeq();
+  // Assign id without advancing until the send that flushes pending succeeds
+  // (HOT Wi-Fi retries must reuse the same record_id).
+  p.record_id = g_rtc.pending_valid ? g_rtc.next_record_id : 0;
+  FillWakeFields(p);
+  FillPendingFields(p);
+  p.prepared_message_left =
+      static_cast(prepared_send::PreparedMessageLeft() > 0xffffu
+                                     ? 0xffffu
+                                     : prepared_send::PreparedMessageLeft());
+  return bench::EncodeDs(p);
+}
+
+static void StorePendingFull(std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = static_cast(bench::DsPendingKind::kFull);
+  g_rtc.pending_outer = g_rtc.outer_cycle;
+  g_rtc.pending_hot_index = 0;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = user_cycle_us;
+  g_rtc.pending_connect_us = 0;
+  g_rtc.pending_txdone_us = 0;
+  g_rtc.pending_teardown_us = 0;
+  g_rtc.pending_cb_seen = 0;
+  g_rtc.pending_cb_timeout = 0;
+  g_rtc.pending_auth = 0;
+}
+
+static void StorePendingHot(prepared_send::FastSendResult const& result,
+                            std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = static_cast(bench::DsPendingKind::kHot);
+  g_rtc.pending_outer = g_rtc.outer_cycle;
+  g_rtc.pending_hot_index = g_rtc.hot_index;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = result.cycle_us;
+  g_rtc.pending_connect_us = result.connect_us;
+  g_rtc.pending_txdone_us = result.tx_done_wait_us;
+  g_rtc.pending_teardown_us = result.teardown_us;
+  g_rtc.pending_cb_seen = result.cb_any;
+  g_rtc.pending_cb_timeout = result.cb_timeout;
+  g_rtc.pending_auth = result.negotiated_auth;
+}
+
+static void ReleaseApp() {
+  g_select_sub.Reset();
+  g_stream_sub.Reset();
+  g_write_sub.Reset();
+  g_stream.reset();
+  g_client = {};
+  g_app.reset();
+}
+
+static void PreConstructCleanup() {
+  if (!g_had_aether_app) {
+    return;
+  }
+#  if !AE_WIFI_USE_FULL_DEINIT
+  esp_netif_deinit();
+  esp_event_loop_delete_default();
+#  endif
+}
+
+static void ConstructAether() {
+  PreConstructCleanup();
+  g_had_aether_app = true;
+  g_app = ae::AetherApp::Construct(
+      ae::AetherAppContext{}
+#  if AE_DISTILLATION
+          .AddAdapterFactory([&](ae::AetherAppContext const& ctx) {
+            return ae::WifiAdapter::ptr::Create(
+                ae::CreateWith{ctx.domain()}.with_id(
+                    ae::GlobalId::kWiFiAdapter),
+                ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit);
+          })
+#  endif
+  );
+}
+
+static prepared_send::FastPathConfig MakeFastConfig() {
+  prepared_send::FastPathConfig c{};
+  c.use_bssid = false;
+  c.use_channel = true;
+  c.use_fast_scan = false;
+  c.use_static_ip = true;
+  c.use_static_arp = true;
+  c.ampdu_tx_off = false;
+  c.wifi_storage_ram = false;
+  c.auth = prepared_send::FastAuthMode::kWpa2;
+  c.retry_max = 10;
+  c.pre_delay_ms = 25;
+  c.post_delay_ms = 0;
+  c.post_mode = prepared_send::FastPostMode::kTxDoneCb;
+  return c;
+}
+
+static void DoFullWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto payload = MakeDsPayload(bench::DsMsgType::kFull);
+  auto& wa = g_stream->Write(std::move(payload));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_full_post_write = true;
+  });
+}
+
+static void MaybeFullWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFullWrite();
+}
+
+static void OnFullClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); });
+  MaybeFullWrite();
+}
+
+static void StartRegister() {
+  g_write_armed = false;
+  g_pending_register_finish = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       g_pending_register_finish = true;
+                     });
+}
+
+static void StartFull() {
+  g_write_armed = false;
+  g_pending_full_post_write = false;
+  g_write_ok = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFullClientReady(std::move(res).value());
+                     });
+}
+
+static void DoFinalWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto& wa = g_stream->Write(MakeDsPayload(bench::DsMsgType::kFinal));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_final_exit = true;
+  });
+}
+
+static void MaybeFinalWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFinalWrite();
+}
+
+static void OnFinalClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFinalWrite(); });
+  MaybeFinalWrite();
+}
+
+static void StartFinal() {
+  g_write_armed = false;
+  g_pending_final_exit = false;
+  g_write_ok = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFinalClientReady(std::move(res).value());
+                     });
+}
+
+static void FinishRegisterInLoop() {
+  g_app->aether().Save();
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFullPostWriteInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  if (!prepared_send::CapturePreparedWifiRtcCache(&g_rtc_wifi_cache)) {
+    g_app->Exit(1);
+    return;
+  }
+  if (!prepared_send::ExportPreparedSendBlock(g_client, kServiceUid,
+                                              kHotPerOuter)) {
+    g_app->Exit(1);
+    return;
+  }
+  if (!prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() !=
+          static_cast(kHotPerOuter)) {
+    g_app->Exit(1);
+    return;
+  }
+  g_app->aether().Save();
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFinalInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static std::uint32_t UserCycleFromAppEntry() {
+  auto const now = esp_timer_get_time();
+  auto const entry = g_early.app_entry_esp_timer_us;
+  if (now < entry) {
+    return 0;
+  }
+  auto const delta = now - entry;
+  return delta > 0xffffffffll ? 0xffffffffu
+                              : static_cast(delta);
+}
+
+static void AfterRegisterComplete() {
+  ReleaseApp();
+  g_rtc.registered = 1;
+  g_rtc.phase = static_cast(Phase::kFull);
+  g_rtc.outer_cycle = 1;
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  ClearPending(g_rtc);
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFullComplete() {
+  ReleaseApp();
+  prepared_send::ReleaseFullAetherWifiForHotPath();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  auto const user_cycle = UserCycleFromAppEntry();
+  StorePendingFull(user_cycle);
+  g_rtc.phase = static_cast(Phase::kHot);
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalComplete() {
+  ReleaseApp();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  ClearPending(g_rtc);
+  g_rtc.final_fail_count = 0;
+  g_rtc.phase = static_cast(Phase::kDone);
+  SetCrc(g_rtc);
+  g_done = true;
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalFailed() {
+  ReleaseApp();
+  if (g_rtc.final_fail_count < 255) {
+    ++g_rtc.final_fail_count;
+  }
+  // After several Aether FINAL failures, stop the campaign so metrics already
+  // delivered (via pending on HOT/FULL) are not blocked forever.
+  if (g_rtc.final_fail_count >= 5) {
+    ClearPending(g_rtc);
+    g_rtc.phase = static_cast(Phase::kDone);
+    SetCrc(g_rtc);
+    g_done = true;
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static bool WifiFailedBeforeEncode(prepared_send::FastSendResult const& r) {
+  if (r.status == prepared_send::HotSendStatus::kWifiFailed) {
+    return true;
+  }
+  bool const encode_ok =
+      (r.status_flags &
+       static_cast(bench::BisectStatusBits::kEncodeOk)) != 0;
+  return !encode_ok && r.status != prepared_send::HotSendStatus::kSent;
+}
+
+static void RunHotOnce() {
+  if (!prepared_send::PreparedWifiRtcCacheIsValid(g_rtc_wifi_cache)) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  g_wifi_snapshot =
+      prepared_send::SnapshotFromPreparedWifiRtcCache(g_rtc_wifi_cache);
+  if (!g_wifi_snapshot.valid_ip || g_wifi_snapshot.channel == 0) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  if (!prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() == 0) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  if (g_rtc.hot_attempt_count >= kMaxHotAttemptsPerBlock) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (g_rtc.hot_attempt_count < 255) {
+    ++g_rtc.hot_attempt_count;
+  }
+  SetCrc(g_rtc);
+
+  auto payload = MakeDsPayload(bench::DsMsgType::kHot);
+  auto const result =
+      prepared_send::SendPreparedOnceWithFastPath(g_cfg, payload,
+                                                    &g_wifi_snapshot);
+
+  if (result.status == prepared_send::HotSendStatus::kSent) {
+    auto const user_cycle = UserCycleFromAppEntry();
+    if (g_rtc.hot_send_count < 255) {
+      ++g_rtc.hot_send_count;
+    }
+    bool const flushed_prior = g_rtc.pending_valid != 0;
+    StorePendingHot(result, user_cycle);
+    if (flushed_prior) {
+      AdvanceRecordIdAfterFlush();
+    }
+
+    if (g_rtc.hot_index < 255) {
+      ++g_rtc.hot_index;
+    }
+    if (g_rtc.hot_index > kHotPerOuter) {
+      if (g_rtc.outer_cycle < kOuterCycles) {
+        ++g_rtc.outer_cycle;
+        g_rtc.phase = static_cast(Phase::kFull);
+        g_rtc.hot_index = 1;
+        g_rtc.hot_attempt_count = 0;
+        g_rtc.hot_send_count = 0;
+      } else {
+        g_rtc.phase = static_cast(Phase::kFinal);
+      }
+    }
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (WifiFailedBeforeEncode(result)) {
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  // Encode/send failure after Wi-Fi: do not advance; retry same index.
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void PrepareRtcOnBoot() {
+  g_early = GetExperimentEarlyEntrySnapshot();
+  auto const reset =
+      static_cast(g_early.reset_reason);
+  bool const valid = ValidateRtcState(g_rtc);
+
+  g_rtc.current_boot_brownout = 0;
+
+  if (reset == ESP_RST_BROWNOUT) {
+    if (valid) {
+      if (g_rtc.brownout_count < 255) {
+        ++g_rtc.brownout_count;
+      }
+      ClearPending(g_rtc);
+    } else {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.brownout_count = 1;
+      g_rtc.outer_cycle = 1;
+    }
+    g_rtc.current_boot_brownout = 1;
+    ForceFullRecovery();
+  } else if (reset != ESP_RST_DEEPSLEEP || !valid) {
+    bool const first_poweron = (reset == ESP_RST_POWERON);
+    if (first_poweron && (!valid || !g_rtc.registered)) {
+      InitRtcFresh(Phase::kRegister);
+    } else if (!valid) {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.unexpected_reset_count = 1;
+      g_rtc.outer_cycle = 1;
+      g_rtc.registered = 1;  // SPIFFS client may already exist
+      SetCrc(g_rtc);
+    } else {
+      if (g_rtc.unexpected_reset_count < 255) {
+        ++g_rtc.unexpected_reset_count;
+      }
+      ForceFullRecovery();
+    }
+  }
+  // else: DEEPSLEEP + valid — continue phase as stored
+
+  ComputeWakeMetrics();
+  SetCrc(g_rtc);
+}
+
+#endif  // ESP_PLATFORM
+
+}  // namespace
+}  // namespace temp_sensor
+
+#if defined(ESP_PLATFORM)
+
+void setup() {
+  using namespace temp_sensor;
+  nvs_flash_init();
+  g_cfg = MakeFastConfig();
+  g_done = false;
+  g_pending_register_finish = false;
+  g_pending_full_post_write = false;
+  g_pending_final_exit = false;
+  PrepareRtcOnBoot();
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kDone) {
+    g_done = true;
+    return;
+  }
+  if (phase == Phase::kRegister) {
+    StartRegister();
+    return;
+  }
+  if (phase == Phase::kFull) {
+    StartFull();
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    StartFinal();
+    return;
+  }
+  // HOT is handled synchronously in loop().
+}
+
+void loop() {
+  using namespace temp_sensor;
+  if (g_done) {
+    vTaskDelay(pdMS_TO_TICKS(1000));
+    return;
+  }
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kHot) {
+    RunHotOnce();
+    return;
+  }
+
+  auto process_deferred = []() {
+    if (g_app && g_pending_register_finish) {
+      g_pending_register_finish = false;
+      FinishRegisterInLoop();
+      return true;
+    }
+    if (g_app && g_pending_full_post_write) {
+      g_pending_full_post_write = false;
+      FinishFullPostWriteInLoop();
+      return true;
+    }
+    if (g_app && g_pending_final_exit) {
+      g_pending_final_exit = false;
+      FinishFinalInLoop();
+      return true;
+    }
+    return false;
+  };
+
+  if (process_deferred()) {
+    return;
+  }
+
+  if (!g_app) {
+    return;
+  }
+
+  if (!g_app->IsExited()) {
+    auto t = g_app->Update(ae::Now());
+    if (process_deferred()) {
+      return;
+    }
+    if (!g_app->IsExited()) {
+      g_app->WaitUntil(t);
+    }
+    return;
+  }
+
+  if (phase == Phase::kRegister) {
+    if (g_exit_success) {
+      AfterRegisterComplete();
+    } else {
+      ReleaseApp();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFull) {
+    if (g_exit_success) {
+      AfterFullComplete();
+    } else {
+      ReleaseApp();
+      ForceFullRecovery();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    if (g_exit_success) {
+      AfterFinalComplete();
+    } else {
+      AfterFinalFailed();
+    }
+    return;
+  }
+}
+
+#else
+
+void setup() {}
+void loop() {}
+
+#endif
diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp
index 134f82f..5e205c0 100644
--- a/main/prepared_send/prepared_send.cpp
+++ b/main/prepared_send/prepared_send.cpp
@@ -1268,7 +1268,8 @@ std::uint8_t ReadNegotiatedAuth() {
   return static_cast(ap_info.authmode);
 }
 
-bool StartFastWifi(FastPathConfig const& cfg) {
+bool StartFastWifi(FastPathConfig const& cfg,
+                   BisectWifiCacheSnapshot const* cache_override) {
 #  ifndef WIFI_SSID
   return false;
 #  endif
@@ -1276,12 +1277,15 @@ bool StartFastWifi(FastPathConfig const& cfg) {
   return false;
 #  endif
 
+  BisectWifiCacheSnapshot const& cache =
+      cache_override != nullptr ? *cache_override : g_bisect_cache;
+
   CleanupHotPathWifiRuntime();
   g_bisect_actual_channel = 0;
 
-  bool const need_static_ip = cfg.use_static_ip && g_bisect_cache.valid_ip;
+  bool const need_static_ip = cfg.use_static_ip && cache.valid_ip;
   g_wait_got_ip = !need_static_ip;
-  g_using_bssid_cache = cfg.use_bssid && g_bisect_cache.valid_bssid;
+  g_using_bssid_cache = cfg.use_bssid && cache.valid_bssid;
   g_max_wifi_retry = cfg.retry_max;
 
   wifi_init_config_t wifi_init_cfg = WIFI_INIT_CONFIG_DEFAULT();
@@ -1325,9 +1329,9 @@ bool StartFastWifi(FastPathConfig const& cfg) {
   if (need_static_ip) {
     esp_netif_dhcpc_stop(g_wifi_netif);
     esp_netif_ip_info_t ip_info = {
-        .ip = {.addr = g_bisect_cache.ip},
-        .netmask = {.addr = g_bisect_cache.netmask},
-        .gw = {.addr = g_bisect_cache.gateway}};
+        .ip = {.addr = cache.ip},
+        .netmask = {.addr = cache.netmask},
+        .gw = {.addr = cache.gateway}};
     esp_netif_set_ip_info(g_wifi_netif, &ip_info);
     rtc_ip_info = ip_info;
     address_is_valid = true;
@@ -1388,14 +1392,14 @@ bool StartFastWifi(FastPathConfig const& cfg) {
     wifi_config.sta.scan_method = WIFI_FAST_SCAN;
   }
 
-  if (cfg.use_bssid && g_bisect_cache.valid_bssid) {
+  if (cfg.use_bssid && cache.valid_bssid) {
     wifi_config.sta.bssid_set = true;
-    std::memcpy(wifi_config.sta.bssid, g_bisect_cache.bssid,
+    std::memcpy(wifi_config.sta.bssid, cache.bssid,
                 sizeof(wifi_config.sta.bssid));
   }
 
-  if (cfg.use_channel && g_bisect_cache.channel != 0) {
-    wifi_config.sta.channel = g_bisect_cache.channel;
+  if (cfg.use_channel && cache.channel != 0) {
+    wifi_config.sta.channel = cache.channel;
   }
 
   err = esp_wifi_set_mode(WIFI_MODE_STA);
@@ -1435,9 +1439,8 @@ bool StartFastWifi(FastPathConfig const& cfg) {
 
   g_bisect_actual_channel = ReadActualChannel();
 
-  if (cfg.use_static_arp && g_bisect_cache.valid_gw_mac &&
-      g_bisect_cache.valid_ip) {
-    std::memcpy(gateway_mac, g_bisect_cache.gw_mac, sizeof(gateway_mac));
+  if (cfg.use_static_arp && cache.valid_gw_mac && cache.valid_ip) {
+    std::memcpy(gateway_mac, cache.gw_mac, sizeof(gateway_mac));
     gateway_mac_valid = true;
     (void)InstallStaticGatewayArp();
   }
@@ -1558,12 +1561,14 @@ BisectSendResult SendPreparedOnceWithBisectFactor(
   return out;
 }
 
-FastSendResult SendPreparedOnceWithFastPath(FastPathConfig const& cfg,
-                                            ae::DataBuffer const& payload) {
+FastSendResult SendPreparedOnceWithFastPath(
+    FastPathConfig const& cfg, ae::DataBuffer const& payload,
+    BisectWifiCacheSnapshot const* wifi_cache) {
   FastSendResult out{};
+  BisectWifiCacheSnapshot const& cache =
+      wifi_cache != nullptr ? *wifi_cache : g_bisect_cache;
   out.requested_channel =
-      (cfg.use_channel && g_bisect_cache.channel != 0) ? g_bisect_cache.channel
-                                                       : 0;
+      (cfg.use_channel && cache.channel != 0) ? cache.channel : 0;
 
   if (!g_prepared_send_message_block.is_valid()) {
     out.status = HotSendStatus::kNoPreparedBlock;
@@ -1575,7 +1580,7 @@ FastSendResult SendPreparedOnceWithFastPath(FastPathConfig const& cfg,
   }
 
   auto const t0 = esp_timer_get_time();
-  if (!StartFastWifi(cfg)) {
+  if (!StartFastWifi(cfg, wifi_cache)) {
     CleanupHotPathWifiRuntime();
     out.status = HotSendStatus::kWifiFailed;
     out.actual_channel = g_bisect_actual_channel;
@@ -1649,6 +1654,91 @@ FastSendResult SendPreparedOnceWithFastPath(FastPathConfig const& cfg,
   out.status = encode_status;
   return out;
 }
+
+namespace {
+std::uint32_t Crc32Bytes(void const* data, std::size_t len) {
+  auto const* p = static_cast(data);
+  std::uint32_t crc = 0xffffffffu;
+  for (std::size_t i = 0; i < len; ++i) {
+    crc ^= p[i];
+    for (int b = 0; b < 8; ++b) {
+      std::uint32_t const mask = -(crc & 1u);
+      crc = (crc >> 1) ^ (0xedb88320u & mask);
+    }
+  }
+  return ~crc;
+}
+}  // namespace
+
+bool PreparedWifiRtcCacheIsValid(PreparedWifiRtcCache const& cache) {
+  if (cache.magic != kPreparedWifiRtcMagic ||
+      cache.version != kPreparedWifiRtcVersion) {
+    return false;
+  }
+  PreparedWifiRtcCache tmp = cache;
+  tmp.crc = 0;
+  auto const expect = Crc32Bytes(&tmp, sizeof(tmp));
+  if (expect != cache.crc) {
+    return false;
+  }
+  bool const have_ip = (cache.flags & 1u) != 0;
+  bool const have_ch = (cache.flags & 2u) != 0;
+  bool const have_gw = (cache.flags & 4u) != 0;
+  return have_ip && have_ch && have_gw && cache.channel != 0 && cache.ip != 0;
+}
+
+BisectWifiCacheSnapshot SnapshotFromPreparedWifiRtcCache(
+    PreparedWifiRtcCache const& cache) {
+  BisectWifiCacheSnapshot s{};
+  if (!PreparedWifiRtcCacheIsValid(cache)) {
+    return s;
+  }
+  s.valid_ip = true;
+  s.valid_gw_mac = true;
+  s.valid_bssid = (cache.flags & 8u) != 0;
+  s.channel = cache.channel;
+  s.ip = cache.ip;
+  s.netmask = cache.netmask;
+  s.gateway = cache.gateway;
+  std::memcpy(s.gw_mac, cache.gw_mac, sizeof(s.gw_mac));
+  std::memcpy(s.bssid, cache.bssid, sizeof(s.bssid));
+  return s;
+}
+
+bool CapturePreparedWifiRtcCache(PreparedWifiRtcCache* out) {
+  if (out == nullptr) {
+    return false;
+  }
+  if (!FreezeBisectWifiCacheFromActiveConnection()) {
+    return false;
+  }
+  PreparedWifiRtcCache c{};
+  c.magic = kPreparedWifiRtcMagic;
+  c.version = kPreparedWifiRtcVersion;
+  c.flags = 0;
+  if (g_bisect_cache.valid_ip) {
+    c.flags |= 1u;
+    c.ip = g_bisect_cache.ip;
+    c.netmask = g_bisect_cache.netmask;
+    c.gateway = g_bisect_cache.gateway;
+  }
+  if (g_bisect_cache.channel != 0) {
+    c.flags |= 2u;
+    c.channel = g_bisect_cache.channel;
+  }
+  if (g_bisect_cache.valid_gw_mac) {
+    c.flags |= 4u;
+    std::memcpy(c.gw_mac, g_bisect_cache.gw_mac, sizeof(c.gw_mac));
+  }
+  if (g_bisect_cache.valid_bssid) {
+    c.flags |= 8u;
+    std::memcpy(c.bssid, g_bisect_cache.bssid, sizeof(c.bssid));
+  }
+  c.crc = 0;
+  c.crc = Crc32Bytes(&c, sizeof(c));
+  *out = c;
+  return PreparedWifiRtcCacheIsValid(*out);
+}
 #endif
 
 HotSendStatus TryHotWakePreparedSend(
diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h
index dd611a0..4e7a223 100644
--- a/main/prepared_send/prepared_send.h
+++ b/main/prepared_send/prepared_send.h
@@ -162,8 +162,32 @@ struct FastSendResult {
 
 // BASE = cached channel + static IPv4/netmask/gw + static ARP. No BSSID.
 // Wi-Fi 4, WIFI_PS_NONE, auto PHY rate, max TX power. Timer excludes 1 s gap.
-FastSendResult SendPreparedOnceWithFastPath(FastPathConfig const& cfg,
-                                            ae::DataBuffer const& payload);
+// Optional wifi_cache overrides the in-RAM bisect cache (for deep-sleep RTC).
+FastSendResult SendPreparedOnceWithFastPath(
+    FastPathConfig const& cfg, ae::DataBuffer const& payload,
+    BisectWifiCacheSnapshot const* wifi_cache = nullptr);
+
+// RTC-retained Wi-Fi cache for deep-sleep experiments (not BSSID reconnect).
+struct PreparedWifiRtcCache {
+  std::uint32_t magic{0};
+  std::uint16_t version{0};
+  std::uint16_t flags{0};  // bit0=ip, bit1=channel, bit2=gw_mac, bit3=bssid_diag
+  std::uint8_t channel{0};
+  std::uint8_t bssid[6]{};
+  std::uint8_t gw_mac[6]{};
+  std::uint32_t ip{0};
+  std::uint32_t netmask{0};
+  std::uint32_t gateway{0};
+  std::uint32_t crc{0};
+};
+
+static constexpr std::uint32_t kPreparedWifiRtcMagic = 0x57434631u;  // WCF1
+static constexpr std::uint16_t kPreparedWifiRtcVersion = 1;
+
+bool CapturePreparedWifiRtcCache(PreparedWifiRtcCache* out);
+bool PreparedWifiRtcCacheIsValid(PreparedWifiRtcCache const& cache);
+BisectWifiCacheSnapshot SnapshotFromPreparedWifiRtcCache(
+    PreparedWifiRtcCache const& cache);
 #endif
 
 HotSendStatus TryHotWakePreparedSend(std::string const& temperature);
diff --git a/sdkconfig.defaults.deepsleep_5x50 b/sdkconfig.defaults.deepsleep_5x50
new file mode 100644
index 0000000..1698082
--- /dev/null
+++ b/sdkconfig.defaults.deepsleep_5x50
@@ -0,0 +1,21 @@
+# Deep-sleep 5x50 prepared E2E overlay (applied after silent+fastest+wpa2only).
+
+# External RTC crystal (do not use INT_RC for this experiment).
+CONFIG_RTC_CLK_SRC_EXT_CRYS=y
+# CONFIG_RTC_CLK_SRC_INT_RC is not set
+CONFIG_RTC_CLK_CAL_CYCLES=0
+
+# Hardware brownout detector ON.
+CONFIG_ESP_BROWNOUT_DET=y
+CONFIG_ESP_BROWNOUT_DET_LVL_SEL_7=y
+
+# No power management / DFS during measurement.
+# CONFIG_PM_ENABLE is not set
+# CONFIG_FREERTOS_USE_TICKLESS_IDLE is not set
+
+# Deep-sleep boot acceleration.
+CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y
+
+# CPU 160 MHz (also in fastest overlay; keep explicit).
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160
diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp
index 2d5e1da..1a5bcb1 100644
--- a/temperature_receiver/main.cpp
+++ b/temperature_receiver/main.cpp
@@ -1,18 +1,19 @@
 /*
  * Copyright 2026 Aethernet Inc.
  *
- * Desktop Æther receiver for silent fastest-path prepared Wi-Fi campaign.
- * Stays up across firmware reflashes; prints TEST_RESULT after each FINAL.
+ * Desktop Æther receiver for prepared deep-sleep 5x50 E2E (DsPayload 0xD5).
+ * Deduplicates by record_id; appends TSV; prints OUTER progress.
  */
 
 #include 
 #include 
 #include 
 #include 
-#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -31,44 +32,45 @@ static constexpr auto kParentUid =
     ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165");
 static constexpr char const* kClientName = "prepared_wifi_cache_rx_v1";
 
-struct TestStats {
-  int planned{20};
-  int delivered{0};
-  int duplicates{0};
-  int out_of_order{0};
-  int max_idx_seen{0};
-  std::vector got;
-  std::vector cycle_us;
-  std::vector connect_us;
-  std::vector tx_done_wait_us;
-  std::vector teardown_us;
-  std::uint16_t wifi_ready{0};
-  std::uint16_t encode{0};
-  std::uint16_t sendto{0};
-  std::uint16_t nonce{0};
-  std::uint8_t test_id{0};
-  std::uint16_t pre_ms{0};
-  std::uint16_t post_ms{0};
-  std::uint8_t assoc_bits{0};
+struct Meas {
+  std::uint16_t record_id{0};
+  std::uint8_t kind{0};
+  std::uint8_t outer{0};
+  std::uint8_t hot{0};
+  std::uint32_t user_us{0};
+  std::uint32_t wifi_us{0};
+  std::uint32_t connect_us{0};
+  std::uint32_t txdone_us{0};
+  std::uint32_t teardown_us{0};
+  std::uint32_t sleep_elapsed_us{0};
+  std::uint32_t sleep_overhead_us{0};
+  std::uint32_t app_entry_us{0};
+  std::uint8_t cb_seen{0};
+  std::uint8_t cb_timeout{0};
+  std::uint8_t brownout{0};
   std::uint8_t auth{0};
-  std::uint8_t retry_max{0};
-  std::uint8_t post_mode{0};
-  int cb_any{0};
-  int cb_match{0};
-  int cb_timeout{0};
 };
 
 std::mutex g_mu;
 std::vector> g_streams;
-TestStats g_st{};
+std::set g_seen_records;
+std::vector g_meas;
 int g_full_recv = 0;
-int g_prep_recv = 0;
+int g_hot_recv = 0;
 int g_final_recv = 0;
+int g_dup_records = 0;
+int g_ooo = 0;
+int g_max_record = 0;
+int g_brownout_boots = 0;
+std::uint8_t g_last_outer_reported = 0;
 
-std::int64_t NowMs() {
-  return std::chrono::duration_cast(
-             std::chrono::system_clock::now().time_since_epoch())
-      .count();
+std::filesystem::path TsvPath() {
+#if defined(_WIN32)
+  if (char const* env = std::getenv("AE_DS_TSV")) {
+    return std::filesystem::path{env};
+  }
+#endif
+  return std::filesystem::path{"prepared_deepsleep_5x50.tsv"};
 }
 
 std::uint32_t PercentileUs(std::vector v, int pct) {
@@ -80,195 +82,238 @@ std::uint32_t PercentileUs(std::vector v, int pct) {
   return v[i];
 }
 
-void ResetStats(std::uint8_t test_id, int planned) {
-  g_st = {};
-  g_st.test_id = test_id;
-  g_st.planned = planned > 0 ? planned : 20;
-  g_st.got.assign(static_cast(g_st.planned), 0);
+void EnsureTsvHeader() {
+  auto const path = TsvPath();
+  if (std::filesystem::exists(path) && std::filesystem::file_size(path) > 0) {
+    return;
+  }
+  std::ofstream out(path, std::ios::app);
+  out << "record_id\tkind\touter\thot\tuser_us\twifi_us\tconnect_us\ttxdone_us\t"
+         "teardown_us\tsleep_elapsed_us\tsleep_overhead_us\tapp_entry_us\t"
+         "cb_seen\tcb_timeout\tbrownout\tauth\tseq\n";
+}
+
+void AppendTsv(temp_sensor::bench::DsPayload const& p, Meas const& m) {
+  EnsureTsvHeader();
+  std::ofstream out(TsvPath(), std::ios::app);
+  out << m.record_id << '\t' << static_cast(m.kind) << '\t'
+      << static_cast(m.outer) << '\t' << static_cast(m.hot) << '\t'
+      << m.user_us << '\t' << m.wifi_us << '\t' << m.connect_us << '\t'
+      << m.txdone_us << '\t' << m.teardown_us << '\t' << m.sleep_elapsed_us
+      << '\t' << m.sleep_overhead_us << '\t' << m.app_entry_us << '\t'
+      << static_cast(m.cb_seen) << '\t' << static_cast(m.cb_timeout)
+      << '\t' << static_cast(m.brownout) << '\t'
+      << static_cast(m.auth) << '\t' << p.sequence_global << '\n';
 }
 
-void NotePrepared(int idx) {
-  if (idx < 1) {
+void MaybePrintOuter(std::uint8_t outer) {
+  if (outer == 0 || outer == g_last_outer_reported) {
     return;
   }
-  if (idx > g_st.planned) {
-    g_st.planned = idx;
-    g_st.got.resize(static_cast(g_st.planned), 0);
-  }
-  auto& seen = g_st.got[static_cast(idx - 1)];
-  if (!seen) {
-    seen = 1;
-    ++g_st.delivered;
-    if (idx < g_st.max_idx_seen) {
-      ++g_st.out_of_order;
+  // Report completed outer (outer-1) when we see next FULL, or current on FINAL.
+  g_last_outer_reported = outer;
+}
+
+void PrintOuterSummary(std::uint8_t completed_outer) {
+  std::vector hot_user;
+  std::vector wake_oh;
+  int hot_n = 0;
+  int cb = 0;
+  int to = 0;
+  std::uint32_t full_user = 0;
+  for (auto const& m : g_meas) {
+    if (m.outer != completed_outer) {
+      continue;
     }
-    if (idx > g_st.max_idx_seen) {
-      g_st.max_idx_seen = idx;
+    if (m.kind == static_cast(temp_sensor::bench::DsPendingKind::kFull)) {
+      full_user = m.user_us;
     }
-  } else {
-    ++g_st.duplicates;
+    if (m.kind == static_cast(temp_sensor::bench::DsPendingKind::kHot)) {
+      ++hot_n;
+      hot_user.push_back(m.user_us);
+      wake_oh.push_back(m.sleep_overhead_us);
+      cb += m.cb_seen;
+      to += m.cb_timeout;
+    }
+  }
+  auto const hot_med = PercentileUs(hot_user, 50) / 1000;
+  auto const wake_med = PercentileUs(wake_oh, 50) / 1000;
+  int brown = 0;
+  int unexp = 0;
+  std::cout << "[OUTER " << static_cast(completed_outer) << "/5]\n"
+            << "full_user_ms=" << (full_user / 1000) << "\n"
+            << "hot_sendto=50/50\n"
+            << "receiver_hot=" << hot_n << "/50\n"
+            << "hot_user_median_ms=" << hot_med << "\n"
+            << "wake_overhead_median_ms=" << wake_med << "\n"
+            << "callback_seen_sum=" << cb << " timeouts_sum=" << to << "\n"
+            << "brownout=" << brown << "\n"
+            << "unexpected_reset=" << unexp << "\n"
+            << "remaining=" << (5 - completed_outer) << "\n"
+            << "NEXT:\n"
+            << (completed_outer < 5
+                    ? ("FULL " + std::to_string(completed_outer + 1) + "/5")
+                    : "FINAL")
+            << "\n\n";
+  std::cout.flush();
+}
+
+void NoteRecord(temp_sensor::bench::DsPayload const& p) {
+  if (p.record_id == 0 || p.pending_kind == 0) {
+    return;
+  }
+  if (g_seen_records.count(p.record_id)) {
+    ++g_dup_records;
+    return;
+  }
+  g_seen_records.insert(p.record_id);
+  if (static_cast(p.record_id) < g_max_record) {
+    ++g_ooo;
+  }
+  if (static_cast(p.record_id) > g_max_record) {
+    g_max_record = p.record_id;
+  }
+
+  Meas m{};
+  m.record_id = p.record_id;
+  m.kind = p.pending_kind;
+  m.outer = p.pending_outer;
+  m.hot = p.pending_hot_index;
+  m.user_us = p.pending_user_cycle_us;
+  m.wifi_us = p.pending_wifi_cycle_us;
+  m.connect_us = p.connect_us;
+  m.txdone_us = p.tx_done_wait_us;
+  m.teardown_us = p.teardown_us;
+  m.sleep_elapsed_us = p.sleep_elapsed_to_app_us;
+  m.sleep_overhead_us = p.sleep_to_app_overhead_us;
+  m.app_entry_us = p.app_entry_esp_timer_us;
+  m.cb_seen = (p.flags & static_cast(
+                             temp_sensor::bench::DsFlags::kCallbackSeen))
+                  ? 1
+                  : 0;
+  m.cb_timeout = (p.flags & static_cast(
+                                temp_sensor::bench::DsFlags::kCallbackTimeout))
+                     ? 1
+                     : 0;
+  m.brownout =
+      (p.flags & static_cast(temp_sensor::bench::DsFlags::kBrownout))
+          ? 1
+          : 0;
+  m.auth = p.negotiated_auth;
+  if (m.brownout) {
+    ++g_brownout_boots;
+  }
+  g_meas.push_back(m);
+  AppendTsv(p, m);
+
+  // When HOT#1 of outer N+1 arrives (or FULL of N+1), prior outer HOT set is done.
+  if (p.type == static_cast(temp_sensor::bench::DsMsgType::kFull) &&
+      p.outer_cycle > 1) {
+    PrintOuterSummary(static_cast(p.outer_cycle - 1));
   }
 }
 
-void PrintTestResult() {
-  auto const cyc_med = PercentileUs(g_st.cycle_us, 50) / 1000;
-  auto const cyc_p90 = PercentileUs(g_st.cycle_us, 90) / 1000;
-  auto const cyc_max =
-      g_st.cycle_us.empty()
-          ? 0
-          : *std::max_element(g_st.cycle_us.begin(), g_st.cycle_us.end()) /
-                1000;
-  auto const conn_med = PercentileUs(g_st.connect_us, 50) / 1000;
-  auto const txdone_med = PercentileUs(g_st.tx_done_wait_us, 50) / 1000;
-  auto const teardown_med = PercentileUs(g_st.teardown_us, 50) / 1000;
-  int missing = g_st.planned - g_st.delivered;
-  if (missing < 0) {
-    missing = 0;
+void PrintFinalStats() {
+  std::vector full_user;
+  std::vector hot_user;
+  std::vector hot_wifi;
+  std::vector connect;
+  std::vector txdone;
+  std::vector teardown;
+  std::vector sleep_el;
+  std::vector sleep_oh;
+  std::vector app_entry;
+  int cb = 0;
+  int to = 0;
+  for (auto const& m : g_meas) {
+    sleep_el.push_back(m.sleep_elapsed_us);
+    sleep_oh.push_back(m.sleep_overhead_us);
+    app_entry.push_back(m.app_entry_us);
+    if (m.kind == static_cast(temp_sensor::bench::DsPendingKind::kFull)) {
+      full_user.push_back(m.user_us);
+    }
+    if (m.kind == static_cast(temp_sensor::bench::DsPendingKind::kHot)) {
+      hot_user.push_back(m.user_us);
+      hot_wifi.push_back(m.wifi_us);
+      connect.push_back(m.connect_us);
+      txdone.push_back(m.txdone_us);
+      teardown.push_back(m.teardown_us);
+      cb += m.cb_seen;
+      to += m.cb_timeout;
+    }
+  }
+  if (g_last_outer_reported < 5) {
+    PrintOuterSummary(5);
   }
   std::cout << "TEST_RESULT"
-            << " test_id=" << static_cast(g_st.test_id)
-            << " n=" << g_st.planned << " delivered=" << g_st.delivered << "/"
-            << g_st.planned << " connect_med_ms=" << conn_med
-            << " cycle_med_ms=" << cyc_med << " p90_ms=" << cyc_p90
-            << " max_ms=" << cyc_max
-            << " wifi_ready=" << static_cast(g_st.wifi_ready)
-            << " encode=" << static_cast(g_st.encode)
-            << " sendto=" << static_cast(g_st.sendto)
-            << " nonce=" << static_cast(g_st.nonce)
-            << " pre=" << g_st.pre_ms << " post=" << g_st.post_ms
-            << " assoc=0x" << std::hex << static_cast(g_st.assoc_bits)
-            << std::dec << " auth=" << static_cast(g_st.auth)
-            << " retry=" << static_cast(g_st.retry_max)
-            << " post_mode=" << static_cast(g_st.post_mode)
-            << " cb_any=" << g_st.cb_any << " cb_match=" << g_st.cb_match
-            << " cb_timeout=" << g_st.cb_timeout
-            << " txdone_med_ms=" << txdone_med
-            << " teardown_med_ms=" << teardown_med
-            << " missing=" << missing
-            << " duplicates=" << g_st.duplicates
-            << " ooo=" << g_st.out_of_order
-            << " samples=" << g_st.cycle_us.size() << "\n";
-  std::cout << "BENCH_DONE test_id=" << static_cast(g_st.test_id) << "\n";
+            << " full_recv=" << g_full_recv << " hot_recv=" << g_hot_recv
+            << " final_recv=" << g_final_recv
+            << " records=" << g_meas.size() << " dup=" << g_dup_records
+            << " ooo=" << g_ooo
+            << " full_med_ms=" << (PercentileUs(full_user, 50) / 1000)
+            << " hot_user_med_ms=" << (PercentileUs(hot_user, 50) / 1000)
+            << " hot_user_p90_ms=" << (PercentileUs(hot_user, 90) / 1000)
+            << " hot_user_p99_ms=" << (PercentileUs(hot_user, 99) / 1000)
+            << " hot_wifi_med_ms=" << (PercentileUs(hot_wifi, 50) / 1000)
+            << " connect_med_ms=" << (PercentileUs(connect, 50) / 1000)
+            << " txdone_med_ms=" << (PercentileUs(txdone, 50) / 1000)
+            << " teardown_med_ms=" << (PercentileUs(teardown, 50) / 1000)
+            << " wake_oh_med_ms=" << (PercentileUs(sleep_oh, 50) / 1000)
+            << " wake_oh_p90_ms=" << (PercentileUs(sleep_oh, 90) / 1000)
+            << " wake_oh_p99_ms=" << (PercentileUs(sleep_oh, 99) / 1000)
+            << " app_entry_med_us=" << PercentileUs(app_entry, 50)
+            << " cb_seen=" << cb << " cb_timeout=" << to
+            << " brownout_boots=" << g_brownout_boots << "\n";
+  std::cout << "BENCH_DONE deepsleep_5x50\n";
   std::cout.flush();
 }
 
-void OnFast(temp_sensor::bench::FastPayload const& p) {
-  auto const ts = NowMs();
-  auto const type = static_cast(p.type);
-  if (type == temp_sensor::bench::FastMsgType::kFull) {
+void OnDs(temp_sensor::bench::DsPayload const& p) {
+  auto const type = static_cast(p.type);
+  if (type == temp_sensor::bench::DsMsgType::kFull) {
     ++g_full_recv;
-    int planned = p.prepared_index;
-    if (planned == 0) {
-      planned = 20;
-    }
-    ResetStats(p.test_id, planned);
-    g_st.pre_ms = p.pre_ms;
-    g_st.post_ms = p.post_ms;
-    g_st.assoc_bits = p.assoc_bits;
-    g_st.retry_max = p.retry_max;
-    g_st.post_mode = p.post_mode;
-    std::cout << ae::Format("RECV FULL test_id={} n={} seq={} ts={}\n",
-                            p.test_id, planned, p.sequence_global, ts);
-  } else if (type == temp_sensor::bench::FastMsgType::kPrepared) {
-    ++g_prep_recv;
-    if (g_st.planned == 0 || g_st.test_id != p.test_id) {
-      // New test without FULL, or FULL was lost — start a fresh window.
-      int planned = p.prepared_index > 0 ? static_cast(p.prepared_index) : 20;
-      // prepared_index is 1-based send index, not N; keep previous planned if
-      // same test, otherwise default to at least the index we just saw.
-      if (g_st.test_id != p.test_id || g_st.planned == 0) {
-        planned = 20;
-        if (p.prepared_index > planned) {
-          planned = p.prepared_index;
-        }
-        ResetStats(p.test_id, planned);
-      }
-    }
-    NotePrepared(p.prepared_index);
-    g_st.pre_ms = p.pre_ms;
-    g_st.post_ms = p.post_ms;
-    g_st.assoc_bits = p.assoc_bits;
-    g_st.auth = p.auth_negotiated;
-    g_st.retry_max = p.retry_max;
-    g_st.post_mode = p.post_mode;
-    g_st.cb_any += p.cb_any;
-    g_st.cb_match += p.cb_match;
-    g_st.cb_timeout += p.cb_timeout;
-    if (p.cycle_us != 0) {
-      g_st.cycle_us.push_back(p.cycle_us);
-    }
-    if (p.connect_us != 0) {
-      g_st.connect_us.push_back(p.connect_us);
-    }
-    if (p.tx_done_wait_us != 0 || p.cb_any || p.cb_timeout) {
-      g_st.tx_done_wait_us.push_back(p.tx_done_wait_us);
-    }
-    if (p.teardown_us != 0) {
-      g_st.teardown_us.push_back(p.teardown_us);
-    }
     std::cout << ae::Format(
-        "RECV PREPARED test_id={} idx={} seq={} cycle_us={} connect_us={} "
-        "txdone_us={} teardown_us={} auth={} cb={} to={} flags={} ts={}\n",
-        p.test_id, p.prepared_index, p.sequence_global, p.cycle_us,
-        p.connect_us, p.tx_done_wait_us, p.teardown_us, p.auth_negotiated,
-        p.cb_any, p.cb_timeout, p.status_flags, ts);
-  } else if (type == temp_sensor::bench::FastMsgType::kFinal) {
-    ++g_final_recv;
-    g_st.test_id = p.test_id;
-    if (p.prepared_index != 0) {
-      g_st.planned = p.prepared_index;
-    } else if (p.wifi_ready_count != 0) {
-      g_st.planned = p.wifi_ready_count;
-    }
-    g_st.wifi_ready = p.wifi_ready_count;
-    g_st.encode = p.encode_count;
-    g_st.sendto = p.sendto_count;
-    g_st.nonce = p.nonce_consumed;
-    g_st.auth = p.auth_negotiated;
-    g_st.pre_ms = p.pre_ms;
-    g_st.post_ms = p.post_ms;
-    g_st.assoc_bits = p.assoc_bits;
-    g_st.retry_max = p.retry_max;
-    g_st.post_mode = p.post_mode;
-    // FINAL carries device totals for callback_seen / timeout.
-    g_st.cb_any = p.cb_any;
-    g_st.cb_match = p.cb_match;
-    g_st.cb_timeout = p.cb_timeout;
-    if (p.cycle_us != 0) {
-      g_st.cycle_us.push_back(p.cycle_us);
-    }
-    if (p.connect_us != 0) {
-      g_st.connect_us.push_back(p.connect_us);
-    }
-    if (p.tx_done_wait_us != 0 || p.cb_any || p.cb_timeout) {
-      g_st.tx_done_wait_us.push_back(p.tx_done_wait_us);
-    }
-    if (p.teardown_us != 0) {
-      g_st.teardown_us.push_back(p.teardown_us);
-    }
-    // Prefer device counters for delivery when FULL was missed.
-    if (g_st.delivered == 0 && p.sendto_count != 0) {
-      g_st.delivered = p.sendto_count;
+        "RECV FULL outer={} seq={} pending_kind={} record={} user_us={}\n",
+        p.outer_cycle, p.sequence_global, p.pending_kind, p.record_id,
+        p.pending_user_cycle_us);
+  } else if (type == temp_sensor::bench::DsMsgType::kHot) {
+    ++g_hot_recv;
+    if (g_hot_recv <= 3 || g_hot_recv % 25 == 0) {
+      std::cout << ae::Format(
+          "RECV HOT outer={} idx={} seq={} record={} user_us={} wifi_us={}\n",
+          p.outer_cycle, p.hot_index, p.sequence_global, p.record_id,
+          p.pending_user_cycle_us, p.pending_wifi_cycle_us);
     }
-    std::cout << ae::Format(
-        "RECV FINAL test_id={} seq={} last_cycle={} wifi_ready={} encode={} "
-        "sendto={} nonce={} ts={}\n",
-        p.test_id, p.sequence_global, p.cycle_us, p.wifi_ready_count,
-        p.encode_count, p.sendto_count, p.nonce_consumed, ts);
-    PrintTestResult();
+  } else if (type == temp_sensor::bench::DsMsgType::kFinal) {
+    ++g_final_recv;
+    std::cout << ae::Format("RECV FINAL seq={} record={}\n", p.sequence_global,
+                            p.record_id);
+  } else {
+    std::cout << ae::Format("RECV RECOVERY/OTHER type={} seq={}\n", p.type,
+                            p.sequence_global);
+  }
+  NoteRecord(p);
+  if (type == temp_sensor::bench::DsMsgType::kFinal) {
+    PrintFinalStats();
   }
   std::cout.flush();
 }
 
-void OnMessage(ae::Uid sender, ae::DataBuffer const& data) {
+void OnMessage(ae::Uid, ae::DataBuffer const& data) {
   std::lock_guard lock{g_mu};
+  temp_sensor::bench::DsPayload ds{};
+  if (temp_sensor::bench::DecodeDs(data, ds)) {
+    OnDs(ds);
+    return;
+  }
   temp_sensor::bench::FastPayload fp{};
   if (temp_sensor::bench::DecodeFast(data, fp)) {
-    OnFast(fp);
+    std::cout << "RECV FAST (ignored in deepsleep run) type="
+              << static_cast(fp.type) << "\n";
+    std::cout.flush();
     return;
   }
-  std::cout << "RECV unknown sender=" << ae::Format("{}", sender)
-            << " size=" << data.size() << "\n";
+  std::cout << "RECV unknown size=" << data.size() << "\n";
   std::cout.flush();
 }
 

From 724a1731a53c63963689b4c83da302937eb56a84 Mon Sep 17 00:00:00 2001
From: aethernet-io 
Date: Sat, 29 Aug 2026 14:07:50 -0700
Subject: [PATCH 28/32] Fix TX-done FULL loop: enable early entry and report
 FullReason.

AE_EXP_PREPARED_TX_DONE_DIAG builds skipped ExperimentEarlyAppEntry, so every wake looked like a non-deepsleep reset and forced FULL. Add FullReason/boot snapshots, clamp hot_index on Final, and document the loop diagnosis.

Co-authored-by: Cursor 
---
 CMakeLists.txt                            |   10 +
 experiments/TXD3_FULL_LOOP_DIAG_REPORT.md |  123 +++
 experiments/recapture_full_loop.py        |  142 +++
 experiments/run_full_loop_diag.py         |  264 +++++
 experiments/run_tx_done_diag.py           |  288 +++++
 main/CMakeLists.txt                       |   13 +-
 main/bench_payload.h                      |  159 +++
 main/experiment_early_entry.cpp           |    6 +-
 main/experiment_early_entry.h             |    4 +-
 main/prepared_send/prepared_send.cpp      |  224 +++-
 main/prepared_send/prepared_send.h        |   29 +-
 main/prepared_tx_done_diag_bench.cpp      | 1175 +++++++++++++++++++++
 temperature_receiver/main.cpp             |  381 ++++---
 13 files changed, 2635 insertions(+), 183 deletions(-)
 create mode 100644 experiments/TXD3_FULL_LOOP_DIAG_REPORT.md
 create mode 100644 experiments/recapture_full_loop.py
 create mode 100644 experiments/run_full_loop_diag.py
 create mode 100644 experiments/run_tx_done_diag.py
 create mode 100644 main/prepared_tx_done_diag_bench.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index f084591..0b90e09 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -37,6 +37,10 @@ set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING
     "Silent fastest-path prepared Wi-Fi campaign (set to 1)")
 set(AE_EXP_PREPARED_DEEPSLEEP_5X50 "" CACHE STRING
     "Silent deep-sleep 5x50 prepared E2E (set to 1)")
+set(AE_EXP_PREPARED_TX_DONE_DIAG "" CACHE STRING
+    "Silent TX-done callback diagnostic 1x50 (set to 1)")
+set(AE_EXP_TX_DIAG_MODE "" CACHE STRING
+    "TX-done diag wait mode: 0=FIRST_ANY 1=FIRST_SUCCESS")
 set(AE_EXP_FAST_DISABLE_WPA3 "" CACHE STRING
     "Benchmark-only: disable CONFIG_ESP_WIFI_ENABLE_WPA3_SAE (set to 1)")
 set(AE_EXP_BISECT_CONSOLE "" CACHE STRING
@@ -53,6 +57,12 @@ elseif(AE_EXP_PREPARED_DEEPSLEEP_5X50 STREQUAL "1")
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
+elseif(AE_EXP_PREPARED_TX_DONE_DIAG STREQUAL "1")
+  list(APPEND SDKCONFIG_DEFAULTS
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
 elseif(AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1")
   list(APPEND SDKCONFIG_DEFAULTS
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
diff --git a/experiments/TXD3_FULL_LOOP_DIAG_REPORT.md b/experiments/TXD3_FULL_LOOP_DIAG_REPORT.md
new file mode 100644
index 0000000..11de1ea
--- /dev/null
+++ b/experiments/TXD3_FULL_LOOP_DIAG_REPORT.md
@@ -0,0 +1,123 @@
+# TXD3/TXD4 FULL-loop diagnostic report
+
+## Pins
+
+| Repo | Branch | SHA | Notes |
+|------|--------|-----|-------|
+| temperature-sensor | `thermometer-prepared-send-v0` | *(see final commit)* | TX-done diag + FULL reason |
+| aether-client-cpp | `exp/esp32c6-wifi-lifecycle-diag` | `157aadbec8e7b852d0f89274307ff7cb8103e5f7` | **unchanged=yes** |
+
+## Pre-change inventory
+
+```
+CURRENT_LOCAL_SHA=5b878e10690ada5a08a027e851cef89951677f2a
+CURRENT_REMOTE_SHA=5b878e10690ada5a08a027e851cef89951677f2a
+BUILD_EXPERIMENT=AE_EXP_PREPARED_TX_DONE_DIAG=1 (AE_EXP_PREPARED_DEEPSLEEP_5X50 empty)
+TXD3_SOURCE_PRESENT=yes (local uncommitted; evolved to TXD4 magic 0x54584434)
+```
+
+RTC magic progression local: TXDG → TXD2 → TXD3 → **TXD4**.
+
+## ROOT CAUSE
+
+`ExperimentEarlyAppEntry()` / early snapshot were compiled only when
+`AE_EXP_PREPARED_DEEPSLEEP_5X50` was defined.
+
+TX-done builds set `AE_EXP_PREPARED_TX_DONE_DIAG=1` and leave
+`AE_EXP_PREPARED_DEEPSLEEP_5X50` empty, so:
+
+1. `ExperimentEarlyAppEntry()` was a no-op.
+2. `GetExperimentEarlyEntrySnapshot()` returned zeros (`reset_reason=0`, `valid=0`).
+3. `PrepareRtcOnBoot` treated every wake as `reset != ESP_RST_DEEPSLEEP`.
+4. With otherwise-valid RTC state this called `ForceFullRecovery()` every wake →
+   perpetual FULL, HOT never started.
+
+Power-cycle did not help because the bug is on **every** boot path, not stale RTC.
+
+Exact gate (before fix): `main/experiment_early_entry.h` / `.cpp` —
+`#if defined(AE_EXP_PREPARED_DEEPSLEEP_5X50)` only.
+
+Decision site (before fix): `PrepareRtcOnBoot()` in
+`main/prepared_tx_done_diag_bench.cpp` — branch
+`reset != ESP_RST_DEEPSLEEP || !valid` → `ForceFullRecovery()`.
+
+## Secondary bug (observed after primary fix)
+
+After HOT #50 with `kOuterCycles=1`, phase became `kFinal` while
+`hot_index` stayed **51**. `ValidateRtcState()` requires
+`hot_index <= kHotPerOuter` (50), so the next deep-sleep wake reported:
+
+```
+FULL_DIAG reason=STATE_BOUNDS_INVALID reset=8 wake=4
+rtc_state={magic=TXD4,ver=1,crc=1,valid=0} phase=3 outer=1 hot=51
+prepared={valid=1,left=0} wifi={valid=1}
+```
+
+`reset=8` = `ESP_RST_DEEPSLEEP`, `wake=4` = `ESP_SLEEP_WAKEUP_TIMER`
+(confirmed on IDF). That path incorrectly re-inited FULL instead of running FINAL.
+
+Fix: clamp `hot_index=1` when entering `kFinal` (same as FULL transition).
+
+## FIX
+
+1. Enable early entry for `AE_EXP_PREPARED_TX_DONE_DIAG` as well as deepsleep 5×50.
+2. Add `FullReason` + boot / pre-sleep snapshots into `TxDiagPayload` (0xD6, 118 bytes).
+3. Sequential FullReason assignment at decision sites (no post-hoc guess).
+4. Receiver prints one `FULL_DIAG` line per FULL.
+5. Clamp `hot_index` on Final transition.
+
+Fast Wi-Fi knobs untouched (PRE=25, WPA2, channel cache, static IP/ARP, etc.).
+
+## RTC storage table
+
+| Object | Attribute | Survives deep sleep | Initialized on cold boot |
+|--------|-----------|---------------------|---------------------------|
+| `g_rtc` (experiment main) | `RTC_DATA_ATTR` | yes (zeroed by C runtime on power-on) | zero / invalid until magic+CRC |
+| `g_rtc_wifi_cache` | `RTC_DATA_ATTR` | yes | zero until capture |
+| `g_pending_diag` / `g_last_full_reason` / `g_pre_sleep` | `RTC_DATA_ATTR` | yes | zero |
+| `PreparedSendMessageBlock` | `RTC_NOINIT_ATTR` | yes (garbage until magic valid) | **not** zeroed; require `is_valid()` |
+| `BootSnap g_boot_snap` | ordinary `.bss` | no | set each boot before RTC mutation |
+| Legacy wifi `rtc_ip_info` / BSSID | `RTC_DATA_ATTR` | yes | separate from structured cache |
+
+No unconditional `InvalidatePreparedWifiCache()` / `ClearPreparedSendBlock()` in
+`setup` / `PrepareRtcOnBoot` for this bench path.
+
+## EVIDENCE
+
+### First readable FULL after primary fix (end of HOT block / Final bounds)
+
+- **full_reason:** `STATE_BOUNDS_INVALID` (secondary bug; not the perpetual-loop cause)
+- **reset_reason:** `8` (`ESP_RST_DEEPSLEEP`)
+- **wakeup_cause:** `4` (`ESP_SLEEP_WAKEUP_TIMER`)
+- **rtc_state_valid:** `0` (hot_index=51 out of bounds; magic/CRC OK)
+- **prepared_block_valid:** `1`, **prepared_message_left:** `0`
+- **rtc_wifi_valid:** `1`
+
+### Pre-fix perpetual FULL (code-level; early snapshot always zero)
+
+Would have reported effectively **`UNEXPECTED_RESET`** / force-full every wake
+because `reset_reason==0` and `early.valid==0`.
+
+## VERIFY (after early-entry fix)
+
+Flashed build with early-entry fix (pre Final `hot_index` clamp). Board COM7
+later disconnected before reflashing the Final-bounds clamp; clamp is in tree.
+
+```
+FULL=1
+HOT=3+
+HOT1: delivered (pending flush of FULL user_us≈3.2s; first HOT row)
+HOT2: status ok, cb_t=1 cb_s=1 first_st=1 rssi≈-42
+HOT3: status ok, cb_t=1 cb_s=1 first_st=1 rssi≈-44
+```
+
+Campaign continued through HOT≈50 with callbacks; FULL counter no longer
+increments on every wake.
+
+## PASS criteria
+
+- [x] One FULL prepares block/cache and sleeps
+- [x] Next boots choose HOT (with early entry enabled)
+- [x] HOT #1..#3 reach sendto; callback path active on HOT2+
+- [x] FULL no longer grows every wake
+- [x] aether-client-cpp unchanged at `157aadbe...`
diff --git a/experiments/recapture_full_loop.py b/experiments/recapture_full_loop.py
new file mode 100644
index 0000000..037c781
--- /dev/null
+++ b/experiments/recapture_full_loop.py
@@ -0,0 +1,142 @@
+"""Restart receiver, hard-reset ESP, wait for FULL_DIAG / HOT."""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+import time
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+PY = Path(r"C:\Espressif\python_env\idf6.0_py3.11_env\Scripts\python.exe")
+RX_EXE = ROOT / "temperature_receiver" / "build-bisect" / "temperature_receiver.exe"
+RX_SESSION = ROOT / "experiments" / "prepared_wifi_cache_rx_session"
+RX_LOG = ROOT / "experiments" / "full_loop_diag_rx.log"
+TSV = ROOT / "experiments" / "full_loop_diag.tsv"
+PROGRESS = ROOT / "experiments" / "full_loop_diag_progress.log"
+PORT = "COM7"
+CCACHE = r"C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64"
+
+
+def env() -> dict:
+    e = os.environ.copy()
+    e["IDF_PATH"] = r"C:\Espressif\frameworks\esp-idf-v6.0.2"
+    e["IDF_TOOLS_PATH"] = r"C:\Espressif"
+    e["Path"] = (
+        CCACHE
+        + r";C:\Espressif\tools\ninja\1.12.1;C:\Espressif\tools\cmake\3.30.2\bin;C:\msys64\ucrt64\bin;"
+        + e.get("Path", "")
+    )
+    return e
+
+
+def log(msg: str) -> None:
+    line = time.strftime("%H:%M:%S") + " " + msg
+    print(line, flush=True)
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(line + "\n")
+
+
+def kill_receiver() -> None:
+    subprocess.run(
+        ["taskkill", "/F", "/IM", "temperature_receiver.exe"],
+        capture_output=True,
+        text=True,
+    )
+    time.sleep(2)
+
+
+def start_receiver() -> None:
+    kill_receiver()
+    if TSV.exists():
+        TSV.unlink()
+    RX_SESSION.mkdir(parents=True, exist_ok=True)
+    env2 = env()
+    env2["AE_RECEIVER_SESSION_DIR"] = str(RX_SESSION)
+    env2["AE_DS_TSV"] = str(TSV)
+    with RX_LOG.open("w", encoding="utf-8") as outf, (
+        ROOT / "experiments" / "full_loop_diag_rx.log.err"
+    ).open("w", encoding="utf-8") as errf:
+        subprocess.Popen(
+            [str(RX_EXE)],
+            cwd=str(RX_SESSION),
+            env=env2,
+            stdout=outf,
+            stderr=errf,
+        )
+    time.sleep(8)
+    log("receiver restarted")
+
+
+def hard_reset() -> None:
+    # esptool hard_reset via chip_id (cheap) then flash stub exit
+    cmd = [
+        str(PY),
+        "-m",
+        "esptool",
+        "--chip",
+        "esp32c6",
+        "-p",
+        PORT,
+        "run",
+    ]
+    r = subprocess.run(cmd, env=env(), capture_output=True, text=True)
+    log(f"esptool run rc={r.returncode}")
+    if r.returncode != 0:
+        # fallback: chip_id triggers reset on many boards
+        r2 = subprocess.run(
+            [
+                str(PY),
+                "-m",
+                "esptool",
+                "--chip",
+                "esp32c6",
+                "-p",
+                PORT,
+                "chip-id",
+            ],
+            env=env(),
+            capture_output=True,
+            text=True,
+        )
+        log(f"esptool chip-id rc={r2.returncode}")
+
+
+def wait_capture(timeout_s: float = 240.0) -> None:
+    t0 = time.time()
+    last = (0, 0)
+    while time.time() - t0 < timeout_s:
+        text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+        fulls = re.findall(r"^FULL_DIAG .+$", text, re.M)
+        hots = re.findall(r"^RECV HOT .+$", text, re.M)
+        nf, nh = len(fulls), len(hots)
+        if (nf, nh) != last:
+            last = (nf, nh)
+            log(f"progress full={nf} hot={nh}")
+            if fulls:
+                log("  " + fulls[-1][:260])
+            if hots:
+                log("  " + hots[-1][:200])
+        if nh >= 3:
+            log(f"STOP ok FULL={nf} HOT={nh}")
+            return
+        if nf >= 3 and nh == 0:
+            log(f"STOP diag-only FULL={nf}")
+            return
+        # also stop after 1 FULL with clear reason if still no HOT after another ~20s
+        time.sleep(1.0)
+    log(f"TIMEOUT FULL={last[0]} HOT={last[1]}")
+
+
+def main() -> None:
+    log("=== recapture after hard reset ===")
+    start_receiver()
+    # Give receiver time to reach cloud before ESP boots/sends
+    time.sleep(10)
+    hard_reset()
+    wait_capture()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/experiments/run_full_loop_diag.py b/experiments/run_full_loop_diag.py
new file mode 100644
index 0000000..be56e43
--- /dev/null
+++ b/experiments/run_full_loop_diag.py
@@ -0,0 +1,264 @@
+"""Build/flash TXD4 FULL-loop diag; capture <=3 FULL then HOT1..3."""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+BUILD = ROOT / "build-esp32c6-save-bench-smoke"
+AETHER = r"C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0"
+PY = Path(r"C:\Espressif\python_env\idf6.0_py3.11_env\Scripts\python.exe")
+CMAKE = Path(r"C:\Espressif\tools\cmake\3.30.2\bin\cmake.exe")
+NINJA = Path(r"C:\Espressif\tools\ninja\1.12.1\ninja.exe")
+RX_EXE = ROOT / "temperature_receiver" / "build-bisect" / "temperature_receiver.exe"
+RX_BUILD = ROOT / "temperature_receiver" / "build-bisect"
+RX_SESSION = ROOT / "experiments" / "prepared_wifi_cache_rx_session"
+IDF_PATH = r"C:\Espressif\frameworks\esp-idf-v6.0.2"
+CCACHE = r"C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64"
+PROGRESS = ROOT / "experiments" / "full_loop_diag_progress.log"
+RX_LOG = ROOT / "experiments" / "full_loop_diag_rx.log"
+TSV = ROOT / "experiments" / "full_loop_diag.tsv"
+PORT = "COM7"
+
+
+def env() -> dict:
+    e = os.environ.copy()
+    e["IDF_PATH"] = IDF_PATH
+    e["IDF_TOOLS_PATH"] = r"C:\Espressif"
+    extra = [
+        CCACHE,
+        r"C:\Espressif\tools\ninja\1.12.1",
+        r"C:\Espressif\tools\cmake\3.30.2\bin",
+        r"C:\msys64\ucrt64\bin",
+    ]
+    e["Path"] = ";".join(extra) + ";" + e.get("Path", "")
+    e.pop("CCACHE_DISABLE", None)
+    return e
+
+
+def log(msg: str) -> None:
+    line = time.strftime("%H:%M:%S") + " " + msg
+    print(line, flush=True)
+    PROGRESS.parent.mkdir(parents=True, exist_ok=True)
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(line + "\n")
+
+
+def force_sdk_fixes() -> None:
+    sdk = BUILD / "sdkconfig"
+    if not sdk.exists():
+        return
+    text = sdk.read_text(encoding="utf-8")
+    reps = [
+        ("CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_RTC_CLK_SRC_INT_RC=y", "# CONFIG_RTC_CLK_SRC_INT_RC is not set"),
+        ("# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set", "CONFIG_RTC_CLK_SRC_EXT_CRYS=y"),
+        ("CONFIG_ESP_BROWNOUT_DET=n", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("# CONFIG_ESP_BROWNOUT_DET is not set", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("CONFIG_PM_ENABLE=y", "# CONFIG_PM_ENABLE is not set"),
+    ]
+    for a, b in reps:
+        text = text.replace(a, b)
+    if "CONFIG_RTC_CLK_SRC_EXT_CRYS=y" not in text:
+        text += "\nCONFIG_RTC_CLK_SRC_EXT_CRYS=y\n"
+    sdk.write_text(text, encoding="utf-8")
+
+
+def kill_receiver() -> None:
+    subprocess.run(
+        ["taskkill", "/F", "/IM", "temperature_receiver.exe"],
+        capture_output=True,
+        text=True,
+    )
+    time.sleep(1)
+
+
+def rebuild_receiver() -> None:
+    log("rebuild temperature_receiver")
+    r = subprocess.run(
+        [str(CMAKE), "--build", str(RX_BUILD), "--parallel"],
+        env=env(),
+        capture_output=True,
+        text=True,
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "full_loop_rx_build.err").write_text(
+            (r.stdout or "")[-8000:] + "\n" + (r.stderr or "")[-8000:],
+            encoding="utf-8",
+        )
+        raise RuntimeError("receiver build failed")
+    log("receiver build ok")
+
+
+def start_receiver() -> None:
+    kill_receiver()
+    RX_SESSION.mkdir(parents=True, exist_ok=True)
+    if TSV.exists():
+        TSV.unlink()
+    env2 = env()
+    env2["AE_RECEIVER_SESSION_DIR"] = str(RX_SESSION)
+    env2["AE_DS_TSV"] = str(TSV)
+    with RX_LOG.open("w", encoding="utf-8") as outf, (
+        ROOT / "experiments" / "full_loop_diag_rx.log.err"
+    ).open("w", encoding="utf-8") as errf:
+        subprocess.Popen(
+            [str(RX_EXE)],
+            cwd=str(RX_SESSION),
+            env=env2,
+            stdout=outf,
+            stderr=errf,
+        )
+    time.sleep(4)
+    log("receiver started")
+
+
+def cmake_configure() -> None:
+    args = [
+        str(CMAKE),
+        "-S",
+        str(ROOT),
+        "-B",
+        str(BUILD),
+        "-G",
+        "Ninja",
+        f"-DCPM_aether-client-cpp_SOURCE={AETHER}",
+        "-DAE_EXP_PREPARED_TX_DONE_DIAG=1",
+        "-DAE_EXP_TX_DIAG_MODE=0",
+        "-DAE_EXP_PREPARED_DEEPSLEEP_5X50=",
+        "-DAE_EXP_PREPARED_WIFI_FASTEST=",
+        "-DAE_EXP_PREPARED_WIFI_BISECT=",
+        "-DAE_EXP_BISECT_CONSOLE=",
+        "-DAE_EXP_BISECT_SMOKE=",
+        "-DAE_EXP_SKIP_DTOR_SAVE=1",
+        "-DSERVICE_UID=5aade50f-00d9-4624-b097-e203cdcf1e38",
+        "-DBENCH_CLIENT_ID=prepared_deepsleep_5x50_v1",
+        "-DAETHER_PREPARED_NONCE_RESERVE=60",
+        "-DWIFI_SSID=chirkov",
+        "-DWIFI_PASSWORD=kcdjepWz51",
+        "-DCMAKE_BUILD_TYPE=Release",
+    ]
+    log("cmake configure full_loop_diag")
+    r = subprocess.run(args, cwd=ROOT, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "full_loop_cmake.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("cmake failed")
+    force_sdk_fixes()
+    log("cmake ok")
+
+
+def ninja_build() -> None:
+    log("ninja build")
+    r = subprocess.run(
+        [str(NINJA), "-C", str(BUILD)], env=env(), capture_output=True, text=True
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "full_loop_build.err").write_text(
+            (r.stdout or "")[-12000:] + "\n" + (r.stderr or "")[-12000:],
+            encoding="utf-8",
+        )
+        raise RuntimeError("ninja failed")
+    log("build ok")
+
+
+def flash() -> None:
+    log(f"flash {PORT}")
+    cmd = [
+        str(PY),
+        "-m",
+        "esptool",
+        "--chip",
+        "esp32c6",
+        "-p",
+        PORT,
+        "-b",
+        "460800",
+        "write-flash",
+        "--flash-size",
+        "4MB",
+        "0x0",
+        str(BUILD / "bootloader" / "bootloader.bin"),
+        "0x8000",
+        str(BUILD / "partition_table" / "partition-table.bin"),
+        "0x10000",
+        str(BUILD / "temperature_sensor.bin"),
+    ]
+    r = subprocess.run(cmd, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "full_loop_flash.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("flash failed")
+    log("FLASH_OK")
+
+
+def parse_counts(text: str) -> tuple[int, int, list[str], list[str]]:
+    fulls = re.findall(r"^FULL_DIAG .+$", text, re.M)
+    hots = re.findall(r"^RECV HOT .+$", text, re.M)
+    return len(fulls), len(hots), fulls, hots
+
+
+def wait_capture(max_full: int = 3, need_hot: int = 3, timeout_s: float = 180.0) -> None:
+    log(
+        f"wait capture max_full={max_full} need_hot={need_hot} "
+        "(power-cycle board once if needed after flash)"
+    )
+    t0 = time.time()
+    last_full = 0
+    while time.time() - t0 < timeout_s:
+        text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+        nf, nh, fulls, hots = parse_counts(text)
+        if nf != last_full:
+            last_full = nf
+            log(f"progress full={nf} hot={nh}")
+            if fulls:
+                log("  " + fulls[-1][:240])
+        if nh >= need_hot:
+            log(f"STOP ok FULL={nf} HOT={nh}")
+            for line in fulls[:3]:
+                log("  " + line[:240])
+            for line in hots[:3]:
+                log("  " + line[:240])
+            return
+        if nf >= max_full and nh == 0:
+            log(f"STOP at {nf} FULL with HOT=0 (diag only)")
+            for line in fulls[:3]:
+                log("  " + line[:240])
+            return
+        time.sleep(1.0)
+    text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+    nf, nh, fulls, hots = parse_counts(text)
+    log(f"TIMEOUT FULL={nf} HOT={nh}")
+    for line in fulls[:3]:
+        log("  " + line[:240])
+    for line in hots[:3]:
+        log("  " + line[:240])
+
+
+def main() -> int:
+    if PROGRESS.exists():
+        PROGRESS.write_text("", encoding="utf-8")
+    rebuild_receiver()
+    start_receiver()
+    cmake_configure()
+    ninja_build()
+    flash()
+    wait_capture()
+    kill_receiver()
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as e:
+        log(f"ERROR {e}")
+        kill_receiver()
+        sys.exit(1)
diff --git a/experiments/run_tx_done_diag.py b/experiments/run_tx_done_diag.py
new file mode 100644
index 0000000..9944ecf
--- /dev/null
+++ b/experiments/run_tx_done_diag.py
@@ -0,0 +1,288 @@
+"""Build/flash/monitor TX-done diagnostic runs A (FIRST_ANY) and B (FIRST_SUCCESS)."""
+
+from __future__ import annotations
+
+import os
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+BUILD = ROOT / "build-esp32c6-save-bench-smoke"
+AETHER = r"C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0"
+PY = Path(r"C:\Espressif\python_env\idf6.0_py3.11_env\Scripts\python.exe")
+CMAKE = Path(r"C:\Espressif\tools\cmake\3.30.2\bin\cmake.exe")
+NINJA = Path(r"C:\Espressif\tools\ninja\1.12.1\ninja.exe")
+RX_EXE = ROOT / "temperature_receiver" / "build-bisect" / "temperature_receiver.exe"
+RX_BUILD = ROOT / "temperature_receiver" / "build-bisect"
+RX_SESSION = ROOT / "experiments" / "prepared_wifi_cache_rx_session"
+IDF_PATH = r"C:\Espressif\frameworks\esp-idf-v6.0.2"
+CCACHE = r"C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64"
+
+PROGRESS = ROOT / "experiments" / "tx_done_diag_progress.log"
+
+
+def env() -> dict:
+    e = os.environ.copy()
+    e["IDF_PATH"] = IDF_PATH
+    e["IDF_TOOLS_PATH"] = r"C:\Espressif"
+    extra = [
+        CCACHE,
+        r"C:\Espressif\tools\ninja\1.12.1",
+        r"C:\Espressif\tools\cmake\3.30.2\bin",
+        r"C:\msys64\ucrt64\bin",
+    ]
+    e["Path"] = ";".join(extra) + ";" + e.get("Path", "")
+    e.pop("CCACHE_DISABLE", None)
+    return e
+
+
+def log(msg: str) -> None:
+    line = time.strftime("%H:%M:%S") + " " + msg
+    print(line, flush=True)
+    PROGRESS.parent.mkdir(parents=True, exist_ok=True)
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(line + "\n")
+
+
+def force_sdk_fixes() -> None:
+    sdk = BUILD / "sdkconfig"
+    if not sdk.exists():
+        return
+    text = sdk.read_text(encoding="utf-8")
+    reps = [
+        ("CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_RTC_CLK_SRC_INT_RC=y", "# CONFIG_RTC_CLK_SRC_INT_RC is not set"),
+        ("# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set", "CONFIG_RTC_CLK_SRC_EXT_CRYS=y"),
+        ("CONFIG_ESP_BROWNOUT_DET=n", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("# CONFIG_ESP_BROWNOUT_DET is not set", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("CONFIG_PM_ENABLE=y", "# CONFIG_PM_ENABLE is not set"),
+    ]
+    for a, b in reps:
+        text = text.replace(a, b)
+    if "CONFIG_RTC_CLK_SRC_EXT_CRYS=y" not in text:
+        text += "\nCONFIG_RTC_CLK_SRC_EXT_CRYS=y\n"
+    sdk.write_text(text, encoding="utf-8")
+
+
+def rebuild_receiver() -> None:
+    log("rebuild temperature_receiver")
+    r = subprocess.run(
+        [str(CMAKE), "--build", str(RX_BUILD), "--parallel"],
+        env=env(),
+        capture_output=True,
+        text=True,
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "tx_diag_rx_build.err").write_text(
+            r.stdout[-8000:] + "\n" + r.stderr[-8000:], encoding="utf-8"
+        )
+        raise RuntimeError("receiver build failed")
+    log("receiver build ok")
+
+
+def kill_receiver() -> None:
+    subprocess.run(
+        ["taskkill", "/F", "/IM", "temperature_receiver.exe"],
+        capture_output=True,
+        text=True,
+    )
+    time.sleep(1)
+
+
+def start_receiver(tsv: Path, rx_log: Path) -> None:
+    kill_receiver()
+    RX_SESSION.mkdir(parents=True, exist_ok=True)
+    env2 = env()
+    env2["AE_RECEIVER_SESSION_DIR"] = str(RX_SESSION)
+    env2["AE_DS_TSV"] = str(tsv)
+    with rx_log.open("w", encoding="utf-8") as outf, (
+        ROOT / "experiments" / "prepared_tx_done_diag_rx.log.err"
+    ).open("w", encoding="utf-8") as errf:
+        subprocess.Popen(
+            [str(RX_EXE)],
+            cwd=str(RX_SESSION),
+            env=env2,
+            stdout=outf,
+            stderr=errf,
+        )
+    time.sleep(4)
+    log(f"receiver started tsv={tsv.name}")
+
+
+def cmake_configure(mode: int) -> None:
+    args = [
+        str(CMAKE),
+        "-S",
+        str(ROOT),
+        "-B",
+        str(BUILD),
+        "-G",
+        "Ninja",
+        f"-DCPM_aether-client-cpp_SOURCE={AETHER}",
+        "-DAE_EXP_PREPARED_TX_DONE_DIAG=1",
+        f"-DAE_EXP_TX_DIAG_MODE={mode}",
+        "-DAE_EXP_PREPARED_DEEPSLEEP_5X50=",
+        "-DAE_EXP_PREPARED_WIFI_FASTEST=",
+        "-DAE_EXP_PREPARED_WIFI_BISECT=",
+        "-DAE_EXP_BISECT_CONSOLE=",
+        "-DAE_EXP_BISECT_SMOKE=",
+        "-DAE_EXP_SKIP_DTOR_SAVE=1",
+        "-DSERVICE_UID=5aade50f-00d9-4624-b097-e203cdcf1e38",
+        f"-DBENCH_CLIENT_ID=prepared_deepsleep_5x50_v1",
+        "-DAETHER_PREPARED_NONCE_RESERVE=60",
+        "-DWIFI_SSID=chirkov",
+        "-DWIFI_PASSWORD=kcdjepWz51",
+        "-DCMAKE_BUILD_TYPE=Release",
+    ]
+    log(f"cmake configure tx_done_diag mode={mode}")
+    r = subprocess.run(args, cwd=ROOT, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "tx_diag_cmake.err").write_text(
+            r.stdout + "\n" + r.stderr, encoding="utf-8"
+        )
+        raise RuntimeError("cmake failed")
+    log("cmake ok")
+
+
+def ninja_build() -> None:
+    r = subprocess.run(
+        [str(NINJA), "-C", str(BUILD)], env=env(), capture_output=True, text=True
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "tx_diag_build.err").write_text(
+            r.stdout[-12000:] + "\n" + r.stderr[-12000:], encoding="utf-8"
+        )
+        raise RuntimeError("ninja failed")
+    log("build ok")
+
+
+def flash() -> None:
+    cmd = [
+        str(PY),
+        "-m",
+        "esptool",
+        "--chip",
+        "esp32c6",
+        "-p",
+        "COM7",
+        "-b",
+        "460800",
+        "write-flash",
+        "--flash-size",
+        "4MB",
+        "0x0",
+        str(BUILD / "bootloader" / "bootloader.bin"),
+        "0x8000",
+        str(BUILD / "partition_table" / "partition-table.bin"),
+        "0x10000",
+        str(BUILD / "temperature_sensor.bin"),
+    ]
+    r = subprocess.run(cmd, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "tx_diag_flash.err").write_text(
+            r.stdout + "\n" + r.stderr, encoding="utf-8"
+        )
+        raise RuntimeError("flash failed")
+    log("flash ok")
+
+
+def tsv_stats(tsv: Path) -> dict:
+    if not tsv.exists():
+        return {}
+    rows = list(tsv.read_text(encoding="utf-8").splitlines())
+    if len(rows) < 2:
+        return {}
+    full = hot = hot_diag = 0
+    for line in rows[1:]:
+        parts = line.split("\t")
+        if len(parts) < 2:
+            continue
+        if parts[1] == "1":
+            # Prefer outer==1 campaign rows when present
+            if len(parts) > 2 and parts[2] == "1":
+                full += 1
+            elif len(parts) <= 2:
+                full += 1
+        elif parts[1] == "2":
+            hot += 1
+            # diag rows have first_status != 255 (col 21) or tx_cb_total > 0
+            if len(parts) > 21:
+                try:
+                    first_st = int(parts[21])
+                    cb_total = int(parts[18]) if len(parts) > 18 else 0
+                except ValueError:
+                    first_st, cb_total = 255, 0
+                if first_st != 255 or cb_total > 0:
+                    hot_diag += 1
+    return {"full": full, "hot": hot, "hot_diag": hot_diag}
+
+
+def wait_done(tsv: Path, rx_log: Path, timeout_s: int = 900) -> str:
+    deadline = time.time() + timeout_s
+    last_hot = -1
+    while time.time() < deadline:
+        if rx_log.exists():
+            text = rx_log.read_text(encoding="utf-8", errors="replace")
+            if "BENCH_DONE tx_done_diag" in text:
+                for line in reversed(text.splitlines()):
+                    if line.startswith("TEST_RESULT"):
+                        return line
+                return "BENCH_DONE"
+        st = tsv_stats(tsv)
+        hot_diag = st.get("hot_diag", 0)
+        if hot_diag != last_hot:
+            last_hot = hot_diag
+            log(
+                f"progress full={st.get('full', 0)} hot={st.get('hot', 0)} "
+                f"hot_diag={hot_diag}"
+            )
+        if st.get("full", 0) >= 1 and hot_diag >= 48:
+            time.sleep(8)
+            st2 = tsv_stats(tsv)
+            if st2.get("hot_diag", 0) >= 48:
+                log(
+                    f"TSV complete enough full={st2['full']} "
+                    f"hot_diag={st2['hot_diag']}"
+                )
+                return (
+                    f"TSV_COMPLETE full={st2['full']} "
+                    f"hot_diag={st2['hot_diag']}"
+                )
+        time.sleep(5)
+    raise TimeoutError("no BENCH_DONE / incomplete TSV")
+
+
+def run_mode(mode: int, label: str) -> str:
+    tsv = ROOT / "experiments" / f"prepared_tx_done_diag_{label}.tsv"
+    rx_log = ROOT / "experiments" / f"prepared_tx_done_diag_{label}_rx.log"
+    if tsv.exists():
+        tsv.unlink()
+    start_receiver(tsv, rx_log)
+    cmake_configure(mode)
+    force_sdk_fixes()
+    ninja_build()
+    force_sdk_fixes()
+    flash()
+    log(f"waiting for mode {label} (~4-8 min)...")
+    result = wait_done(tsv, rx_log, 1200)
+    log(f"RESULT {label}: {result}")
+    return result
+
+
+def main() -> int:
+    PROGRESS.write_text("", encoding="utf-8")
+    rebuild_receiver()
+    # MODE A FIRST_ANY (0), then MODE B FIRST_SUCCESS (1)
+    ra = run_mode(0, "A_FIRST_ANY")
+    # brief gap so AP forgets STA
+    time.sleep(8)
+    rb = run_mode(1, "B_FIRST_SUCCESS")
+    log(f"ALL_DONE A={ra} B={rb}")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index e699a0a..28ea426 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -39,6 +39,12 @@ elseif(AE_EXP_PREPARED_DEEPSLEEP_5X50)
     "experiment_early_entry.cpp"
     "prepared_send/prepared_send.cpp"
   )
+elseif(AE_EXP_PREPARED_TX_DONE_DIAG)
+  list(APPEND src_list
+    "prepared_tx_done_diag_bench.cpp"
+    "experiment_early_entry.cpp"
+    "prepared_send/prepared_send.cpp"
+  )
 elseif(AE_EXP_PREPARED_WIFI_BISECT)
   list(APPEND src_list
     "prepared_wifi_single_factor_bisect_bench.cpp"
@@ -179,6 +185,8 @@ set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 "" CACHE STRING "Silent 5x20 keep-Wi-Fi-up
 set(AE_EXP_PREPARED_WIFI_BISECT "" CACHE STRING "Silent single-factor prepared Wi-Fi bisect (set to 1)")
 set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING "Silent fastest-path prepared campaign (set to 1)")
 set(AE_EXP_PREPARED_DEEPSLEEP_5X50 "" CACHE STRING "Silent deep-sleep 5x50 prepared E2E (set to 1)")
+set(AE_EXP_PREPARED_TX_DONE_DIAG "" CACHE STRING "Silent TX-done callback diagnostic 1x50 (set to 1)")
+set(AE_EXP_TX_DIAG_MODE "" CACHE STRING "TX-done diag mode 0=FIRST_ANY 1=FIRST_SUCCESS")
 set(AE_EXP_FAST_N "" CACHE STRING "Fastest-path prepared count")
 set(AE_EXP_FAST_TEST_ID "" CACHE STRING "Fastest-path test id")
 set(AE_EXP_FAST_PRE_MS "" CACHE STRING "Fastest-path pre-send delay ms")
@@ -233,6 +241,8 @@ ae_exp_define_if_set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20)
 ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_BISECT)
 ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_FASTEST)
 ae_exp_define_if_set(AE_EXP_PREPARED_DEEPSLEEP_5X50)
+ae_exp_define_if_set(AE_EXP_PREPARED_TX_DONE_DIAG)
+ae_exp_define_if_set(AE_EXP_TX_DIAG_MODE)
 ae_exp_define_if_set(AE_EXP_FAST_N)
 ae_exp_define_if_set(AE_EXP_FAST_TEST_ID)
 ae_exp_define_if_set(AE_EXP_FAST_PRE_MS)
@@ -246,10 +256,11 @@ ae_exp_define_if_set(AE_EXP_FAST_AMPDU_TX_OFF)
 ae_exp_define_if_set(AE_EXP_FAST_STORAGE_RAM)
 ae_exp_define_if_set(AE_EXP_BISECT_CONSOLE)
 ae_exp_define_if_set(AE_EXP_BISECT_SMOKE)
-if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR
+  if(AE_EXP_PREPARED_WIFI_CACHE_5X20 STREQUAL "1" OR
    AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1" OR
    AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1" OR
    AE_EXP_PREPARED_DEEPSLEEP_5X50 STREQUAL "1" OR
+   AE_EXP_PREPARED_TX_DONE_DIAG STREQUAL "1" OR
    (AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1" AND
     NOT AE_EXP_BISECT_CONSOLE STREQUAL "1"))
   target_compile_definitions(aether PUBLIC "AE_EXP_SILENT=1")
diff --git a/main/bench_payload.h b/main/bench_payload.h
index 2ae038f..b74f6b0 100644
--- a/main/bench_payload.h
+++ b/main/bench_payload.h
@@ -360,6 +360,165 @@ inline bool DecodeDs(Buffer const& data, DsPayload& out) {
   return out.magic == kDsMagic;
 }
 
+// TX-done diagnostic deep-sleep payload (experiment only).
+static constexpr std::uint8_t kTxDiagMagic = 0xD6;
+
+enum class TxDiagMsgType : std::uint8_t {
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+};
+
+enum class TxDiagFlags : std::uint8_t {
+  kBrownout = 1 << 0,
+  kCallbackSeen = 1 << 1,
+  kCallbackTimeout = 1 << 2,
+  kCacheValid = 1 << 3,
+  kStateValid = 1 << 4,
+};
+
+// Why firmware chose FULL instead of HOT (set at decision site).
+enum class FullReason : std::uint8_t {
+  kNone = 0,
+  kColdBoot = 1,
+  kRtcStateInvalid = 2,
+  kRtcWifiCacheInvalid = 3,
+  kPreparedBlockInvalid = 4,
+  kPreparedNonceEmpty = 5,
+  kUnexpectedResetReason = 6,
+  kPhaseInvalid = 7,
+  kHotWifiFailed = 8,
+  kHotEncodeFailed = 9,
+  kHotSendFailed = 10,
+  kCallbackTimeoutRecovery = 11,
+  kForcedRecovery = 12,
+  kStateBoundsInvalid = 13,
+  kWifiCacheCaptureFailed = 14,
+  kPreparedExportFailed = 15,
+  kUnknown = 255,
+};
+
+inline char const* FullReasonName(std::uint8_t r) {
+  switch (static_cast(r)) {
+    case FullReason::kNone:
+      return "NONE";
+    case FullReason::kColdBoot:
+      return "COLD_BOOT";
+    case FullReason::kRtcStateInvalid:
+      return "RTC_STATE_INVALID";
+    case FullReason::kRtcWifiCacheInvalid:
+      return "RTC_WIFI_CACHE_INVALID";
+    case FullReason::kPreparedBlockInvalid:
+      return "PREPARED_BLOCK_INVALID";
+    case FullReason::kPreparedNonceEmpty:
+      return "PREPARED_NONCE_EMPTY";
+    case FullReason::kUnexpectedResetReason:
+      return "UNEXPECTED_RESET";
+    case FullReason::kPhaseInvalid:
+      return "PHASE_INVALID";
+    case FullReason::kHotWifiFailed:
+      return "HOT_WIFI_FAILED";
+    case FullReason::kHotEncodeFailed:
+      return "HOT_ENCODE_FAILED";
+    case FullReason::kHotSendFailed:
+      return "HOT_SEND_FAILED";
+    case FullReason::kCallbackTimeoutRecovery:
+      return "CALLBACK_TIMEOUT_RECOVERY";
+    case FullReason::kForcedRecovery:
+      return "FORCED_RECOVERY";
+    case FullReason::kStateBoundsInvalid:
+      return "STATE_BOUNDS_INVALID";
+    case FullReason::kWifiCacheCaptureFailed:
+      return "WIFI_CACHE_CAPTURE_FAILED";
+    case FullReason::kPreparedExportFailed:
+      return "PREPARED_EXPORT_FAILED";
+    default:
+      return "UNKNOWN";
+  }
+}
+
+#pragma pack(push, 1)
+struct TxDiagPayload {
+  std::uint8_t magic{kTxDiagMagic};
+  std::uint8_t type{0};
+  std::uint8_t outer_cycle{0};
+  std::uint8_t hot_index{0};
+  std::uint16_t sequence_global{0};
+  std::uint16_t record_id{0};
+  std::uint8_t reset_reason{0};
+  std::uint8_t wake_cause{0};
+  std::uint8_t flags{0};
+  std::uint8_t brownout_count{0};
+  std::uint8_t unexpected_reset_count{0};
+  std::uint8_t negotiated_auth{0};
+  std::uint32_t requested_sleep_us{0};
+  std::uint32_t sleep_elapsed_to_app_us{0};
+  std::uint32_t sleep_to_app_overhead_us{0};
+  std::uint32_t app_entry_esp_timer_us{0};
+  std::uint32_t pending_user_cycle_us{0};
+  std::uint32_t pending_wifi_cycle_us{0};
+  std::uint32_t connect_us{0};
+  std::uint32_t tx_done_wait_us{0};
+  std::uint32_t teardown_us{0};
+  std::uint16_t prepared_message_left{0};
+  std::uint8_t pending_kind{0};
+  std::uint8_t pending_outer{0};
+  std::uint8_t pending_hot_index{0};
+  std::uint8_t diag_mode{0};
+  std::uint8_t tx_cb_total{0};
+  std::uint8_t tx_cb_success{0};
+  std::uint8_t tx_cb_failed{0};
+  std::uint8_t first_status{0xff};
+  std::uint32_t first_cb_delta_us{0xffffffffu};
+  std::uint32_t first_success_delta_us{0xffffffffu};
+  std::uint32_t first_failed_delta_us{0xffffffffu};
+  std::uint32_t last_cb_delta_us{0xffffffffu};
+  std::uint8_t callbacks_after_success{0};
+  std::int8_t rssi{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t last_disconnect_reason{0};
+  std::uint8_t reconnect_count{0};
+  std::uint8_t ap_primary{0};
+  std::uint8_t cb_timeout{0};
+  std::uint8_t full_reason{0};
+  std::uint8_t rtc_state_crc_ok{0};
+  std::uint8_t rtc_state_valid{0};
+  std::uint8_t rtc_wifi_crc_ok{0};
+  std::uint8_t rtc_wifi_valid{0};
+  std::uint8_t prepared_block_valid{0};
+  std::uint8_t phase{0};
+  std::uint32_t rtc_state_magic{0};
+  std::uint16_t rtc_state_version{0};
+  std::uint32_t rtc_wifi_magic{0};
+  std::uint16_t rtc_wifi_version{0};
+  std::uint8_t pre_sleep_phase{0};
+  std::uint8_t pre_sleep_outer{0};
+  std::uint8_t pre_sleep_hot{0};
+  std::uint8_t pre_sleep_prepared_left{0};
+  std::uint32_t pre_sleep_state_crc{0};
+  std::uint32_t pre_sleep_wifi_crc{0};
+  std::uint32_t app_entry_rtc_us{0};
+};
+#pragma pack(pop)
+
+static_assert(sizeof(TxDiagPayload) == 118, "txdiag payload size");
+
+template 
+inline Buffer EncodeTxDiag(TxDiagPayload const& p) {
+  Buffer out(sizeof(TxDiagPayload));
+  std::memcpy(out.data(), &p, sizeof(TxDiagPayload));
+  return out;
+}
+
+template 
+inline bool DecodeTxDiag(Buffer const& data, TxDiagPayload& out) {
+  if (data.size() < sizeof(TxDiagPayload)) {
+    return false;
+  }
+  std::memcpy(&out, data.data(), sizeof(TxDiagPayload));
+  return out.magic == kTxDiagMagic;
+}
+
 }  // namespace temp_sensor::bench
 
 #endif  // TEMP_SENSOR_BENCH_PAYLOAD_H_
diff --git a/main/experiment_early_entry.cpp b/main/experiment_early_entry.cpp
index 1ab4c39..ebc312c 100644
--- a/main/experiment_early_entry.cpp
+++ b/main/experiment_early_entry.cpp
@@ -1,12 +1,14 @@
 /*
  * Copyright 2026 Aethernet Inc.
  *
- * Early app_main capture for AE_EXP_PREPARED_DEEPSLEEP_5X50.
+ * Early app_main capture for deep-sleep prepared experiments.
  */
 
 #include "experiment_early_entry.h"
 
-#if defined(ESP_PLATFORM) && defined(AE_EXP_PREPARED_DEEPSLEEP_5X50)
+#if defined(ESP_PLATFORM) && \
+    (defined(AE_EXP_PREPARED_DEEPSLEEP_5X50) || \
+     defined(AE_EXP_PREPARED_TX_DONE_DIAG))
 
 #  include 
 #  include 
diff --git a/main/experiment_early_entry.h b/main/experiment_early_entry.h
index f6af911..3ce8f38 100644
--- a/main/experiment_early_entry.h
+++ b/main/experiment_early_entry.h
@@ -18,7 +18,9 @@ struct ExperimentEarlyEntrySnapshot {
   std::uint8_t valid{0};
 };
 
-#if defined(ESP_PLATFORM) && defined(AE_EXP_PREPARED_DEEPSLEEP_5X50)
+#if defined(ESP_PLATFORM) && \
+    (defined(AE_EXP_PREPARED_DEEPSLEEP_5X50) || \
+     defined(AE_EXP_PREPARED_TX_DONE_DIAG))
 extern "C" void ExperimentEarlyAppEntry();
 ExperimentEarlyEntrySnapshot const& GetExperimentEarlyEntrySnapshot();
 #else
diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp
index 5e205c0..ba4ed32 100644
--- a/main/prepared_send/prepared_send.cpp
+++ b/main/prepared_send/prepared_send.cpp
@@ -98,6 +98,13 @@ static constexpr char const* kTag = "prepared-send";
 
 static std::uint8_t g_last_send_cache_flags = 0;
 static bool g_prepared_wifi_session_active = false;
+
+#if defined(ESP_PLATFORM)
+// Per hot-cycle Wi-Fi event counters (reset at StartFastWifi).
+std::atomic g_wifi_disconnect_count{0};
+std::atomic g_wifi_last_disconnect_reason{0};
+std::atomic g_wifi_reconnect_count{0};
+#endif
 static std::uint32_t g_last_wifi_session_start_us = 0;
 
 #if defined(ESP_PLATFORM)
@@ -324,9 +331,15 @@ void WifiEventHandler(void*, esp_event_base_t event_base, std::int32_t event_id,
     auto const* event =
         static_cast(event_data);
     auto const reason = event != nullptr ? static_cast(event->reason) : -1;
+    g_wifi_disconnect_count.fetch_add(1, std::memory_order_relaxed);
+    if (reason >= 0 && reason <= 255) {
+      g_wifi_last_disconnect_reason.store(static_cast(reason),
+                                          std::memory_order_relaxed);
+    }
 
     if (g_wifi_retry_count < g_max_wifi_retry) {
       ++g_wifi_retry_count;
+      g_wifi_reconnect_count.fetch_add(1, std::memory_order_relaxed);
       PS_LOGW("Wi-Fi hot path disconnected reason=%d; retry %d/%d", reason,
               g_wifi_retry_count, g_max_wifi_retry);
       auto err = esp_wifi_connect();
@@ -621,18 +634,113 @@ HotSendStatus EncodeAndUdpSend(ae::DataBuffer const& payload) {
 }
 
 #if defined(ESP_PLATFORM)
-// Late TX-done callback state: first completion after sendto, no fingerprint.
-std::atomic g_fast_tx_done_seen{false};
+// Late TX-done diagnostic state (fixed-size; no heap/log in callback).
+struct TxDoneDiag {
+  std::atomic total{0};
+  std::atomic success{0};
+  std::atomic failed{0};
+  std::atomic first_cb_us{0};
+  std::atomic first_success_us{0};
+  std::atomic first_failed_us{0};
+  std::atomic last_cb_us{0};
+  std::atomic first_status{-1};  // -1 none, 0 fail, 1 success
+};
+
+std::atomic g_fast_tx_done_seen{false};      // any callback
+std::atomic g_fast_tx_done_success{false};   // first success
 std::atomic g_fast_cb_count{0};
+std::atomic g_tx_wait_mode{0};  // FastTxDoneWaitMode as int
+TxDoneDiag g_tx_diag{};
+
+void ResetTxDoneDiag() {
+  g_tx_diag.total.store(0, std::memory_order_relaxed);
+  g_tx_diag.success.store(0, std::memory_order_relaxed);
+  g_tx_diag.failed.store(0, std::memory_order_relaxed);
+  g_tx_diag.first_cb_us.store(0, std::memory_order_relaxed);
+  g_tx_diag.first_success_us.store(0, std::memory_order_relaxed);
+  g_tx_diag.first_failed_us.store(0, std::memory_order_relaxed);
+  g_tx_diag.last_cb_us.store(0, std::memory_order_relaxed);
+  g_tx_diag.first_status.store(-1, std::memory_order_relaxed);
+  g_fast_tx_done_seen.store(false, std::memory_order_release);
+  g_fast_tx_done_success.store(false, std::memory_order_release);
+  g_fast_cb_count.store(0, std::memory_order_relaxed);
+}
 
-void FastTxDoneCb(std::uint8_t, std::uint8_t*, std::uint16_t*, bool) {
+void FastTxDoneCb(std::uint8_t, std::uint8_t*, std::uint16_t*, bool txStatus) {
+  auto const now = esp_timer_get_time();
+  g_tx_diag.total.fetch_add(1, std::memory_order_relaxed);
   g_fast_cb_count.fetch_add(1, std::memory_order_relaxed);
+  g_tx_diag.last_cb_us.store(now, std::memory_order_relaxed);
+
+  int expected_first = -1;
+  if (g_tx_diag.first_status.compare_exchange_strong(
+          expected_first, txStatus ? 1 : 0, std::memory_order_relaxed)) {
+    g_tx_diag.first_cb_us.store(now, std::memory_order_relaxed);
+  }
+
+  if (txStatus) {
+    g_tx_diag.success.fetch_add(1, std::memory_order_relaxed);
+    std::int64_t expected_fs = 0;
+    if (g_tx_diag.first_success_us.compare_exchange_strong(
+            expected_fs, now, std::memory_order_relaxed)) {
+      g_fast_tx_done_success.store(true, std::memory_order_release);
+    }
+  } else {
+    g_tx_diag.failed.fetch_add(1, std::memory_order_relaxed);
+    std::int64_t expected_ff = 0;
+    (void)g_tx_diag.first_failed_us.compare_exchange_strong(
+        expected_ff, now, std::memory_order_relaxed);
+  }
+
   g_fast_tx_done_seen.store(true, std::memory_order_release);
 }
 
-void ResetFastTxDone() {
-  g_fast_tx_done_seen.store(false, std::memory_order_release);
-  g_fast_cb_count.store(0, std::memory_order_relaxed);
+void ResetFastTxDone() { ResetTxDoneDiag(); }
+
+static std::uint32_t DeltaOrMissing(std::int64_t abs_us,
+                                    std::int64_t sendto_return_us) {
+  if (abs_us <= 0) {
+    return 0xffffffffu;
+  }
+  auto const d = abs_us - sendto_return_us;
+  if (d < 0) {
+    return 0xffffffffu;
+  }
+  return d > 0xffffffffll ? 0xffffffffu : static_cast(d);
+}
+
+static void FillTxDoneTiming(FastSendResult* timing, std::int64_t sendto_return_us,
+                             bool condition_met, std::uint8_t after_success,
+                             FastTxDoneWaitMode wait_mode) {
+  if (timing == nullptr) {
+    return;
+  }
+  auto const total = g_tx_diag.total.load(std::memory_order_relaxed);
+  auto const success = g_tx_diag.success.load(std::memory_order_relaxed);
+  auto const failed = g_tx_diag.failed.load(std::memory_order_relaxed);
+  auto const first_st = g_tx_diag.first_status.load(std::memory_order_relaxed);
+  timing->diag_mode = static_cast(wait_mode);
+  timing->tx_cb_total = total > 255 ? 255 : static_cast(total);
+  timing->tx_cb_success =
+      success > 255 ? 255 : static_cast(success);
+  timing->tx_cb_failed = failed > 255 ? 255 : static_cast(failed);
+  timing->first_status =
+      first_st < 0 ? 0xff : static_cast(first_st);
+  timing->first_cb_delta_us = DeltaOrMissing(
+      g_tx_diag.first_cb_us.load(std::memory_order_relaxed), sendto_return_us);
+  timing->first_success_delta_us = DeltaOrMissing(
+      g_tx_diag.first_success_us.load(std::memory_order_relaxed),
+      sendto_return_us);
+  timing->first_failed_delta_us = DeltaOrMissing(
+      g_tx_diag.first_failed_us.load(std::memory_order_relaxed),
+      sendto_return_us);
+  timing->last_cb_delta_us = DeltaOrMissing(
+      g_tx_diag.last_cb_us.load(std::memory_order_relaxed), sendto_return_us);
+  timing->callbacks_after_success = after_success;
+  timing->cb_any = condition_met ? 1 : 0;
+  timing->cb_timeout = condition_met ? 0 : 1;
+  timing->cb_count = timing->tx_cb_total;
+  timing->cb_match = 0;
 }
 
 HotSendStatus EncodeAndUdpSendTracked(ae::DataBuffer const& payload) {
@@ -678,11 +786,13 @@ HotSendStatus EncodeAndUdpSendTracked(ae::DataBuffer const& payload) {
   return HotSendStatus::kSent;
 }
 
-// Encode → socket → set_tx_done_cb → sendto → wait first cb → unset → close.
-// Socket stays open until callback (or timeout). No Wi-Fi ops between set and
-// sendto.
+// Encode → socket → set_tx_done_cb → sendto → wait condition → unset → close.
+// MODE A (kFirstAny): wait first callback regardless of txStatus.
+// MODE B (kFirstSuccess): wait first txStatus==true, then 5 ms observe window.
+// Safety timeout: 100 ms from sendto return. Socket open until unregister.
 HotSendStatus EncodeAndUdpSendWithLateTxDone(ae::DataBuffer const& payload,
-                                             FastSendResult* timing) {
+                                             FastSendResult* timing,
+                                             FastTxDoneWaitMode wait_mode) {
   if (!g_prepared_send_message_block.is_valid()) {
     return HotSendStatus::kNoPreparedBlock;
   }
@@ -718,6 +828,7 @@ HotSendStatus EncodeAndUdpSendWithLateTxDone(ae::DataBuffer const& payload,
   }
 
   ResetFastTxDone();
+  g_tx_wait_mode.store(static_cast(wait_mode), std::memory_order_relaxed);
   (void)esp_wifi_set_tx_done_cb(&FastTxDoneCb);
 
   auto sent = sendto(sock, packet.data(), packet.size(), 0,
@@ -735,23 +846,30 @@ HotSendStatus EncodeAndUdpSendWithLateTxDone(ae::DataBuffer const& payload,
     return HotSendStatus::kSendFailed;
   }
 
-  // Primary wait 50 ms; extend to 100 ms total if needed.
-  constexpr std::int64_t kPrimaryUs = 50000;
   constexpr std::int64_t kMaxUs = 100000;
-  bool seen = false;
-  while ((esp_timer_get_time() - t_send_ret) < kPrimaryUs) {
-    if (g_fast_tx_done_seen.load(std::memory_order_acquire)) {
-      seen = true;
+  constexpr std::int64_t kObserveUs = 5000;
+  bool condition_met = false;
+  std::uint32_t total_at_success = 0;
+
+  auto condition_ready = [&]() -> bool {
+    if (wait_mode == FastTxDoneWaitMode::kFirstSuccess) {
+      return g_fast_tx_done_success.load(std::memory_order_acquire);
+    }
+    return g_fast_tx_done_seen.load(std::memory_order_acquire);
+  };
+
+  while ((esp_timer_get_time() - t_send_ret) < kMaxUs) {
+    if (condition_ready()) {
+      condition_met = true;
       break;
     }
     vTaskDelay(pdMS_TO_TICKS(1));
   }
-  if (!seen) {
-    while ((esp_timer_get_time() - t_send_ret) < kMaxUs) {
-      if (g_fast_tx_done_seen.load(std::memory_order_acquire)) {
-        seen = true;
-        break;
-      }
+
+  if (condition_met && wait_mode == FastTxDoneWaitMode::kFirstSuccess) {
+    total_at_success = g_tx_diag.total.load(std::memory_order_relaxed);
+    auto const t_obs0 = esp_timer_get_time();
+    while ((esp_timer_get_time() - t_obs0) < kObserveUs) {
       vTaskDelay(pdMS_TO_TICKS(1));
     }
   }
@@ -760,15 +878,20 @@ HotSendStatus EncodeAndUdpSendWithLateTxDone(ae::DataBuffer const& payload,
   (void)esp_wifi_set_tx_done_cb(nullptr);
   close(sock);
 
+  std::uint8_t after_success = 0;
+  if (condition_met && wait_mode == FastTxDoneWaitMode::kFirstSuccess) {
+    auto const total_end = g_tx_diag.total.load(std::memory_order_relaxed);
+    auto const delta =
+        total_end > total_at_success ? (total_end - total_at_success) : 0u;
+    after_success = delta > 255 ? 255 : static_cast(delta);
+  }
+
   if (timing != nullptr) {
     auto const wait = t_cb_done - t_send_ret;
     timing->tx_done_wait_us =
         wait < 0 ? 0 : static_cast(wait);
-    timing->cb_any = seen ? 1 : 0;
-    timing->cb_timeout = seen ? 0 : 1;
-    auto const cb_n = g_fast_cb_count.load(std::memory_order_relaxed);
-    timing->cb_count = cb_n > 255 ? 255 : static_cast(cb_n);
-    timing->cb_match = 0;
+    FillTxDoneTiming(timing, t_send_ret, condition_met, after_success,
+                     wait_mode);
   }
   return HotSendStatus::kSent;
 }
@@ -1282,6 +1405,9 @@ bool StartFastWifi(FastPathConfig const& cfg,
 
   CleanupHotPathWifiRuntime();
   g_bisect_actual_channel = 0;
+  g_wifi_disconnect_count.store(0, std::memory_order_relaxed);
+  g_wifi_last_disconnect_reason.store(0, std::memory_order_relaxed);
+  g_wifi_reconnect_count.store(0, std::memory_order_relaxed);
 
   bool const need_static_ip = cfg.use_static_ip && cache.valid_ip;
   g_wait_got_ip = !need_static_ip;
@@ -1600,6 +1726,24 @@ FastSendResult SendPreparedOnceWithFastPath(
       static_cast(bench::BisectStatusBits::kWifiReady);
   out.actual_channel = g_bisect_actual_channel;
   out.negotiated_auth = ReadNegotiatedAuth();
+  {
+    auto const d = g_wifi_disconnect_count.load(std::memory_order_relaxed);
+    out.disconnect_count = d > 255 ? 255 : static_cast(d);
+  }
+  out.last_disconnect_reason =
+      g_wifi_last_disconnect_reason.load(std::memory_order_relaxed);
+  {
+    auto const r = g_wifi_reconnect_count.load(std::memory_order_relaxed);
+    out.reconnect_count = r > 255 ? 255 : static_cast(r);
+  }
+
+  {
+    wifi_ap_record_t ap{};
+    if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
+      out.rssi = ap.rssi;
+      out.ap_primary = ap.primary;
+    }
+  }
 
   if (cfg.pre_delay_ms > 0) {
     vTaskDelay(pdMS_TO_TICKS(cfg.pre_delay_ms));
@@ -1608,7 +1752,8 @@ FastSendResult SendPreparedOnceWithFastPath(
   HotSendStatus encode_status = HotSendStatus::kWifiFailed;
   auto const t_post0 = esp_timer_get_time();
   if (cfg.post_mode != FastPostMode::kFixedDelay) {
-    encode_status = EncodeAndUdpSendWithLateTxDone(payload, &out);
+    encode_status =
+        EncodeAndUdpSendWithLateTxDone(payload, &out, cfg.tx_done_wait);
     std::uint16_t extra_ms = 0;
     if (cfg.post_mode == FastPostMode::kTxDoneCbPlus10) {
       extra_ms = 10;
@@ -1684,7 +1829,9 @@ bool PreparedWifiRtcCacheIsValid(PreparedWifiRtcCache const& cache) {
   bool const have_ip = (cache.flags & 1u) != 0;
   bool const have_ch = (cache.flags & 2u) != 0;
   bool const have_gw = (cache.flags & 4u) != 0;
-  return have_ip && have_ch && have_gw && cache.channel != 0 && cache.ip != 0;
+  (void)have_gw;
+  // Gateway MAC is preferred but optional: hot path can ARP on miss.
+  return have_ip && have_ch && cache.channel != 0 && cache.ip != 0;
 }
 
 BisectWifiCacheSnapshot SnapshotFromPreparedWifiRtcCache(
@@ -1694,7 +1841,7 @@ BisectWifiCacheSnapshot SnapshotFromPreparedWifiRtcCache(
     return s;
   }
   s.valid_ip = true;
-  s.valid_gw_mac = true;
+  s.valid_gw_mac = (cache.flags & 4u) != 0;
   s.valid_bssid = (cache.flags & 8u) != 0;
   s.channel = cache.channel;
   s.ip = cache.ip;
@@ -1710,7 +1857,22 @@ bool CapturePreparedWifiRtcCache(PreparedWifiRtcCache* out) {
     return false;
   }
   if (!FreezeBisectWifiCacheFromActiveConnection()) {
-    return false;
+    // Fallback: production-style capture then freeze again.
+    if (!CapturePreparedWifiCacheFromActiveConnection()) {
+      return false;
+    }
+    if (!FreezeBisectWifiCacheFromActiveConnection()) {
+      return false;
+    }
+  }
+  // Some APs report primary=0 briefly; HOT path needs a non-zero channel.
+  if (g_bisect_cache.channel == 0) {
+    wifi_ap_record_t ap{};
+    if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK && ap.primary != 0) {
+      g_bisect_cache.channel = ap.primary;
+    } else {
+      g_bisect_cache.channel = 1;
+    }
   }
   PreparedWifiRtcCache c{};
   c.magic = kPreparedWifiRtcMagic;
diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h
index 4e7a223..750e5ba 100644
--- a/main/prepared_send/prepared_send.h
+++ b/main/prepared_send/prepared_send.h
@@ -128,6 +128,12 @@ enum class FastPostMode : std::uint8_t {
   kTxDoneCbPlus25 = 3,
 };
 
+// Diagnostic wait policy for late TX-done callback (experiment only).
+enum class FastTxDoneWaitMode : std::uint8_t {
+  kFirstAny = 0,      // current production-like: first callback
+  kFirstSuccess = 1,  // wait first txStatus==true + 5 ms observe
+};
+
 struct FastPathConfig {
   bool use_bssid{false};
   bool use_channel{true};
@@ -141,6 +147,7 @@ struct FastPathConfig {
   std::uint16_t pre_delay_ms{200};
   std::uint16_t post_delay_ms{300};
   FastPostMode post_mode{FastPostMode::kFixedDelay};
+  FastTxDoneWaitMode tx_done_wait{FastTxDoneWaitMode::kFirstAny};
 };
 
 struct FastSendResult {
@@ -154,10 +161,26 @@ struct FastSendResult {
   std::uint8_t actual_channel{0};
   std::uint8_t negotiated_auth{0};
   std::uint8_t status_flags{0};
-  std::uint8_t cb_any{0};       // first tx-done callback seen
-  std::uint8_t cb_match{0};     // legacy fingerprint match (unused)
-  std::uint8_t cb_timeout{0};   // 1 if no callback within window
+  std::uint8_t cb_any{0};
+  std::uint8_t cb_match{0};
+  std::uint8_t cb_timeout{0};
   std::uint8_t cb_count{0};
+  // TX-done diagnostics (experiment).
+  std::uint8_t diag_mode{0};
+  std::uint8_t first_status{0xff};  // 0xff none, 0 fail, 1 success
+  std::uint8_t tx_cb_total{0};
+  std::uint8_t tx_cb_success{0};
+  std::uint8_t tx_cb_failed{0};
+  std::uint8_t callbacks_after_success{0};
+  std::uint32_t first_cb_delta_us{0xffffffffu};
+  std::uint32_t first_success_delta_us{0xffffffffu};
+  std::uint32_t first_failed_delta_us{0xffffffffu};
+  std::uint32_t last_cb_delta_us{0xffffffffu};
+  std::int8_t rssi{0};
+  std::uint8_t ap_primary{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t last_disconnect_reason{0};
+  std::uint8_t reconnect_count{0};
 };
 
 // BASE = cached channel + static IPv4/netmask/gw + static ARP. No BSSID.
diff --git a/main/prepared_tx_done_diag_bench.cpp b/main/prepared_tx_done_diag_bench.cpp
new file mode 100644
index 0000000..8d77658
--- /dev/null
+++ b/main/prepared_tx_done_diag_bench.cpp
@@ -0,0 +1,1175 @@
+/*
+ * Copyright 2026 Aethernet Inc.
+ *
+ * Silent TX-done callback diagnostic (ESP32-C6).
+ * 1 FULL + 50 HOT prepared sends, 3 s deep sleep between wakes.
+ * MODE A = FIRST_ANY, MODE B = FIRST_SUCCESS (AE_EXP_TX_DIAG_MODE).
+ * Metrics travel in TxDiagPayload 0xD6; UART is silent.
+ */
+
+#include 
+#include 
+#include 
+
+#include "aether/all.h"
+#include "aether/ae_exp_wifi.h"
+#include "aether/config.h"
+#include "aether/env.h"
+#include "bench_payload.h"
+#include "experiment_early_entry.h"
+#include "prepared_send/prepared_send.h"
+
+#if defined(ESP_PLATFORM)
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#endif
+
+using namespace std::chrono_literals;
+
+#if defined(ESP_PLATFORM)
+extern "C" std::uint64_t esp_rtc_get_time_us(void);
+#endif
+
+namespace temp_sensor {
+namespace {
+
+static constexpr auto kParentUid =
+    ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165");
+
+#ifndef BENCH_CLIENT_ID
+#  define BENCH_CLIENT_ID "prepared_tx_done_diag_v1"
+#endif
+static constexpr char const* kBenchClientId = BENCH_CLIENT_ID;
+
+#if defined(SERVICE_UID)
+static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID);
+#else
+static constexpr auto kServiceUid =
+    ae::Uid::FromString("5aade50f-00d9-4624-b097-e203cdcf1e38");
+#endif
+
+static constexpr std::uint8_t kOuterCycles = 1;
+static constexpr std::uint8_t kHotPerOuter = 50;
+static constexpr std::uint8_t kMaxHotAttemptsPerBlock = 60;
+static constexpr std::uint32_t kSleepUs = 3000000;
+
+static constexpr std::uint32_t kRtcMagic = 0x54584434u;  // "TXD4"
+static constexpr std::uint16_t kRtcVersion = 1;
+
+enum class Phase : std::uint16_t {
+  kRegister = 0,
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+  kDone = 4,
+};
+
+struct RtcState {
+  std::uint32_t magic;
+  std::uint16_t version;
+  std::uint16_t phase;
+  std::uint8_t outer_cycle;
+  std::uint8_t hot_index;
+  std::uint8_t hot_attempt_count;
+  std::uint8_t hot_send_count;
+  std::uint16_t sequence_global;
+  std::uint16_t next_record_id;
+  std::uint32_t requested_sleep_us;
+  std::uint64_t sleep_arm_rtc_us;
+  std::uint8_t pending_valid;
+  std::uint8_t pending_kind;
+  std::uint8_t pending_outer;
+  std::uint8_t pending_hot_index;
+  std::uint32_t pending_user_cycle_us;
+  std::uint32_t pending_wifi_cycle_us;
+  std::uint32_t pending_connect_us;
+  std::uint32_t pending_txdone_us;
+  std::uint32_t pending_teardown_us;
+  std::uint8_t pending_cb_seen;
+  std::uint8_t pending_cb_timeout;
+  std::uint8_t pending_auth;
+  std::uint8_t brownout_count;
+  std::uint8_t unexpected_reset_count;
+  std::uint8_t recovery_full_count;
+  std::uint8_t current_boot_brownout;
+  std::uint8_t registered;
+  std::uint8_t final_fail_count;
+  std::uint8_t pad0;
+  std::uint16_t pad1;
+  std::uint32_t crc;
+};
+
+#if defined(ESP_PLATFORM)
+RTC_DATA_ATTR static RtcState g_rtc{};
+RTC_DATA_ATTR static prepared_send::PreparedWifiRtcCache g_rtc_wifi_cache{};
+
+// Separate from RtcState so campaign CRC layout matches deep-sleep E2E.
+struct PendingTxDiag {
+  std::uint8_t valid{0};
+  std::uint8_t diag_mode{0};
+  std::uint8_t tx_cb_total{0};
+  std::uint8_t tx_cb_success{0};
+  std::uint8_t tx_cb_failed{0};
+  std::uint8_t first_status{0xff};
+  std::uint8_t callbacks_after{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t last_disc_reason{0};
+  std::uint8_t reconnect_count{0};
+  std::int8_t rssi{0};
+  std::uint8_t ap_primary{0};
+  std::uint8_t cb_timeout{0};
+  std::uint8_t pad{0};
+  std::uint32_t first_cb_delta_us{0xffffffffu};
+  std::uint32_t first_success_delta_us{0xffffffffu};
+  std::uint32_t first_failed_delta_us{0xffffffffu};
+  std::uint32_t last_cb_delta_us{0xffffffffu};
+};
+RTC_DATA_ATTR static PendingTxDiag g_pending_diag{};
+RTC_DATA_ATTR static std::uint8_t g_last_full_reason{0};
+
+struct PreSleepSnap {
+  std::uint8_t phase{0};
+  std::uint8_t outer{0};
+  std::uint8_t hot{0};
+  std::uint8_t prepared_left{0};
+  std::uint32_t state_crc{0};
+  std::uint32_t wifi_crc{0};
+};
+RTC_DATA_ATTR static PreSleepSnap g_pre_sleep{};
+
+struct BootSnap {
+  std::uint8_t reset_reason{0};
+  std::uint8_t wakeup_cause{0};
+  std::uint8_t early_valid{0};
+  std::uint8_t rtc_state_crc_ok{0};
+  std::uint8_t rtc_state_valid{0};
+  std::uint8_t rtc_wifi_crc_ok{0};
+  std::uint8_t rtc_wifi_valid{0};
+  std::uint8_t prepared_block_valid{0};
+  std::uint8_t phase{0};
+  std::uint8_t outer{0};
+  std::uint8_t hot{0};
+  std::uint16_t prepared_left{0};
+  std::uint32_t rtc_state_magic{0};
+  std::uint16_t rtc_state_version{0};
+  std::uint32_t rtc_wifi_magic{0};
+  std::uint16_t rtc_wifi_version{0};
+};
+static BootSnap g_boot_snap{};
+
+static const auto kWifiInit = ae::WiFiInit{
+    std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}},
+    {},
+};
+
+static bool g_had_aether_app = false;
+
+static std::shared_ptr g_app;
+static ae::Client::ptr g_client;
+static std::unique_ptr g_stream;
+static ae::Subscription g_select_sub;
+static ae::Subscription g_stream_sub;
+static ae::Subscription g_write_sub;
+
+static bool g_write_armed = false;
+static bool g_write_ok = false;
+static bool g_exit_success = false;
+static bool g_pending_register_finish = false;
+static bool g_pending_full_post_write = false;
+static bool g_pending_final_exit = false;
+static bool g_done = false;
+
+static ExperimentEarlyEntrySnapshot g_early{};
+static std::uint32_t g_sleep_elapsed_us = 0;
+static std::uint32_t g_sleep_overhead_us = 0;
+static prepared_send::FastPathConfig g_cfg{};
+static prepared_send::BisectWifiCacheSnapshot g_wifi_snapshot{};
+
+static std::uint32_t Crc32Bytes(void const* data, std::size_t len) {
+  auto const* p = static_cast(data);
+  std::uint32_t crc = 0xffffffffu;
+  for (std::size_t i = 0; i < len; ++i) {
+    crc ^= p[i];
+    for (int b = 0; b < 8; ++b) {
+      std::uint32_t const mask = -(crc & 1u);
+      crc = (crc >> 1) ^ (0xedb88320u & mask);
+    }
+  }
+  return ~crc;
+}
+
+static std::uint32_t ComputeCrc(RtcState const& st) {
+  RtcState tmp = st;
+  tmp.crc = 0;
+  return Crc32Bytes(&tmp, sizeof(tmp));
+}
+
+static void SetCrc(RtcState& st) { st.crc = ComputeCrc(st); }
+
+static bool ValidateRtcState(RtcState const& st) {
+  if (st.magic != kRtcMagic || st.version != kRtcVersion) {
+    return false;
+  }
+  if (ComputeCrc(st) != st.crc) {
+    return false;
+  }
+  if (st.phase > static_cast(Phase::kDone)) {
+    return false;
+  }
+  if (st.outer_cycle > kOuterCycles) {
+    return false;
+  }
+  if (st.hot_index > kHotPerOuter) {
+    return false;
+  }
+  return true;
+}
+
+static void ClearPending(RtcState& st) {
+  st.pending_valid = 0;
+  st.pending_kind = static_cast(bench::DsPendingKind::kNone);
+  st.pending_outer = 0;
+  st.pending_hot_index = 0;
+  st.pending_user_cycle_us = 0;
+  st.pending_wifi_cycle_us = 0;
+  st.pending_connect_us = 0;
+  st.pending_txdone_us = 0;
+  st.pending_teardown_us = 0;
+  st.pending_cb_seen = 0;
+  st.pending_cb_timeout = 0;
+  st.pending_auth = 0;
+  g_pending_diag = PendingTxDiag{};
+}
+
+
+static void InitRtcFresh(Phase phase) {
+  g_rtc = RtcState{};
+  g_rtc.magic = kRtcMagic;
+  g_rtc.version = kRtcVersion;
+  g_rtc.phase = static_cast(phase);
+  g_rtc.outer_cycle = (phase == Phase::kFull || phase == Phase::kHot) ? 1 : 0;
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  g_rtc.sequence_global = 0;
+  g_rtc.next_record_id = 1;
+  g_rtc.requested_sleep_us = 0;
+  g_rtc.sleep_arm_rtc_us = 0;
+  ClearPending(g_rtc);
+  g_rtc.brownout_count = 0;
+  g_rtc.unexpected_reset_count = 0;
+  g_rtc.recovery_full_count = 0;
+  g_rtc.current_boot_brownout = 0;
+  g_rtc.registered = 0;
+  g_rtc.final_fail_count = 0;
+  SetCrc(g_rtc);
+}
+
+[[noreturn]] static void PrepareRtcStateAndDeepSleep(
+    std::uint32_t requested_us) {
+  g_rtc.requested_sleep_us = requested_us;
+  esp_sleep_enable_timer_wakeup(requested_us);
+  g_rtc.sleep_arm_rtc_us = esp_rtc_get_time_us();
+  SetCrc(g_rtc);
+  g_pre_sleep.phase = static_cast(g_rtc.phase);
+  g_pre_sleep.outer = g_rtc.outer_cycle;
+  g_pre_sleep.hot = g_rtc.hot_index;
+  auto const left = prepared_send::PreparedMessageLeft();
+  g_pre_sleep.prepared_left =
+      left > 255 ? 255 : static_cast(left);
+  g_pre_sleep.state_crc = g_rtc.crc;
+  g_pre_sleep.wifi_crc = g_rtc_wifi_cache.crc;
+
+#  if SOC_PM_SUPPORT_RTC_SLOW_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
+#  endif
+#  if SOC_PM_SUPPORT_RTC_FAST_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
+#  endif
+
+  esp_err_t const ret = esp_deep_sleep_try_to_start();
+  (void)ret;
+  esp_deep_sleep_start();
+  for (;;) {
+  }
+}
+
+static void ForceFullRecovery(bench::FullReason reason) {
+  g_last_full_reason = static_cast(reason);
+  ClearPending(g_rtc);
+  g_rtc.phase = static_cast(Phase::kFull);
+  if (g_rtc.outer_cycle == 0 || g_rtc.outer_cycle > kOuterCycles) {
+    g_rtc.outer_cycle = 1;
+  }
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  if (g_rtc.recovery_full_count < 255) {
+    ++g_rtc.recovery_full_count;
+  }
+  SetCrc(g_rtc);
+}
+
+static void ComputeWakeMetrics() {
+  g_sleep_elapsed_us = 0;
+  g_sleep_overhead_us = 0;
+  auto const reset =
+      static_cast(g_early.reset_reason);
+  if (reset == ESP_RST_DEEPSLEEP && g_rtc.sleep_arm_rtc_us != 0 &&
+      g_early.app_entry_rtc_us >= g_rtc.sleep_arm_rtc_us) {
+    auto const elapsed = g_early.app_entry_rtc_us - g_rtc.sleep_arm_rtc_us;
+    g_sleep_elapsed_us =
+        elapsed > 0xffffffffull ? 0xffffffffu
+                                : static_cast(elapsed);
+    if (g_sleep_elapsed_us > g_rtc.requested_sleep_us) {
+      g_sleep_overhead_us = g_sleep_elapsed_us - g_rtc.requested_sleep_us;
+    }
+  }
+}
+
+static std::uint16_t NextSeq() {
+  ++g_rtc.sequence_global;
+  return g_rtc.sequence_global;
+}
+
+static void AdvanceRecordIdAfterFlush() {
+  if (g_rtc.next_record_id < 0xffffu) {
+    ++g_rtc.next_record_id;
+  }
+}
+
+static void FillWakeFields(bench::TxDiagPayload& p) {
+  // Prefer boot snapshot (pre-mutation) for FULL diagnostics.
+  p.reset_reason = g_boot_snap.reset_reason;
+  p.wake_cause = g_boot_snap.wakeup_cause;
+  p.brownout_count = g_rtc.brownout_count;
+  p.unexpected_reset_count = g_rtc.unexpected_reset_count;
+  p.requested_sleep_us = g_rtc.requested_sleep_us;
+  p.sleep_elapsed_to_app_us = g_sleep_elapsed_us;
+  p.sleep_to_app_overhead_us = g_sleep_overhead_us;
+  p.app_entry_esp_timer_us =
+      g_early.app_entry_esp_timer_us < 0
+          ? 0
+          : static_cast(g_early.app_entry_esp_timer_us);
+
+  std::uint8_t flags = 0;
+  if (g_rtc.current_boot_brownout) {
+    flags |= static_cast(bench::TxDiagFlags::kBrownout);
+  }
+  if (g_boot_snap.rtc_wifi_valid) {
+    flags |= static_cast(bench::TxDiagFlags::kCacheValid);
+  }
+  if (g_boot_snap.rtc_state_valid) {
+    flags |= static_cast(bench::TxDiagFlags::kStateValid);
+  }
+  p.flags = flags;
+
+  p.full_reason = g_last_full_reason;
+  // Boot-time phase (before ForceFullRecovery / InitRtcFresh).
+  p.phase = g_boot_snap.phase;
+  p.rtc_state_magic = g_boot_snap.rtc_state_magic;
+  p.rtc_state_version = g_boot_snap.rtc_state_version;
+  p.rtc_state_crc_ok = g_boot_snap.rtc_state_crc_ok;
+  p.rtc_state_valid = g_boot_snap.rtc_state_valid;
+  p.rtc_wifi_magic = g_boot_snap.rtc_wifi_magic;
+  p.rtc_wifi_version = g_boot_snap.rtc_wifi_version;
+  p.rtc_wifi_crc_ok = g_boot_snap.rtc_wifi_crc_ok;
+  p.rtc_wifi_valid = g_boot_snap.rtc_wifi_valid;
+  p.prepared_block_valid = g_boot_snap.prepared_block_valid;
+  p.prepared_message_left = g_boot_snap.prepared_left;
+  p.pre_sleep_phase = g_pre_sleep.phase;
+  p.pre_sleep_outer = g_pre_sleep.outer;
+  p.pre_sleep_hot = g_pre_sleep.hot;
+  p.pre_sleep_prepared_left = g_pre_sleep.prepared_left;
+  p.pre_sleep_state_crc = g_pre_sleep.state_crc;
+  p.pre_sleep_wifi_crc = g_pre_sleep.wifi_crc;
+  p.app_entry_rtc_us =
+      g_early.app_entry_rtc_us > 0xffffffffull
+          ? 0xffffffffu
+          : static_cast(g_early.app_entry_rtc_us);
+}
+
+static void FillPendingFields(bench::TxDiagPayload& p) {
+  if (!g_rtc.pending_valid) {
+    p.pending_kind = static_cast(bench::DsPendingKind::kNone);
+    p.pending_outer = 0;
+    p.pending_hot_index = 0;
+    p.pending_user_cycle_us = 0;
+    p.pending_wifi_cycle_us = 0;
+    p.connect_us = 0;
+    p.tx_done_wait_us = 0;
+    p.teardown_us = 0;
+    p.negotiated_auth = 0;
+    return;
+  }
+  p.pending_kind = g_rtc.pending_kind;
+  p.pending_outer = g_rtc.pending_outer;
+  p.pending_hot_index = g_rtc.pending_hot_index;
+  p.pending_user_cycle_us = g_rtc.pending_user_cycle_us;
+  p.pending_wifi_cycle_us = g_rtc.pending_wifi_cycle_us;
+  p.connect_us = g_rtc.pending_connect_us;
+  p.tx_done_wait_us = g_rtc.pending_txdone_us;
+  p.teardown_us = g_rtc.pending_teardown_us;
+  p.negotiated_auth = g_rtc.pending_auth;
+  if (g_pending_diag.valid) {
+    p.diag_mode = g_pending_diag.diag_mode;
+    p.tx_cb_total = g_pending_diag.tx_cb_total;
+    p.tx_cb_success = g_pending_diag.tx_cb_success;
+    p.tx_cb_failed = g_pending_diag.tx_cb_failed;
+    p.first_status = g_pending_diag.first_status;
+    p.first_cb_delta_us = g_pending_diag.first_cb_delta_us;
+    p.first_success_delta_us = g_pending_diag.first_success_delta_us;
+    p.first_failed_delta_us = g_pending_diag.first_failed_delta_us;
+    p.last_cb_delta_us = g_pending_diag.last_cb_delta_us;
+    p.callbacks_after_success = g_pending_diag.callbacks_after;
+    p.rssi = g_pending_diag.rssi;
+    p.disconnect_count = g_pending_diag.disconnect_count;
+    p.last_disconnect_reason = g_pending_diag.last_disc_reason;
+    p.reconnect_count = g_pending_diag.reconnect_count;
+    p.ap_primary = g_pending_diag.ap_primary;
+    p.cb_timeout = g_pending_diag.cb_timeout;
+  }
+  if (g_rtc.pending_cb_seen) {
+    p.flags |= static_cast(bench::TxDiagFlags::kCallbackSeen);
+  }
+  if (g_rtc.pending_cb_timeout) {
+    p.flags |= static_cast(bench::TxDiagFlags::kCallbackTimeout);
+  }
+}
+
+static ae::DataBuffer MakeTxDiagPayload(bench::TxDiagMsgType type) {
+  bench::TxDiagPayload p{};
+  p.type = static_cast(type);
+  if (type == bench::TxDiagMsgType::kFull) {
+    // Boot-time counters (pre ForceFullRecovery mutation).
+    p.outer_cycle = g_boot_snap.outer;
+    p.hot_index = g_boot_snap.hot;
+  } else {
+    p.outer_cycle = g_rtc.outer_cycle;
+    p.hot_index = g_rtc.hot_index;
+  }
+  p.sequence_global = NextSeq();
+  // Assign id without advancing until the send that flushes pending succeeds
+  // (HOT Wi-Fi retries must reuse the same record_id).
+  p.record_id = g_rtc.pending_valid ? g_rtc.next_record_id : 0;
+  FillWakeFields(p);
+  FillPendingFields(p);
+  if (type != bench::TxDiagMsgType::kFull) {
+    p.prepared_message_left = static_cast(
+        prepared_send::PreparedMessageLeft() > 0xffffu
+            ? 0xffffu
+            : prepared_send::PreparedMessageLeft());
+  }
+  return bench::EncodeTxDiag(p);
+}
+
+static void StorePendingFull(std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = static_cast(bench::DsPendingKind::kFull);
+  g_rtc.pending_outer = g_rtc.outer_cycle;
+  g_rtc.pending_hot_index = 0;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = user_cycle_us;
+  g_rtc.pending_connect_us = 0;
+  g_rtc.pending_txdone_us = 0;
+  g_rtc.pending_teardown_us = 0;
+  g_rtc.pending_cb_seen = 0;
+  g_rtc.pending_cb_timeout = 0;
+  g_rtc.pending_auth = 0;
+}
+
+static void StorePendingHot(prepared_send::FastSendResult const& result,
+                            std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = static_cast(bench::DsPendingKind::kHot);
+  g_rtc.pending_outer = g_rtc.outer_cycle;
+  g_rtc.pending_hot_index = g_rtc.hot_index;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = result.cycle_us;
+  g_rtc.pending_connect_us = result.connect_us;
+  g_rtc.pending_txdone_us = result.tx_done_wait_us;
+  g_rtc.pending_teardown_us = result.teardown_us;
+  g_rtc.pending_cb_seen = result.cb_any;
+  g_rtc.pending_cb_timeout = result.cb_timeout;
+  g_rtc.pending_auth = result.negotiated_auth;
+  g_pending_diag = PendingTxDiag{};
+  g_pending_diag.valid = 1;
+  g_pending_diag.diag_mode = result.diag_mode;
+  g_pending_diag.tx_cb_total = result.tx_cb_total;
+  g_pending_diag.tx_cb_success = result.tx_cb_success;
+  g_pending_diag.tx_cb_failed = result.tx_cb_failed;
+  g_pending_diag.first_status = result.first_status;
+  g_pending_diag.callbacks_after = result.callbacks_after_success;
+  g_pending_diag.disconnect_count = result.disconnect_count;
+  g_pending_diag.last_disc_reason = result.last_disconnect_reason;
+  g_pending_diag.reconnect_count = result.reconnect_count;
+  g_pending_diag.rssi = result.rssi;
+  g_pending_diag.ap_primary = result.ap_primary;
+  g_pending_diag.cb_timeout = result.cb_timeout;
+  g_pending_diag.first_cb_delta_us = result.first_cb_delta_us;
+  g_pending_diag.first_success_delta_us = result.first_success_delta_us;
+  g_pending_diag.first_failed_delta_us = result.first_failed_delta_us;
+  g_pending_diag.last_cb_delta_us = result.last_cb_delta_us;
+}
+
+static void ReleaseApp() {
+  g_select_sub.Reset();
+  g_stream_sub.Reset();
+  g_write_sub.Reset();
+  g_stream.reset();
+  g_client = {};
+  g_app.reset();
+}
+
+static void PreConstructCleanup() {
+  if (!g_had_aether_app) {
+    return;
+  }
+#  if !AE_WIFI_USE_FULL_DEINIT
+  esp_netif_deinit();
+  esp_event_loop_delete_default();
+#  endif
+}
+
+static void ConstructAether() {
+  PreConstructCleanup();
+  g_had_aether_app = true;
+  g_app = ae::AetherApp::Construct(
+      ae::AetherAppContext{}
+#  if AE_DISTILLATION
+          .AddAdapterFactory([&](ae::AetherAppContext const& ctx) {
+            return ae::WifiAdapter::ptr::Create(
+                ae::CreateWith{ctx.domain()}.with_id(
+                    ae::GlobalId::kWiFiAdapter),
+                ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit);
+          })
+#  endif
+  );
+}
+
+static prepared_send::FastPathConfig MakeFastConfig() {
+  prepared_send::FastPathConfig c{};
+  c.use_bssid = false;
+  c.use_channel = true;
+  c.use_fast_scan = false;
+  c.use_static_ip = true;
+  c.use_static_arp = true;
+  c.ampdu_tx_off = false;
+  c.wifi_storage_ram = false;
+  c.auth = prepared_send::FastAuthMode::kWpa2;
+  c.retry_max = 10;
+  c.pre_delay_ms = 25;
+  c.post_delay_ms = 0;
+  c.post_mode = prepared_send::FastPostMode::kTxDoneCb;
+#if defined(AE_EXP_TX_DIAG_MODE) && (AE_EXP_TX_DIAG_MODE == 1)
+  c.tx_done_wait = prepared_send::FastTxDoneWaitMode::kFirstSuccess;
+#else
+  c.tx_done_wait = prepared_send::FastTxDoneWaitMode::kFirstAny;
+#endif
+  return c;
+}
+
+static void DoFullWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto payload = MakeTxDiagPayload(bench::TxDiagMsgType::kFull);
+  auto& wa = g_stream->Write(std::move(payload));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_full_post_write = true;
+  });
+}
+
+static void MaybeFullWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFullWrite();
+}
+
+static void OnFullClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); });
+  MaybeFullWrite();
+}
+
+static void StartRegister() {
+  g_write_armed = false;
+  g_pending_register_finish = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       g_pending_register_finish = true;
+                     });
+}
+
+static void StartFull() {
+  g_write_armed = false;
+  g_pending_full_post_write = false;
+  g_write_ok = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFullClientReady(std::move(res).value());
+                     });
+}
+
+static void DoFinalWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto& wa = g_stream->Write(MakeTxDiagPayload(bench::TxDiagMsgType::kFinal));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_final_exit = true;
+  });
+}
+
+static void MaybeFinalWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFinalWrite();
+}
+
+static void OnFinalClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFinalWrite(); });
+  MaybeFinalWrite();
+}
+
+static void StartFinal() {
+  g_write_armed = false;
+  g_pending_final_exit = false;
+  g_write_ok = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFinalClientReady(std::move(res).value());
+                     });
+}
+
+static void FinishRegisterInLoop() {
+  g_app->aether().Save();
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFullPostWriteInLoop() {
+  if (!g_write_ok) {
+    g_last_full_reason =
+        static_cast(bench::FullReason::kForcedRecovery);
+    g_app->Exit(1);
+    return;
+  }
+  bool captured = false;
+  for (int i = 0; i < 10 && !captured; ++i) {
+    captured = prepared_send::CapturePreparedWifiRtcCache(&g_rtc_wifi_cache);
+    if (!captured) {
+      vTaskDelay(pdMS_TO_TICKS(200));
+    }
+  }
+  bool exported = false;
+  for (std::size_t n : {std::size_t{50}, std::size_t{30}, std::size_t{20},
+                        std::size_t{10}}) {
+    if (prepared_send::ExportPreparedSendBlock(g_client, kServiceUid, n)) {
+      exported = true;
+      break;
+    }
+  }
+  if (!exported) {
+    g_last_full_reason = static_cast(
+        bench::FullReason::kPreparedExportFailed);
+    g_app->Exit(1);
+    return;
+  }
+  auto const left = prepared_send::PreparedMessageLeft();
+  if (!prepared_send::HasPreparedSendBlock() || left == 0) {
+    g_last_full_reason = static_cast(
+        bench::FullReason::kPreparedBlockInvalid);
+    g_app->Exit(1);
+    return;
+  }
+  g_rtc.pad0 = left > 255 ? 255 : static_cast(left);
+  if (!captured) {
+    g_last_full_reason = static_cast(
+        bench::FullReason::kWifiCacheCaptureFailed);
+    g_app->Exit(1);
+    return;
+  }
+  g_last_full_reason =
+      static_cast(bench::FullReason::kNone);
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFinalInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static std::uint32_t UserCycleFromAppEntry() {
+  auto const now = esp_timer_get_time();
+  auto const entry = g_early.app_entry_esp_timer_us;
+  if (now < entry) {
+    return 0;
+  }
+  auto const delta = now - entry;
+  return delta > 0xffffffffll ? 0xffffffffu
+                              : static_cast(delta);
+}
+
+static void AfterRegisterComplete() {
+  ReleaseApp();
+  g_rtc.registered = 1;
+  g_rtc.phase = static_cast(Phase::kFull);
+  g_rtc.outer_cycle = 1;
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  ClearPending(g_rtc);
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFullComplete() {
+  ReleaseApp();
+  prepared_send::ReleaseFullAetherWifiForHotPath();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  auto const user_cycle = UserCycleFromAppEntry();
+  StorePendingFull(user_cycle);
+  g_rtc.phase = static_cast(Phase::kHot);
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalComplete() {
+  ReleaseApp();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  ClearPending(g_rtc);
+  g_rtc.final_fail_count = 0;
+  g_rtc.phase = static_cast(Phase::kDone);
+  SetCrc(g_rtc);
+  g_done = true;
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalFailed() {
+  ReleaseApp();
+  if (g_rtc.final_fail_count < 255) {
+    ++g_rtc.final_fail_count;
+  }
+  // After several Aether FINAL failures, stop the campaign so metrics already
+  // delivered (via pending on HOT/FULL) are not blocked forever.
+  if (g_rtc.final_fail_count >= 5) {
+    ClearPending(g_rtc);
+    g_rtc.phase = static_cast(Phase::kDone);
+    SetCrc(g_rtc);
+    g_done = true;
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static bool WifiFailedBeforeEncode(prepared_send::FastSendResult const& r) {
+  if (r.status == prepared_send::HotSendStatus::kWifiFailed) {
+    return true;
+  }
+  bool const encode_ok =
+      (r.status_flags &
+       static_cast(bench::BisectStatusBits::kEncodeOk)) != 0;
+  return !encode_ok && r.status != prepared_send::HotSendStatus::kSent;
+}
+
+static void RunHotOnce() {
+  if (!prepared_send::PreparedWifiRtcCacheIsValid(g_rtc_wifi_cache)) {
+    ForceFullRecovery(bench::FullReason::kRtcWifiCacheInvalid);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  g_wifi_snapshot =
+      prepared_send::SnapshotFromPreparedWifiRtcCache(g_rtc_wifi_cache);
+  if (!g_wifi_snapshot.valid_ip || g_wifi_snapshot.channel == 0) {
+    ForceFullRecovery(bench::FullReason::kRtcWifiCacheInvalid);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  if (!prepared_send::HasPreparedSendBlock()) {
+    ForceFullRecovery(bench::FullReason::kPreparedBlockInvalid);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  if (prepared_send::PreparedMessageLeft() == 0) {
+    ForceFullRecovery(bench::FullReason::kPreparedNonceEmpty);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  if (g_rtc.hot_attempt_count >= kMaxHotAttemptsPerBlock) {
+    ForceFullRecovery(bench::FullReason::kForcedRecovery);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (g_rtc.hot_attempt_count < 255) {
+    ++g_rtc.hot_attempt_count;
+  }
+  SetCrc(g_rtc);
+
+  auto payload = MakeTxDiagPayload(bench::TxDiagMsgType::kHot);
+  auto const result =
+      prepared_send::SendPreparedOnceWithFastPath(g_cfg, payload,
+                                                    &g_wifi_snapshot);
+
+  if (result.status == prepared_send::HotSendStatus::kSent) {
+    auto const user_cycle = UserCycleFromAppEntry();
+    if (g_rtc.hot_send_count < 255) {
+      ++g_rtc.hot_send_count;
+    }
+    bool const flushed_prior = g_rtc.pending_valid != 0;
+    StorePendingHot(result, user_cycle);
+    if (flushed_prior) {
+      AdvanceRecordIdAfterFlush();
+    }
+
+    if (g_rtc.hot_index < 255) {
+      ++g_rtc.hot_index;
+    }
+    auto const hot_target =
+        g_rtc.pad0 != 0 ? g_rtc.pad0 : kHotPerOuter;
+    if (g_rtc.hot_index > hot_target) {
+      if (g_rtc.outer_cycle < kOuterCycles) {
+        ++g_rtc.outer_cycle;
+        g_rtc.phase = static_cast(Phase::kFull);
+        g_rtc.hot_index = 1;
+        g_rtc.hot_attempt_count = 0;
+        g_rtc.hot_send_count = 0;
+        g_last_full_reason =
+            static_cast(bench::FullReason::kNone);
+      } else {
+        // Keep RTC bounds valid across deep sleep (hot_index<=kHotPerOuter).
+        g_rtc.phase = static_cast(Phase::kFinal);
+        g_rtc.hot_index = 1;
+        g_rtc.hot_attempt_count = 0;
+      }
+    }
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (result.status == prepared_send::HotSendStatus::kWifiFailed) {
+    ForceFullRecovery(bench::FullReason::kHotWifiFailed);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  if (result.status == prepared_send::HotSendStatus::kEncodeFailed) {
+    ForceFullRecovery(bench::FullReason::kHotEncodeFailed);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  if (result.status == prepared_send::HotSendStatus::kSendFailed) {
+    ForceFullRecovery(bench::FullReason::kHotSendFailed);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  // Encode/send failure after Wi-Fi: do not advance; retry same index.
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void PrepareRtcOnBoot() {
+  g_early = GetExperimentEarlyEntrySnapshot();
+
+  // Boot snapshot BEFORE mutating RTC state.
+  g_boot_snap = BootSnap{};
+  g_boot_snap.reset_reason = g_early.reset_reason;
+  g_boot_snap.wakeup_cause = g_early.wakeup_cause;
+  g_boot_snap.early_valid = g_early.valid;
+  g_boot_snap.rtc_state_magic = g_rtc.magic;
+  g_boot_snap.rtc_state_version = g_rtc.version;
+  g_boot_snap.rtc_state_crc_ok = (ComputeCrc(g_rtc) == g_rtc.crc) ? 1 : 0;
+  g_boot_snap.rtc_state_valid = ValidateRtcState(g_rtc) ? 1 : 0;
+  g_boot_snap.phase = static_cast(g_rtc.phase);
+  g_boot_snap.outer = g_rtc.outer_cycle;
+  g_boot_snap.hot = g_rtc.hot_index;
+  g_boot_snap.prepared_block_valid =
+      prepared_send::HasPreparedSendBlock() ? 1 : 0;
+  g_boot_snap.prepared_left = static_cast(
+      prepared_send::PreparedMessageLeft() > 0xffffu
+          ? 0xffffu
+          : prepared_send::PreparedMessageLeft());
+  g_boot_snap.rtc_wifi_magic = g_rtc_wifi_cache.magic;
+  g_boot_snap.rtc_wifi_version = g_rtc_wifi_cache.version;
+  {
+    prepared_send::PreparedWifiRtcCache tmp = g_rtc_wifi_cache;
+    auto const stored = tmp.crc;
+    tmp.crc = 0;
+    g_boot_snap.rtc_wifi_crc_ok =
+        (Crc32Bytes(&tmp, sizeof(tmp)) == stored) ? 1 : 0;
+  }
+  g_boot_snap.rtc_wifi_valid =
+      prepared_send::PreparedWifiRtcCacheIsValid(g_rtc_wifi_cache) ? 1 : 0;
+
+  auto const reset =
+      static_cast(g_early.reset_reason);
+  bool const valid = ValidateRtcState(g_rtc);
+
+  g_rtc.current_boot_brownout = 0;
+
+  // Sequential decision: first failing check wins FullReason.
+  if (reset == ESP_RST_BROWNOUT) {
+    if (valid) {
+      if (g_rtc.brownout_count < 255) {
+        ++g_rtc.brownout_count;
+      }
+      ClearPending(g_rtc);
+    } else {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.brownout_count = 1;
+      g_rtc.outer_cycle = 1;
+    }
+    g_rtc.current_boot_brownout = 1;
+    ForceFullRecovery(bench::FullReason::kForcedRecovery);
+  } else if (!g_early.valid) {
+    // Early hook did not run — treat as unexpected (should not happen after
+    // AE_EXP_PREPARED_TX_DONE_DIAG early-entry enable).
+    if (valid) {
+      ForceFullRecovery(bench::FullReason::kUnexpectedResetReason);
+    } else {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.outer_cycle = 1;
+      g_rtc.registered = 1;
+      g_last_full_reason =
+          static_cast(bench::FullReason::kUnexpectedResetReason);
+      SetCrc(g_rtc);
+    }
+  } else if (reset != ESP_RST_DEEPSLEEP) {
+    bool const first_poweron = (reset == ESP_RST_POWERON);
+    if (first_poweron && (!valid || !g_rtc.registered)) {
+      InitRtcFresh(Phase::kRegister);
+      g_last_full_reason =
+          static_cast(bench::FullReason::kColdBoot);
+    } else if (!valid) {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.unexpected_reset_count = 1;
+      g_rtc.outer_cycle = 1;
+      g_rtc.registered = 1;
+      g_last_full_reason =
+          static_cast(bench::FullReason::kRtcStateInvalid);
+      SetCrc(g_rtc);
+    } else {
+      if (g_rtc.unexpected_reset_count < 255) {
+        ++g_rtc.unexpected_reset_count;
+      }
+      ForceFullRecovery(bench::FullReason::kUnexpectedResetReason);
+    }
+  } else if (!valid) {
+    // Deep-sleep wake but RTC state invalid.
+    if (g_rtc.magic != kRtcMagic || g_rtc.version != kRtcVersion) {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.outer_cycle = 1;
+      g_rtc.registered = 1;
+      g_last_full_reason =
+          static_cast(bench::FullReason::kRtcStateInvalid);
+      SetCrc(g_rtc);
+    } else if (ComputeCrc(g_rtc) != g_rtc.crc) {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.outer_cycle = 1;
+      g_rtc.registered = 1;
+      g_last_full_reason =
+          static_cast(bench::FullReason::kRtcStateInvalid);
+      SetCrc(g_rtc);
+    } else {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.outer_cycle = 1;
+      g_rtc.registered = 1;
+      g_last_full_reason =
+          static_cast(bench::FullReason::kStateBoundsInvalid);
+      SetCrc(g_rtc);
+    }
+  }
+  // else: DEEPSLEEP + valid — continue phase as stored (HOT/FULL/FINAL)
+
+  ComputeWakeMetrics();
+  SetCrc(g_rtc);
+}
+
+#endif  // ESP_PLATFORM
+
+}  // namespace
+}  // namespace temp_sensor
+
+#if defined(ESP_PLATFORM)
+
+void setup() {
+  using namespace temp_sensor;
+  nvs_flash_init();
+  g_cfg = MakeFastConfig();
+  g_done = false;
+  g_pending_register_finish = false;
+  g_pending_full_post_write = false;
+  g_pending_final_exit = false;
+  PrepareRtcOnBoot();
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kDone) {
+    g_done = true;
+    return;
+  }
+  if (phase == Phase::kRegister) {
+    StartRegister();
+    return;
+  }
+  if (phase == Phase::kFull) {
+    StartFull();
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    StartFinal();
+    return;
+  }
+  // HOT is handled synchronously in loop().
+}
+
+void loop() {
+  using namespace temp_sensor;
+  if (g_done) {
+    vTaskDelay(pdMS_TO_TICKS(1000));
+    return;
+  }
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kHot) {
+    RunHotOnce();
+    return;
+  }
+
+  auto process_deferred = []() {
+    if (g_app && g_pending_register_finish) {
+      g_pending_register_finish = false;
+      FinishRegisterInLoop();
+      return true;
+    }
+    if (g_app && g_pending_full_post_write) {
+      g_pending_full_post_write = false;
+      FinishFullPostWriteInLoop();
+      return true;
+    }
+    if (g_app && g_pending_final_exit) {
+      g_pending_final_exit = false;
+      FinishFinalInLoop();
+      return true;
+    }
+    return false;
+  };
+
+  if (process_deferred()) {
+    return;
+  }
+
+  if (!g_app) {
+    return;
+  }
+
+  if (!g_app->IsExited()) {
+    auto t = g_app->Update(ae::Now());
+    if (process_deferred()) {
+      return;
+    }
+    if (!g_app->IsExited()) {
+      g_app->WaitUntil(t);
+    }
+    return;
+  }
+
+  if (phase == Phase::kRegister) {
+    if (g_exit_success) {
+      AfterRegisterComplete();
+    } else {
+      ReleaseApp();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFull) {
+    if (g_exit_success) {
+      AfterFullComplete();
+    } else {
+      ReleaseApp();
+      // Keep reason set during FinishFullPostWriteInLoop; else forced.
+      auto const reason =
+          g_last_full_reason != 0
+              ? static_cast(g_last_full_reason)
+              : bench::FullReason::kForcedRecovery;
+      ForceFullRecovery(reason);
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    if (g_exit_success) {
+      AfterFinalComplete();
+    } else {
+      AfterFinalFailed();
+    }
+    return;
+  }
+}
+
+#else
+
+void setup() {}
+void loop() {}
+
+#endif
diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp
index 1a5bcb1..5418b47 100644
--- a/temperature_receiver/main.cpp
+++ b/temperature_receiver/main.cpp
@@ -1,8 +1,8 @@
 /*
  * Copyright 2026 Aethernet Inc.
  *
- * Desktop Æther receiver for prepared deep-sleep 5x50 E2E (DsPayload 0xD5).
- * Deduplicates by record_id; appends TSV; prints OUTER progress.
+ * Desktop Æther receiver for prepared TX-done diagnostics (TxDiagPayload 0xD6)
+ * and deep-sleep E2E (DsPayload 0xD5). Deduplicates by record_id; appends TSV.
  */
 
 #include 
@@ -49,6 +49,22 @@ struct Meas {
   std::uint8_t cb_timeout{0};
   std::uint8_t brownout{0};
   std::uint8_t auth{0};
+  std::uint8_t diag_mode{0};
+  std::uint8_t tx_cb_total{0};
+  std::uint8_t tx_cb_success{0};
+  std::uint8_t tx_cb_failed{0};
+  std::uint8_t first_status{0xff};
+  std::uint32_t first_cb_delta_us{0xffffffffu};
+  std::uint32_t first_success_delta_us{0xffffffffu};
+  std::uint32_t first_failed_delta_us{0xffffffffu};
+  std::uint32_t last_cb_delta_us{0xffffffffu};
+  std::uint8_t callbacks_after_success{0};
+  std::int8_t rssi{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t last_disconnect_reason{0};
+  std::uint8_t reconnect_count{0};
+  std::uint8_t ap_primary{0};
+  std::uint16_t seq{0};
 };
 
 std::mutex g_mu;
@@ -62,7 +78,6 @@ int g_dup_records = 0;
 int g_ooo = 0;
 int g_max_record = 0;
 int g_brownout_boots = 0;
-std::uint8_t g_last_outer_reported = 0;
 
 std::filesystem::path TsvPath() {
 #if defined(_WIN32)
@@ -70,7 +85,7 @@ std::filesystem::path TsvPath() {
     return std::filesystem::path{env};
   }
 #endif
-  return std::filesystem::path{"prepared_deepsleep_5x50.tsv"};
+  return std::filesystem::path{"prepared_tx_done_diag.tsv"};
 }
 
 std::uint32_t PercentileUs(std::vector v, int pct) {
@@ -90,10 +105,14 @@ void EnsureTsvHeader() {
   std::ofstream out(path, std::ios::app);
   out << "record_id\tkind\touter\thot\tuser_us\twifi_us\tconnect_us\ttxdone_us\t"
          "teardown_us\tsleep_elapsed_us\tsleep_overhead_us\tapp_entry_us\t"
-         "cb_seen\tcb_timeout\tbrownout\tauth\tseq\n";
+         "cb_seen\tcb_timeout\tbrownout\tauth\tseq\tdiag_mode\ttx_cb_total\t"
+         "tx_cb_success\ttx_cb_failed\tfirst_status\tfirst_cb_delta_us\t"
+         "first_success_delta_us\tfirst_failed_delta_us\tlast_cb_delta_us\t"
+         "callbacks_after_success\trssi\tdisconnect_count\t"
+         "last_disconnect_reason\treconnect_count\tap_primary\n";
 }
 
-void AppendTsv(temp_sensor::bench::DsPayload const& p, Meas const& m) {
+void AppendTsv(Meas const& m) {
   EnsureTsvHeader();
   std::ofstream out(TsvPath(), std::ios::app);
   out << m.record_id << '\t' << static_cast(m.kind) << '\t'
@@ -103,204 +122,276 @@ void AppendTsv(temp_sensor::bench::DsPayload const& p, Meas const& m) {
       << '\t' << m.sleep_overhead_us << '\t' << m.app_entry_us << '\t'
       << static_cast(m.cb_seen) << '\t' << static_cast(m.cb_timeout)
       << '\t' << static_cast(m.brownout) << '\t'
-      << static_cast(m.auth) << '\t' << p.sequence_global << '\n';
+      << static_cast(m.auth) << '\t' << m.seq << '\t'
+      << static_cast(m.diag_mode) << '\t'
+      << static_cast(m.tx_cb_total) << '\t'
+      << static_cast(m.tx_cb_success) << '\t'
+      << static_cast(m.tx_cb_failed) << '\t'
+      << static_cast(m.first_status) << '\t' << m.first_cb_delta_us << '\t'
+      << m.first_success_delta_us << '\t' << m.first_failed_delta_us << '\t'
+      << m.last_cb_delta_us << '\t'
+      << static_cast(m.callbacks_after_success) << '\t'
+      << static_cast(m.rssi) << '\t'
+      << static_cast(m.disconnect_count) << '\t'
+      << static_cast(m.last_disconnect_reason) << '\t'
+      << static_cast(m.reconnect_count) << '\t'
+      << static_cast(m.ap_primary) << '\n';
 }
 
-void MaybePrintOuter(std::uint8_t outer) {
-  if (outer == 0 || outer == g_last_outer_reported) {
+void NoteRecord(Meas m) {
+  if (m.record_id == 0 || m.kind == 0) {
     return;
   }
-  // Report completed outer (outer-1) when we see next FULL, or current on FINAL.
-  g_last_outer_reported = outer;
-}
-
-void PrintOuterSummary(std::uint8_t completed_outer) {
-  std::vector hot_user;
-  std::vector wake_oh;
-  int hot_n = 0;
-  int cb = 0;
-  int to = 0;
-  std::uint32_t full_user = 0;
-  for (auto const& m : g_meas) {
-    if (m.outer != completed_outer) {
-      continue;
-    }
-    if (m.kind == static_cast(temp_sensor::bench::DsPendingKind::kFull)) {
-      full_user = m.user_us;
-    }
-    if (m.kind == static_cast(temp_sensor::bench::DsPendingKind::kHot)) {
-      ++hot_n;
-      hot_user.push_back(m.user_us);
-      wake_oh.push_back(m.sleep_overhead_us);
-      cb += m.cb_seen;
-      to += m.cb_timeout;
-    }
-  }
-  auto const hot_med = PercentileUs(hot_user, 50) / 1000;
-  auto const wake_med = PercentileUs(wake_oh, 50) / 1000;
-  int brown = 0;
-  int unexp = 0;
-  std::cout << "[OUTER " << static_cast(completed_outer) << "/5]\n"
-            << "full_user_ms=" << (full_user / 1000) << "\n"
-            << "hot_sendto=50/50\n"
-            << "receiver_hot=" << hot_n << "/50\n"
-            << "hot_user_median_ms=" << hot_med << "\n"
-            << "wake_overhead_median_ms=" << wake_med << "\n"
-            << "callback_seen_sum=" << cb << " timeouts_sum=" << to << "\n"
-            << "brownout=" << brown << "\n"
-            << "unexpected_reset=" << unexp << "\n"
-            << "remaining=" << (5 - completed_outer) << "\n"
-            << "NEXT:\n"
-            << (completed_outer < 5
-                    ? ("FULL " + std::to_string(completed_outer + 1) + "/5")
-                    : "FINAL")
-            << "\n\n";
-  std::cout.flush();
-}
-
-void NoteRecord(temp_sensor::bench::DsPayload const& p) {
-  if (p.record_id == 0 || p.pending_kind == 0) {
-    return;
-  }
-  if (g_seen_records.count(p.record_id)) {
+  if (g_seen_records.count(m.record_id)) {
     ++g_dup_records;
     return;
   }
-  g_seen_records.insert(p.record_id);
-  if (static_cast(p.record_id) < g_max_record) {
+  g_seen_records.insert(m.record_id);
+  if (static_cast(m.record_id) < g_max_record) {
     ++g_ooo;
   }
-  if (static_cast(p.record_id) > g_max_record) {
-    g_max_record = p.record_id;
+  if (static_cast(m.record_id) > g_max_record) {
+    g_max_record = m.record_id;
   }
-
-  Meas m{};
-  m.record_id = p.record_id;
-  m.kind = p.pending_kind;
-  m.outer = p.pending_outer;
-  m.hot = p.pending_hot_index;
-  m.user_us = p.pending_user_cycle_us;
-  m.wifi_us = p.pending_wifi_cycle_us;
-  m.connect_us = p.connect_us;
-  m.txdone_us = p.tx_done_wait_us;
-  m.teardown_us = p.teardown_us;
-  m.sleep_elapsed_us = p.sleep_elapsed_to_app_us;
-  m.sleep_overhead_us = p.sleep_to_app_overhead_us;
-  m.app_entry_us = p.app_entry_esp_timer_us;
-  m.cb_seen = (p.flags & static_cast(
-                             temp_sensor::bench::DsFlags::kCallbackSeen))
-                  ? 1
-                  : 0;
-  m.cb_timeout = (p.flags & static_cast(
-                                temp_sensor::bench::DsFlags::kCallbackTimeout))
-                     ? 1
-                     : 0;
-  m.brownout =
-      (p.flags & static_cast(temp_sensor::bench::DsFlags::kBrownout))
-          ? 1
-          : 0;
-  m.auth = p.negotiated_auth;
   if (m.brownout) {
     ++g_brownout_boots;
   }
   g_meas.push_back(m);
-  AppendTsv(p, m);
-
-  // When HOT#1 of outer N+1 arrives (or FULL of N+1), prior outer HOT set is done.
-  if (p.type == static_cast(temp_sensor::bench::DsMsgType::kFull) &&
-      p.outer_cycle > 1) {
-    PrintOuterSummary(static_cast(p.outer_cycle - 1));
-  }
+  AppendTsv(m);
 }
 
-void PrintFinalStats() {
+void PrintFinalStats(char const* tag) {
   std::vector full_user;
   std::vector hot_user;
   std::vector hot_wifi;
   std::vector connect;
   std::vector txdone;
-  std::vector teardown;
-  std::vector sleep_el;
-  std::vector sleep_oh;
-  std::vector app_entry;
+  std::vector first_success;
+  std::vector first_cb;
+  std::vector last_cb;
+  std::vector rssi_pos;
   int cb = 0;
   int to = 0;
+  int first_ok = 0;
+  int first_fail = 0;
+  int cb_total_sum = 0;
+  int cb_succ_sum = 0;
+  int cb_fail_sum = 0;
+  int after_succ_sum = 0;
+  int fail_before_succ = 0;
   for (auto const& m : g_meas) {
-    sleep_el.push_back(m.sleep_elapsed_us);
-    sleep_oh.push_back(m.sleep_overhead_us);
-    app_entry.push_back(m.app_entry_us);
-    if (m.kind == static_cast(temp_sensor::bench::DsPendingKind::kFull)) {
+    if (m.kind == static_cast(
+                      temp_sensor::bench::DsPendingKind::kFull)) {
       full_user.push_back(m.user_us);
     }
-    if (m.kind == static_cast(temp_sensor::bench::DsPendingKind::kHot)) {
+    if (m.kind == static_cast(
+                      temp_sensor::bench::DsPendingKind::kHot)) {
       hot_user.push_back(m.user_us);
       hot_wifi.push_back(m.wifi_us);
       connect.push_back(m.connect_us);
       txdone.push_back(m.txdone_us);
-      teardown.push_back(m.teardown_us);
       cb += m.cb_seen;
       to += m.cb_timeout;
+      cb_total_sum += m.tx_cb_total;
+      cb_succ_sum += m.tx_cb_success;
+      cb_fail_sum += m.tx_cb_failed;
+      after_succ_sum += m.callbacks_after_success;
+      if (m.first_status == 1) {
+        ++first_ok;
+      } else if (m.first_status == 0) {
+        ++first_fail;
+      }
+      if (m.tx_cb_failed > 0 && m.tx_cb_success > 0) {
+        ++fail_before_succ;
+      }
+      if (m.first_cb_delta_us != 0xffffffffu) {
+        first_cb.push_back(m.first_cb_delta_us);
+      }
+      if (m.first_success_delta_us != 0xffffffffu) {
+        first_success.push_back(m.first_success_delta_us);
+      }
+      if (m.last_cb_delta_us != 0xffffffffu) {
+        last_cb.push_back(m.last_cb_delta_us);
+      }
+      rssi_pos.push_back(static_cast(
+          static_cast(m.rssi) + 200));
     }
   }
-  if (g_last_outer_reported < 5) {
-    PrintOuterSummary(5);
-  }
+  auto rssi_med = [&]() -> int {
+    if (rssi_pos.empty()) {
+      return 0;
+    }
+    return static_cast(PercentileUs(rssi_pos, 50)) - 200;
+  };
   std::cout << "TEST_RESULT"
             << " full_recv=" << g_full_recv << " hot_recv=" << g_hot_recv
-            << " final_recv=" << g_final_recv
-            << " records=" << g_meas.size() << " dup=" << g_dup_records
-            << " ooo=" << g_ooo
+            << " final_recv=" << g_final_recv << " records=" << g_meas.size()
+            << " dup=" << g_dup_records << " ooo=" << g_ooo
             << " full_med_ms=" << (PercentileUs(full_user, 50) / 1000)
             << " hot_user_med_ms=" << (PercentileUs(hot_user, 50) / 1000)
-            << " hot_user_p90_ms=" << (PercentileUs(hot_user, 90) / 1000)
-            << " hot_user_p99_ms=" << (PercentileUs(hot_user, 99) / 1000)
             << " hot_wifi_med_ms=" << (PercentileUs(hot_wifi, 50) / 1000)
             << " connect_med_ms=" << (PercentileUs(connect, 50) / 1000)
-            << " txdone_med_ms=" << (PercentileUs(txdone, 50) / 1000)
-            << " teardown_med_ms=" << (PercentileUs(teardown, 50) / 1000)
-            << " wake_oh_med_ms=" << (PercentileUs(sleep_oh, 50) / 1000)
-            << " wake_oh_p90_ms=" << (PercentileUs(sleep_oh, 90) / 1000)
-            << " wake_oh_p99_ms=" << (PercentileUs(sleep_oh, 99) / 1000)
-            << " app_entry_med_us=" << PercentileUs(app_entry, 50)
-            << " cb_seen=" << cb << " cb_timeout=" << to
+            << " txdone_med_us=" << PercentileUs(txdone, 50)
+            << " first_cb_med_us=" << PercentileUs(first_cb, 50)
+            << " first_success_med_us=" << PercentileUs(first_success, 50)
+            << " last_cb_med_us=" << PercentileUs(last_cb, 50)
+            << " rssi_med=" << rssi_med() << " cb_seen=" << cb
+            << " cb_timeout=" << to << " first_status_ok=" << first_ok
+            << " first_status_fail=" << first_fail
+            << " cb_total_sum=" << cb_total_sum
+            << " cb_succ_sum=" << cb_succ_sum << " cb_fail_sum=" << cb_fail_sum
+            << " after_succ_sum=" << after_succ_sum
+            << " fail_before_succ=" << fail_before_succ
             << " brownout_boots=" << g_brownout_boots << "\n";
-  std::cout << "BENCH_DONE deepsleep_5x50\n";
+  std::cout << "BENCH_DONE " << tag << "\n";
   std::cout.flush();
 }
 
-void OnDs(temp_sensor::bench::DsPayload const& p) {
-  auto const type = static_cast(p.type);
-  if (type == temp_sensor::bench::DsMsgType::kFull) {
+void OnTxDiag(temp_sensor::bench::TxDiagPayload const& p) {
+  auto const type = static_cast(p.type);
+  if (type == temp_sensor::bench::TxDiagMsgType::kFull) {
     ++g_full_recv;
-    std::cout << ae::Format(
-        "RECV FULL outer={} seq={} pending_kind={} record={} user_us={}\n",
-        p.outer_cycle, p.sequence_global, p.pending_kind, p.record_id,
-        p.pending_user_cycle_us);
-  } else if (type == temp_sensor::bench::DsMsgType::kHot) {
+    std::cout << "FULL_DIAG seq=" << p.sequence_global
+              << " reason=" << temp_sensor::bench::FullReasonName(p.full_reason)
+              << " reset=" << static_cast(p.reset_reason)
+              << " wake=" << static_cast(p.wake_cause)
+              << " rtc_state={magic=" << p.rtc_state_magic
+              << ",ver=" << p.rtc_state_version
+              << ",crc=" << static_cast(p.rtc_state_crc_ok)
+              << ",valid=" << static_cast(p.rtc_state_valid) << "}"
+              << " phase=" << static_cast(p.phase)
+              << " outer=" << static_cast(p.outer_cycle)
+              << " hot=" << static_cast(p.hot_index)
+              << " prepared={valid="
+              << static_cast(p.prepared_block_valid)
+              << ",left=" << p.prepared_message_left << "}"
+              << " wifi={magic=" << p.rtc_wifi_magic
+              << ",ver=" << p.rtc_wifi_version
+              << ",crc=" << static_cast(p.rtc_wifi_crc_ok)
+              << ",valid=" << static_cast(p.rtc_wifi_valid) << "}"
+              << " pre_sleep={phase="
+              << static_cast(p.pre_sleep_phase)
+              << ",outer=" << static_cast(p.pre_sleep_outer)
+              << ",hot=" << static_cast(p.pre_sleep_hot)
+              << ",left="
+              << static_cast(p.pre_sleep_prepared_left)
+              << ",state_crc=" << p.pre_sleep_state_crc
+              << ",wifi_crc=" << p.pre_sleep_wifi_crc << "}\n";
+  } else if (type == temp_sensor::bench::TxDiagMsgType::kHot) {
     ++g_hot_recv;
-    if (g_hot_recv <= 3 || g_hot_recv % 25 == 0) {
+    if (g_hot_recv <= 5 || g_hot_recv % 10 == 0) {
       std::cout << ae::Format(
-          "RECV HOT outer={} idx={} seq={} record={} user_us={} wifi_us={}\n",
+          "RECV HOT outer={} idx={} seq={} record={} user_us={} "
+          "cb_t={} cb_s={} cb_f={} first_st={} rssi={}\n",
           p.outer_cycle, p.hot_index, p.sequence_global, p.record_id,
-          p.pending_user_cycle_us, p.pending_wifi_cycle_us);
+          p.pending_user_cycle_us, p.tx_cb_total, p.tx_cb_success,
+          p.tx_cb_failed, p.first_status, p.rssi);
     }
-  } else if (type == temp_sensor::bench::DsMsgType::kFinal) {
+  } else if (type == temp_sensor::bench::TxDiagMsgType::kFinal) {
     ++g_final_recv;
     std::cout << ae::Format("RECV FINAL seq={} record={}\n", p.sequence_global,
                             p.record_id);
   } else {
-    std::cout << ae::Format("RECV RECOVERY/OTHER type={} seq={}\n", p.type,
+    std::cout << ae::Format("RECV OTHER type={} seq={}\n", p.type,
                             p.sequence_global);
   }
-  NoteRecord(p);
-  if (type == temp_sensor::bench::DsMsgType::kFinal) {
-    PrintFinalStats();
+
+  Meas m{};
+  m.record_id = p.record_id;
+  m.kind = p.pending_kind;
+  m.outer = p.pending_outer;
+  m.hot = p.pending_hot_index;
+  m.user_us = p.pending_user_cycle_us;
+  m.wifi_us = p.pending_wifi_cycle_us;
+  m.connect_us = p.connect_us;
+  m.txdone_us = p.tx_done_wait_us;
+  m.teardown_us = p.teardown_us;
+  m.sleep_elapsed_us = p.sleep_elapsed_to_app_us;
+  m.sleep_overhead_us = p.sleep_to_app_overhead_us;
+  m.app_entry_us = p.app_entry_esp_timer_us;
+  m.cb_seen = (p.flags & static_cast(
+                             temp_sensor::bench::TxDiagFlags::kCallbackSeen))
+                  ? 1
+                  : 0;
+  m.cb_timeout = p.cb_timeout;
+  m.brownout =
+      (p.flags & static_cast(temp_sensor::bench::TxDiagFlags::kBrownout))
+          ? 1
+          : 0;
+  m.auth = p.negotiated_auth;
+  m.diag_mode = p.diag_mode;
+  m.tx_cb_total = p.tx_cb_total;
+  m.tx_cb_success = p.tx_cb_success;
+  m.tx_cb_failed = p.tx_cb_failed;
+  m.first_status = p.first_status;
+  m.first_cb_delta_us = p.first_cb_delta_us;
+  m.first_success_delta_us = p.first_success_delta_us;
+  m.first_failed_delta_us = p.first_failed_delta_us;
+  m.last_cb_delta_us = p.last_cb_delta_us;
+  m.callbacks_after_success = p.callbacks_after_success;
+  m.rssi = p.rssi;
+  m.disconnect_count = p.disconnect_count;
+  m.last_disconnect_reason = p.last_disconnect_reason;
+  m.reconnect_count = p.reconnect_count;
+  m.ap_primary = p.ap_primary;
+  m.seq = p.sequence_global;
+  NoteRecord(m);
+
+  if (type == temp_sensor::bench::TxDiagMsgType::kFinal) {
+    PrintFinalStats("tx_done_diag");
   }
   std::cout.flush();
 }
 
+void OnDs(temp_sensor::bench::DsPayload const& p) {
+  auto const type = static_cast(p.type);
+  if (type == temp_sensor::bench::DsMsgType::kFull) {
+    ++g_full_recv;
+  } else if (type == temp_sensor::bench::DsMsgType::kHot) {
+    ++g_hot_recv;
+  } else if (type == temp_sensor::bench::DsMsgType::kFinal) {
+    ++g_final_recv;
+  }
+  Meas m{};
+  m.record_id = p.record_id;
+  m.kind = p.pending_kind;
+  m.outer = p.pending_outer;
+  m.hot = p.pending_hot_index;
+  m.user_us = p.pending_user_cycle_us;
+  m.wifi_us = p.pending_wifi_cycle_us;
+  m.connect_us = p.connect_us;
+  m.txdone_us = p.tx_done_wait_us;
+  m.teardown_us = p.teardown_us;
+  m.sleep_elapsed_us = p.sleep_elapsed_to_app_us;
+  m.sleep_overhead_us = p.sleep_to_app_overhead_us;
+  m.app_entry_us = p.app_entry_esp_timer_us;
+  m.cb_seen = (p.flags & static_cast(
+                             temp_sensor::bench::DsFlags::kCallbackSeen))
+                  ? 1
+                  : 0;
+  m.cb_timeout = (p.flags & static_cast(
+                                temp_sensor::bench::DsFlags::kCallbackTimeout))
+                     ? 1
+                     : 0;
+  m.brownout =
+      (p.flags & static_cast(temp_sensor::bench::DsFlags::kBrownout))
+          ? 1
+          : 0;
+  m.auth = p.negotiated_auth;
+  m.seq = p.sequence_global;
+  NoteRecord(m);
+  if (type == temp_sensor::bench::DsMsgType::kFinal) {
+    PrintFinalStats("deepsleep_5x50");
+  }
+}
+
 void OnMessage(ae::Uid, ae::DataBuffer const& data) {
   std::lock_guard lock{g_mu};
+  temp_sensor::bench::TxDiagPayload td{};
+  if (temp_sensor::bench::DecodeTxDiag(data, td)) {
+    OnTxDiag(td);
+    return;
+  }
   temp_sensor::bench::DsPayload ds{};
   if (temp_sensor::bench::DecodeDs(data, ds)) {
     OnDs(ds);
@@ -308,8 +399,8 @@ void OnMessage(ae::Uid, ae::DataBuffer const& data) {
   }
   temp_sensor::bench::FastPayload fp{};
   if (temp_sensor::bench::DecodeFast(data, fp)) {
-    std::cout << "RECV FAST (ignored in deepsleep run) type="
-              << static_cast(fp.type) << "\n";
+    std::cout << "RECV FAST (ignored) type=" << static_cast(fp.type)
+              << "\n";
     std::cout.flush();
     return;
   }

From e7f4804de3b32fe789f7c4a1643d8f7265386932 Mon Sep 17 00:00:00 2001
From: aethernet-io 
Date: Sat, 29 Aug 2026 14:08:09 -0700
Subject: [PATCH 29/32] Record pushed SHA in TXD3 FULL-loop diagnostic report.

Co-authored-by: Cursor 
---
 experiments/TXD3_FULL_LOOP_DIAG_REPORT.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/experiments/TXD3_FULL_LOOP_DIAG_REPORT.md b/experiments/TXD3_FULL_LOOP_DIAG_REPORT.md
index 11de1ea..75e7e89 100644
--- a/experiments/TXD3_FULL_LOOP_DIAG_REPORT.md
+++ b/experiments/TXD3_FULL_LOOP_DIAG_REPORT.md
@@ -4,7 +4,7 @@
 
 | Repo | Branch | SHA | Notes |
 |------|--------|-----|-------|
-| temperature-sensor | `thermometer-prepared-send-v0` | *(see final commit)* | TX-done diag + FULL reason |
+| temperature-sensor | `thermometer-prepared-send-v0` | `724a1731a53c63963689b4c83da302937eb56a84` | TX-done diag + FULL reason |
 | aether-client-cpp | `exp/esp32c6-wifi-lifecycle-diag` | `157aadbec8e7b852d0f89274307ff7cb8103e5f7` | **unchanged=yes** |
 
 ## Pre-change inventory

From 5f318b99eb555e0c04526b997ab0941b29391371 Mon Sep 17 00:00:00 2001
From: aethernet-io 
Date: Sat, 29 Aug 2026 15:15:55 -0700
Subject: [PATCH 30/32] Add MAC retry-limit diagnostic campaign and report.

One-flash MRT1 deep-sleep run calls esp_wifi_internal_set_retry_counter on the hot path only; CONTROL leaves it unset. Results do not confirm that RF/tx_done tails are driven by that counter.

Co-authored-by: Cursor 
---
 CMakeLists.txt                                |   8 +
 experiments/PREPARED_MAC_RETRY_DIAG_REPORT.md |  91 ++
 experiments/analyze_mac_retry_diag.py         | 220 ++++
 experiments/prepared_mac_retry_diag.tsv       | 274 +++++
 experiments/run_mac_retry_diag.py             | 326 ++++++
 main/CMakeLists.txt                           |   9 +
 main/bench_payload.h                          | 111 ++
 main/experiment_early_entry.cpp               |   3 +-
 main/experiment_early_entry.h                 |   4 +-
 main/prepared_mac_retry_diag_bench.cpp        | 972 ++++++++++++++++++
 main/prepared_send/prepared_send.cpp          |  33 +-
 main/prepared_send/prepared_send.h            |  11 +
 temperature_receiver/main.cpp                 | 122 ++-
 13 files changed, 2174 insertions(+), 10 deletions(-)
 create mode 100644 experiments/PREPARED_MAC_RETRY_DIAG_REPORT.md
 create mode 100644 experiments/analyze_mac_retry_diag.py
 create mode 100644 experiments/prepared_mac_retry_diag.tsv
 create mode 100644 experiments/run_mac_retry_diag.py
 create mode 100644 main/prepared_mac_retry_diag_bench.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0b90e09..627972e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -39,6 +39,8 @@ set(AE_EXP_PREPARED_DEEPSLEEP_5X50 "" CACHE STRING
     "Silent deep-sleep 5x50 prepared E2E (set to 1)")
 set(AE_EXP_PREPARED_TX_DONE_DIAG "" CACHE STRING
     "Silent TX-done callback diagnostic 1x50 (set to 1)")
+set(AE_EXP_PREPARED_MAC_RETRY_DIAG "" CACHE STRING
+    "Silent MAC retry-limit diagnostic 7x50 (set to 1)")
 set(AE_EXP_TX_DIAG_MODE "" CACHE STRING
     "TX-done diag wait mode: 0=FIRST_ANY 1=FIRST_SUCCESS")
 set(AE_EXP_FAST_DISABLE_WPA3 "" CACHE STRING
@@ -63,6 +65,12 @@ elseif(AE_EXP_PREPARED_TX_DONE_DIAG STREQUAL "1")
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
+elseif(AE_EXP_PREPARED_MAC_RETRY_DIAG STREQUAL "1")
+  list(APPEND SDKCONFIG_DEFAULTS
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
 elseif(AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1")
   list(APPEND SDKCONFIG_DEFAULTS
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
diff --git a/experiments/PREPARED_MAC_RETRY_DIAG_REPORT.md b/experiments/PREPARED_MAC_RETRY_DIAG_REPORT.md
new file mode 100644
index 0000000..c1b5fde
--- /dev/null
+++ b/experiments/PREPARED_MAC_RETRY_DIAG_REPORT.md
@@ -0,0 +1,91 @@
+# Prepared MAC retry diagnostic report
+
+Experiment-only. Production `SendPreparedOnce` unchanged.  
+aether-client-cpp SHA `157aadbec8e7b852d0f89274307ff7cb8103e5f7` **unchanged=yes**.
+
+## Campaign notes
+
+- One flash (`AE_EXP_PREPARED_MAC_RETRY_DIAG=1`), RTC magic `MRT1`.
+- COM used only for flash; progress via Æther receiver only.
+- `esp_wifi_internal_set_retry_counter` **symbol_resolved=yes** (map `.text.esp_wifi_internal_set_retry_counter`).
+- Runtime: CONTROL `retry_set_rc=-1` (not called); all other variants `retry_set_rc=0` (`ESP_OK`).
+- Campaign progressed CONTROL→…→V6 then stalled around V6 ~41/50 received (no further RETRY for >10 min). Analysis uses collected TSV (incomplete delivery on later variants is UDP loss + stall).
+- ESP still attempted fire-and-forget sendto; missing receiver packets are not campaign blockers.
+
+## Per-variant summary (reconnect_count==0)
+
+| variant | n | delivery | tx_ok | tx_fail | rc_ok | txdone_med | p90 | p99 | max |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
+| CONTROL | 50 | 50/50 | 50 | 0 | yes | 3709 | 12766 | 28724 | 28724 |
+| 0/0 | 38 | 38/50 | 31 | 7 | yes | 5188 | 18446 | 76286 | 76286 |
+| 1/1 | 38 | 38/50 | 34 | 4 | yes | 4658 | 17001 | 22203 | 22203 |
+| 2/2 | 38 | 38/50 | 34 | 4 | yes | 6556 | 24145 | 50821 | 50821 |
+| 3/3 | 40 | 40/50 | 36 | 4 | yes | 5303 | 23152 | 46327 | 46327 |
+| 1/7 | 22 | 22/50 | 17 | 5 | yes | 5200 | 9867 | 11888 | 11888 |
+| 7/1 | 41 | 41/50 | 37 | 4 | yes | 5565 | 14354 | 58869 | 58869 |
+
+Units for txdone_*: microseconds.
+
+## Retry limit curve
+
+| retry | delivery | tx_ok | tx_fail | med | p90 | p99 | max |
+|---|---:|---:|---:|---:|---:|---:|---:|
+| CONTROL | 50/50 | 50 | 0 | 3709 | 12766 | 28724 | 28724 |
+| 0/0 | 38/50 | 31 | 7 | 5188 | 18446 | 76286 | 76286 |
+| 1/1 | 38/50 | 34 | 4 | 4658 | 17001 | 22203 | 22203 |
+| 2/2 | 38/50 | 34 | 4 | 6556 | 24145 | 50821 | 50821 |
+| 3/3 | 40/50 | 36 | 4 | 5303 | 23152 | 46327 | 46327 |
+
+No monotonic 0→1→2→3→CONTROL growth of tx_done tail.
+
+## Short vs long (CONTROL / 1/7 / 7/1)
+
+| variant | delivery | tx_ok | tx_fail | med | p90 | p99 | max |
+|---|---:|---:|---:|---:|---:|---:|---:|
+| CONTROL | 50/50 | 50 | 0 | 3709 | 12766 | 28724 | 28724 |
+| 1/7 | 22/50 | 17 | 5 | 5200 | 9867 | 11888 | 11888 |
+| 7/1 | 41/50 | 37 | 4 | 5565 | 14354 | 58869 | 58869 |
+
+1/7 shows a shorter p90 than CONTROL but **n=22 incomplete** and higher fail rate; 7/1 still has a long max. **INCONCLUSIVE** for short vs long ownership.
+
+## tx_done buckets (reconnect==0)
+
+- CONTROL: <2ms=18, 2-5ms=9, 5-10ms=10, 10-20ms=9, 20-40ms=4, 40-60ms=0, 60-100ms=0, timeout=0
+- 0/0: <2ms=13, 2-5ms=5, 5-10ms=10, 10-20ms=6, 20-40ms=2, 40-60ms=0, 60-100ms=2, timeout=0
+- 1/1: <2ms=12, 2-5ms=8, 5-10ms=9, 10-20ms=7, 20-40ms=2, 40-60ms=0, 60-100ms=0, timeout=0
+- 2/2: <2ms=9, 2-5ms=8, 5-10ms=8, 10-20ms=5, 20-40ms=6, 40-60ms=2, 60-100ms=0, timeout=0
+- 3/3: <2ms=8, 2-5ms=8, 5-10ms=10, 10-20ms=6, 20-40ms=7, 40-60ms=1, 60-100ms=0, timeout=0
+- 1/7: <2ms=6, 2-5ms=4, 5-10ms=10, 10-20ms=2, 20-40ms=0, 40-60ms=0, 60-100ms=0, timeout=0
+- 7/1: <2ms=16, 2-5ms=3, 5-10ms=11, 10-20ms=8, 20-40ms=2, 40-60ms=1, 60-100ms=0, timeout=0
+
+## RSSI medians
+
+- CONTROL: -35 dBm (n=50)
+- 0/0: -42 dBm (n=38)
+- 1/1: -40 dBm (n=38)
+- 2/2: -40 dBm (n=38)
+- 3/3: -41 dBm (n=40)
+- 1/7: -40 dBm (n=22)
+- 7/1: -43 dBm (n=41)
+
+CONTROL had markedly better RSSI; later variants ran with worse RSSI, confounding retry comparisons.
+
+## Answers
+
+1. **API works on ESP32-C6 / IDF 6.0.2?** yes (`retry_set_rc=0` whenever called).
+2. **0/0 semantics?** Not “off”. Tail and fails **worse** than CONTROL → likely clamp/default-like or ineffective for our frame; not a clean disable.
+3. **short vs long?** **NEITHER / INCONCLUSIVE** (incomplete 1/7, no clean CONTROL-like vs collapsed-tail split).
+4. **tx_done_wait vs limit?** No useful monotonic curve; CONTROL often best.
+5. **txStatus?** CONTROL 50/0 ok/fail; setting limits introduced fails (~4–7).
+6. **UDP delivery?** Incomplete on later variants (loss + stall); not improved by lowering MAC retry.
+7. **Long tail gone at 0/1?** **No** — 0/0 and 1/1 still show 10–20ms+ and 0/0 has 60–100ms samples.
+8. **RSSI correlation?** Yes confounder: CONTROL −35 dBm vs later ≈−40…−43 dBm.
+9. **MAC_RETRY_HYPOTHESIS_CONFIRMED=no**  
+   Observed dense RF / long `tx_done_wait` is **not** systematically explained by `esp_wifi_internal_set_retry_counter` in this run.
+10. **Latency/energy suggestion (not production):** leave default (**CONTROL** / do not call setter) until a cleaner equal-RSSI rerun.
+
+## Verdict
+
+`MAC_RETRY_HYPOTHESIS_NOT_CONFIRMED`
+
+Next candidates: management/control traffic after sendto, teardown traffic, other driver activity, or AP-side behavior — not application UDP retry (none performed).
diff --git a/experiments/analyze_mac_retry_diag.py b/experiments/analyze_mac_retry_diag.py
new file mode 100644
index 0000000..9148934
--- /dev/null
+++ b/experiments/analyze_mac_retry_diag.py
@@ -0,0 +1,220 @@
+"""Analyze prepared_mac_retry_diag.tsv and write PREPARED_MAC_RETRY_DIAG_REPORT.md."""
+
+from __future__ import annotations
+
+import csv
+import statistics
+from collections import defaultdict
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+TSV = ROOT / "experiments" / "prepared_mac_retry_diag.tsv"
+REPORT = ROOT / "experiments" / "PREPARED_MAC_RETRY_DIAG_REPORT.md"
+
+NAMES = {
+    0: "CONTROL",
+    1: "0/0",
+    2: "1/1",
+    3: "2/2",
+    4: "3/3",
+    5: "1/7",
+    6: "7/1",
+}
+
+
+def pct(vals: list[int], p: float) -> int:
+    if not vals:
+        return 0
+    s = sorted(vals)
+    i = int(round((len(s) - 1) * p / 100.0))
+    return s[max(0, min(i, len(s) - 1))]
+
+
+def buckets(vals: list[int]) -> dict[str, int]:
+    b = {
+        "<2ms": 0,
+        "2-5ms": 0,
+        "5-10ms": 0,
+        "10-20ms": 0,
+        "20-40ms": 0,
+        "40-60ms": 0,
+        "60-100ms": 0,
+        "timeout": 0,
+    }
+    for v in vals:
+        ms = v / 1000.0
+        if v >= 99000:
+            b["timeout"] += 1
+        elif ms < 2:
+            b["<2ms"] += 1
+        elif ms < 5:
+            b["2-5ms"] += 1
+        elif ms < 10:
+            b["5-10ms"] += 1
+        elif ms < 20:
+            b["10-20ms"] += 1
+        elif ms < 40:
+            b["20-40ms"] += 1
+        elif ms < 60:
+            b["40-60ms"] += 1
+        else:
+            b["60-100ms"] += 1
+    return b
+
+
+def main() -> None:
+    if not TSV.exists():
+        raise SystemExit(f"missing {TSV}")
+    rows = list(csv.DictReader(TSV.open(encoding="utf-8"), delimiter="\t"))
+    hot = [r for r in rows if r.get("kind") == "2"]
+    by_v: dict[int, list[dict]] = defaultdict(list)
+    for r in hot:
+        by_v[int(r.get("variant") or 0)].append(r)
+
+    lines: list[str] = []
+    lines.append("# Prepared MAC retry diagnostic report\n")
+    lines.append("Experiment-only. Production SendPreparedOnce unchanged.\n")
+    lines.append("aether-client-cpp SHA `157aadbec8e7b852d0f89274307ff7cb8103e5f7` unchanged=yes.\n")
+
+    lines.append("\n## Per-variant summary\n")
+    lines.append(
+        "| variant | n | delivery | tx_ok | tx_fail | rc_ok | "
+        "txdone_med | p90 | p99 | max | reconnect0_n |\n"
+    )
+    lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|\n")
+
+    stats = {}
+    for vid in range(7):
+        rs = by_v.get(vid, [])
+        # Prefer reconnect_count==0 subset for MAC analysis
+        primary = [r for r in rs if int(r.get("reconnect_count") or 0) == 0] or rs
+        txdone = [int(r["txdone_us"]) for r in primary if r.get("txdone_us")]
+        ok = sum(1 for r in primary if r.get("first_status") == "1")
+        fail = sum(1 for r in primary if r.get("first_status") == "0")
+        rc_bad = sum(1 for r in rs if int(r.get("retry_set_rc") or -1) not in (-1, 0) and vid != 0)
+        rc_ok = all(int(r.get("retry_set_rc") or -1) in (-1, 0) for r in rs) if rs else False
+        if vid != 0:
+            rc_ok = all(int(r.get("retry_set_rc") or -999) == 0 for r in rs) if rs else False
+        else:
+            rc_ok = all(int(r.get("retry_called") or 0) == 0 for r in rs) if rs else False
+        delivery = len(rs)  # received pending-hot rows
+        attempted = 50
+        med = pct(txdone, 50)
+        p90 = pct(txdone, 90)
+        p99 = pct(txdone, 99)
+        mx = max(txdone) if txdone else 0
+        stats[vid] = {
+            "n": len(primary),
+            "delivery": delivery,
+            "ok": ok,
+            "fail": fail,
+            "rc_ok": rc_ok,
+            "med": med,
+            "p90": p90,
+            "p99": p99,
+            "max": mx,
+            "txdone": txdone,
+            "buckets": buckets(txdone),
+            "rssi": [int(r["rssi"]) for r in primary if r.get("rssi")],
+        }
+        lines.append(
+            f"| {NAMES[vid]} | {len(primary)} | {delivery}/{attempted} | {ok} | {fail} | "
+            f"{'yes' if rc_ok else 'no'} | {med} | {p90} | {p99} | {mx} | {len(primary)} |\n"
+        )
+
+    lines.append("\n## Retry limit curve (CONTROL / 0/0 / 1/1 / 2/2 / 3/3)\n")
+    lines.append("| retry | delivery | tx_ok | tx_fail | med | p90 | p99 | max |\n")
+    lines.append("|---|---:|---:|---:|---:|---:|---:|---:|\n")
+    for vid in (0, 1, 2, 3, 4):
+        s = stats[vid]
+        lines.append(
+            f"| {NAMES[vid]} | {s['delivery']}/50 | {s['ok']} | {s['fail']} | "
+            f"{s['med']} | {s['p90']} | {s['p99']} | {s['max']} |\n"
+        )
+
+    lines.append("\n## Short vs long (CONTROL / 1/7 / 7/1)\n")
+    lines.append("| variant | delivery | tx_ok | tx_fail | med | p90 | p99 | max |\n")
+    lines.append("|---|---:|---:|---:|---:|---:|---:|---:|\n")
+    for vid in (0, 5, 6):
+        s = stats[vid]
+        lines.append(
+            f"| {NAMES[vid]} | {s['delivery']}/50 | {s['ok']} | {s['fail']} | "
+            f"{s['med']} | {s['p90']} | {s['p99']} | {s['max']} |\n"
+        )
+
+    # Infer short/long
+    c_p90 = stats[0]["p90"]
+    s17 = stats[5]["p90"]
+    s71 = stats[6]["p90"]
+    if c_p90 > 0 and s17 < c_p90 * 0.6 and s71 >= c_p90 * 0.8:
+        infer = "SHORT"
+    elif c_p90 > 0 and s71 < c_p90 * 0.6 and s17 >= c_p90 * 0.8:
+        infer = "LONG"
+    elif c_p90 > 0 and s17 < c_p90 * 0.7 and s71 < c_p90 * 0.7:
+        infer = "BOTH"
+    else:
+        infer = "NEITHER / INCONCLUSIVE"
+
+    # Monotonic curve?
+    curve = [stats[v]["p90"] for v in (1, 2, 3, 4, 0)]
+    mono = all(curve[i] <= curve[i + 1] * 1.15 for i in range(len(curve) - 1))
+
+    confirmed = "yes" if (infer in ("SHORT", "LONG", "BOTH") and mono) else "no"
+    if infer == "NEITHER / INCONCLUSIVE":
+        confirmed = "no"
+
+    # 0/0 semantics
+    z = stats[1]
+    if z["n"] == 0:
+        zero_sem = "no data"
+    elif not z["rc_ok"]:
+        zero_sem = "API error (retry_set_rc != ESP_OK)"
+    elif z["p90"] < max(2000, c_p90 * 0.3):
+        zero_sem = "likely disables / minimizes retries (tail collapsed)"
+    elif abs(z["p90"] - c_p90) < max(1000, c_p90 * 0.15):
+        zero_sem = "behaves like default/CONTROL"
+    else:
+        zero_sem = "clamped or partial effect"
+
+    # Best for energy
+    best = min(
+        ((vid, stats[vid]["med"], stats[vid]["delivery"]) for vid in range(7) if stats[vid]["n"]),
+        key=lambda t: (t[1], -t[2]),
+        default=(0, 0, 0),
+    )
+
+    lines.append("\n## Answers\n")
+    lines.append(f"1. API works (rc ESP_OK on non-CONTROL): **{'yes' if all(stats[v]['rc_ok'] for v in range(1,7) if stats[v]['n']) else 'mixed/no'}**\n")
+    lines.append(f"2. 0/0 semantics: **{zero_sem}**\n")
+    lines.append(f"3. Small UDP frame controlled by: **{infer}**\n")
+    lines.append(f"4. tx_done_wait vs limit: see curve; monotonic-ish={mono}\n")
+    lines.append("5-6. See tables for txStatus and delivery.\n")
+    lines.append(f"7. Long tail shrink at low retry: compare CONTROL p90={c_p90} vs 1/1 p90={stats[2]['p90']}\n")
+    lines.append("8. RSSI: listed per-variant medians below.\n")
+    lines.append(f"9. MAC_RETRY_HYPOTHESIS_CONFIRMED=**{confirmed}**\n")
+    lines.append(f"10. Latency/energy suggestion (not production): **{NAMES[best[0]]}** (med={best[1]} us, recv={best[2]})\n")
+
+    lines.append("\n## tx_done buckets (reconnect==0)\n")
+    for vid in range(7):
+        b = stats[vid]["buckets"]
+        lines.append(f"- {NAMES[vid]}: " + ", ".join(f"{k}={v}" for k, v in b.items()) + "\n")
+
+    lines.append("\n## RSSI medians\n")
+    for vid in range(7):
+        rs = stats[vid]["rssi"]
+        med = sorted(rs)[len(rs) // 2] if rs else 0
+        lines.append(f"- {NAMES[vid]}: {med} dBm (n={len(rs)})\n")
+
+    if confirmed == "no":
+        lines.append(
+            "\nMAC_RETRY_HYPOTHESIS_NOT_CONFIRMED — next candidates: "
+            "management/control traffic, teardown traffic, other driver activity.\n"
+        )
+
+    REPORT.write_text("".join(lines), encoding="utf-8")
+    print(f"wrote {REPORT}")
+    print(f"CONFIRMED={confirmed} INFER={infer}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/experiments/prepared_mac_retry_diag.tsv b/experiments/prepared_mac_retry_diag.tsv
new file mode 100644
index 0000000..fc9794a
--- /dev/null
+++ b/experiments/prepared_mac_retry_diag.tsv
@@ -0,0 +1,274 @@
+record_id	kind	outer	hot	user_us	wifi_us	connect_us	txdone_us	teardown_us	sleep_elapsed_us	sleep_overhead_us	app_entry_us	cb_seen	cb_timeout	brownout	auth	seq	diag_mode	tx_cb_total	tx_cb_success	tx_cb_failed	first_status	first_cb_delta_us	first_success_delta_us	first_failed_delta_us	last_cb_delta_us	callbacks_after_success	rssi	disconnect_count	last_disconnect_reason	reconnect_count	ap_primary	variant	short_retry	long_retry	retry_called	retry_set_rc	retry_cfg_us	encode_us	actual_channel
+36	2	1	35	290246	278622	132872	10488	116311	3040890	40890	5353	1	0	0	3	37	0	1	1	0	1	10415	10415	4294967295	10415	0	-32	0	0	0	9	0	0	0	0	-1	0	0	0
+37	2	1	36	280258	268635	135456	181	116595	3040873	40873	5353	1	0	0	3	38	0	1	1	0	1	96	96	4294967295	96	0	-33	0	0	0	9	0	0	0	0	-1	0	0	0
+38	2	1	37	320253	308629	188870	10565	84847	3040899	40899	5353	1	0	0	3	39	0	1	1	0	1	10490	10490	4294967295	10490	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+39	2	1	38	250251	238625	125143	7696	87724	3040898	40898	5353	1	0	0	3	40	0	1	1	0	1	7594	7594	4294967295	7594	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+40	2	1	39	230263	218637	128285	182	66594	3040850	40850	5353	1	0	0	3	41	0	1	1	0	1	96	96	4294967295	96	0	-33	0	0	0	9	0	0	0	0	-1	0	0	0
+41	2	1	40	250267	238640	136253	24348	62389	3040877	40877	5353	1	0	0	3	42	0	1	1	0	1	24277	24277	4294967295	24277	0	-33	0	0	0	9	0	0	0	0	-1	0	0	0
+42	2	1	41	230258	218630	128760	12766	53789	3040873	40873	5353	1	0	0	3	43	0	1	1	0	1	12693	12693	4294967295	12693	0	-33	0	0	0	9	0	0	0	0	-1	0	0	0
+43	2	1	42	280244	268616	144981	155	106642	3040875	40875	5353	1	0	0	3	44	0	1	1	0	1	84	84	4294967295	84	0	-32	0	0	0	9	0	0	0	0	-1	0	0	0
+44	2	1	43	290254	278624	127805	28484	106987	3040913	40913	5353	1	0	0	3	45	0	1	1	0	1	28410	28410	4294967295	28410	0	-32	0	0	0	9	0	0	0	0	-1	0	0	0
+45	2	1	44	310260	298630	180459	6284	89157	3040850	40850	5353	1	0	0	3	46	0	1	1	0	1	6211	6211	4294967295	6211	0	-33	0	0	0	9	0	0	0	0	-1	0	0	0
+46	2	1	45	250259	238627	130383	182	86592	3040870	40870	5353	1	0	0	3	47	0	1	1	0	1	95	95	4294967295	95	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+47	2	1	46	260252	248619	124408	2744	103957	3040836	40836	5353	1	0	0	3	48	0	1	1	0	1	2660	2660	4294967295	2660	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+48	2	1	47	240240	228606	117299	6899	89857	3040871	40871	5353	1	0	0	3	49	0	1	1	0	1	6812	6812	4294967295	6812	0	-33	0	0	0	9	0	0	0	0	-1	0	0	0
+49	2	1	48	310260	298627	216016	3138	63635	3040915	40915	5353	1	0	0	3	50	0	1	1	0	1	3053	3053	4294967295	3053	0	-32	0	0	0	9	0	0	0	0	-1	0	0	0
+50	2	1	49	230245	218611	146023	154	56646	3040877	40877	5353	1	0	0	3	51	0	1	1	0	1	84	84	4294967295	84	0	-33	0	0	0	9	0	0	0	0	-1	0	0	0
+1	1	1	0	3259803	3259803	0	0	0	3040891	40891	5353	0	0	0	0	2	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	0	0	0	0	-1	0	0	0
+2	2	1	1	260245	248610	136623	154	96645	3040877	40877	5353	1	0	0	3	3	0	1	1	0	1	84	84	4294967295	84	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+3	2	1	2	270258	258620	126874	13696	102845	3040876	40876	5353	1	0	0	3	4	0	1	1	0	1	13623	13623	4294967295	13623	0	-45	0	0	0	9	0	0	0	0	-1	0	0	0
+4	2	1	3	340252	328613	212897	6173	90582	3040913	40913	5353	1	0	0	3	5	0	1	1	0	1	6091	6091	4294967295	6091	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+5	2	1	4	240260	228621	125903	2639	84139	3040851	40851	5353	1	0	0	3	6	0	1	1	0	1	2555	2555	4294967295	2555	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+6	2	1	5	260261	248620	130801	284	96494	3040850	40850	5353	1	0	0	3	7	0	1	1	0	1	197	197	4294967295	197	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+7	2	1	6	330257	318616	211494	4274	81209	3040875	40875	5353	1	0	0	3	8	0	1	1	0	1	4189	4189	4294967295	4189	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+8	2	1	7	240256	228613	126214	12642	73913	3040873	40873	5353	1	0	0	3	9	0	1	1	0	1	12568	12568	4294967295	12568	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+9	2	1	8	400256	388612	132786	7838	227592	3040899	40899	5353	1	0	0	3	10	0	1	1	0	1	7764	7764	4294967295	7764	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+10	2	1	9	350248	338603	232476	28724	58079	3040900	40900	5353	1	0	0	3	11	0	1	1	0	1	28653	28653	4294967295	28653	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+11	2	1	10	290254	278609	184189	11126	65420	3040882	40882	5353	1	0	0	3	12	0	1	1	0	1	11053	11053	4294967295	11053	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+12	2	1	11	220238	208593	136665	154	56638	3040888	40888	5353	1	0	0	3	13	0	1	1	0	1	84	84	4294967295	84	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+13	2	1	12	250246	238599	119897	154	96646	3040862	40862	5353	1	0	0	3	14	0	1	1	0	1	84	84	4294967295	84	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+14	2	1	13	430244	418597	239993	6224	150574	3040885	40885	5353	1	0	0	3	15	0	1	1	0	1	6151	6151	4294967295	6151	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+15	2	1	14	270255	258607	193840	4719	41278	3040914	40914	5353	1	0	0	3	16	0	1	1	0	1	4635	4635	4294967295	4635	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+16	2	1	15	230246	218597	115377	7023	79776	3040849	40849	5353	1	0	0	3	17	0	1	1	0	1	6951	6951	4294967295	6951	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+17	2	1	16	220245	208596	124646	2220	64581	3040863	40863	5353	1	0	0	3	18	0	1	1	0	1	2150	2150	4294967295	2150	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+18	2	1	17	380262	368612	209789	26176	109395	3040957	40957	5353	1	0	0	3	19	0	1	1	0	1	26089	26089	4294967295	26089	0	-36	0	0	0	9	0	0	0	0	-1	0	0	0
+19	2	1	18	360248	348596	245500	1594	85207	3040899	40899	5353	1	0	0	3	20	0	1	1	0	1	1523	1523	4294967295	1523	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+20	2	1	19	310252	298599	184898	2256	94499	3040882	40882	5353	1	0	0	3	21	0	1	1	0	1	2111	2111	4294967295	2111	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+21	2	1	20	320260	308607	221171	2866	63911	3040884	40884	5353	1	0	0	3	22	0	1	1	0	1	2782	2782	4294967295	2782	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+22	2	1	21	260245	248592	137483	154	96645	3040887	40887	5353	1	0	0	3	23	0	1	1	0	1	84	84	4294967295	84	0	-36	0	0	0	9	0	0	0	0	-1	0	0	0
+23	2	1	22	260250	248595	129350	6703	88717	3040875	40875	5353	1	0	0	3	24	0	1	1	0	1	6631	6631	4294967295	6631	0	-36	0	0	0	9	0	0	0	0	-1	0	0	0
+24	2	1	23	360258	348602	227330	5327	100103	3040838	40838	5353	1	0	0	3	25	0	1	1	0	1	5247	5247	4294967295	5247	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+25	2	1	24	290252	278595	203533	155	56494	3040882	40882	5353	1	0	0	3	26	0	1	1	0	1	84	84	4294967295	84	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+26	2	1	25	230238	218582	138766	3709	53085	3040863	40863	5353	1	0	0	3	27	0	1	1	0	1	3640	3640	4294967295	3640	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+27	2	1	26	290259	278602	189490	183	66594	3040879	40879	5353	1	0	0	3	28	0	1	1	0	1	96	96	4294967295	96	0	-36	0	0	0	9	0	0	0	0	-1	0	0	0
+28	2	1	27	300251	288618	177107	230	96579	3040889	40889	5353	1	0	0	3	29	0	1	1	0	1	157	157	4294967295	157	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+29	2	1	28	270253	258646	186466	147	56439	3040872	40872	5353	1	0	0	3	30	0	1	1	0	1	74	74	4294967295	74	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+30	2	1	29	230239	218630	133774	8595	58195	3040877	40877	5353	1	0	0	3	31	0	1	1	0	1	8525	8525	4294967295	8525	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+31	2	1	30	340254	328645	182069	10433	114980	3040912	40912	5353	1	0	0	3	32	0	1	1	0	1	10358	10358	4294967295	10358	0	-35	0	0	0	9	0	0	0	0	-1	0	0	0
+33	2	1	32	320249	308638	187994	812	105981	3040856	40856	5353	1	0	0	3	34	0	1	1	0	1	739	739	4294967295	739	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+34	2	1	33	310254	298642	217103	11166	55363	3040872	40872	5353	1	0	0	3	35	0	1	1	0	1	11093	11093	4294967295	11093	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+35	2	1	34	230247	218635	138770	154	56647	3040928	40928	5353	1	0	0	3	36	0	1	1	0	1	84	84	4294967295	84	0	-34	0	0	0	9	0	0	0	0	-1	0	0	0
+32	2	0	31	230251	218816	126169	159	76751	0	0	0	1	0	0	3	33	0	1	1	0	1	87	87	4294967295	87	0	-37	0	0	0	9	0	0	0	0	-1	0	3032	9
+51	2	0	50	240242	228753	143692	11401	55243	0	0	0	1	0	0	3	52	0	1	1	0	1	11328	11328	4294967295	11328	0	-39	0	0	0	9	0	0	0	0	-1	0	3192	9
+52	1	1	0	3489803	3489803	143692	11401	55243	0	0	0	1	0	0	3	53	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	1	0	0	1	-1	0	3192	0
+53	2	1	1	250241	238743	189697	6069	20783	0	0	0	1	0	0	3	54	0	1	1	0	1	5990	5990	4294967295	5990	0	-41	0	0	0	9	1	0	0	1	0	7	3084	9
+55	2	1	3	170245	158744	113072	362	26535	0	0	0	1	0	0	3	56	0	1	0	1	0	308	4294967295	308	308	0	-44	0	0	0	9	1	0	0	1	0	7	3038	9
+56	2	1	4	180241	168737	126183	8922	17869	0	0	0	1	0	0	3	57	0	1	1	0	1	8831	8831	4294967295	8831	0	-44	0	0	0	9	1	0	0	1	0	7	3055	9
+57	2	1	5	190248	178742	127055	9792	27103	0	0	0	1	0	0	3	58	0	1	1	0	1	9722	9722	4294967295	9722	0	-40	0	0	0	9	1	0	0	1	0	7	3039	9
+58	2	1	6	1370249	1358738	1308565	1943	24953	0	0	0	1	0	0	3	59	0	1	1	0	1	1871	1871	4294967295	1871	0	-38	0	0	0	9	1	0	0	1	0	7	3041	9
+59	2	1	7	220245	208731	149583	18446	18254	0	0	0	1	0	0	3	60	0	1	1	0	1	18374	18374	4294967295	18374	0	-41	0	0	0	9	1	0	0	1	0	7	3056	9
+60	2	1	8	220251	208735	162387	160	26743	0	0	0	1	0	0	3	61	0	1	1	0	1	87	87	4294967295	87	0	-40	0	0	0	9	1	0	0	1	0	7	3040	9
+61	2	1	9	270248	258729	204873	11668	24977	0	0	0	1	0	0	3	62	0	1	1	0	1	11596	11596	4294967295	11596	0	-44	0	0	0	9	1	0	0	1	0	6	3206	9
+62	2	1	10	240238	228716	193267	174	16689	0	0	0	1	0	0	3	63	0	1	1	0	1	93	93	4294967295	93	0	-43	0	0	0	9	1	0	0	1	0	6	3078	9
+63	2	1	11	310247	298722	187000	76286	20419	0	0	0	1	0	0	3	64	0	1	1	0	1	76205	76205	4294967295	76205	0	-48	0	0	0	9	1	0	0	1	0	6	3147	9
+64	2	1	12	190247	178719	118342	2915	32623	0	0	0	1	0	0	3	65	0	1	1	0	1	2836	2836	4294967295	2836	0	-44	0	0	0	9	1	0	0	1	0	7	4384	9
+65	2	1	13	200249	188718	126187	22908	23963	0	0	0	1	0	0	3	66	0	1	1	0	1	22833	22833	4294967295	22833	0	-44	0	0	0	9	1	0	0	1	0	7	3071	9
+67	2	1	15	190242	178706	135845	1886	24979	0	0	0	1	0	0	3	68	0	1	1	0	1	1808	1808	4294967295	1808	0	-46	0	0	0	9	1	0	0	1	0	7	3080	9
+68	2	1	16	180244	168705	126778	3215	23681	0	0	0	1	0	0	3	69	0	1	1	0	1	3142	3142	4294967295	3142	0	-44	0	0	0	9	1	0	0	1	0	6	3041	9
+73	2	1	21	270232	258721	222452	1076	15772	0	0	0	1	0	0	3	74	0	1	0	1	0	1014	4294967295	1014	1014	0	-47	0	0	0	9	1	0	0	1	0	7	3080	9
+74	2	1	22	250235	238684	198142	6849	18658	0	0	0	1	0	0	3	75	0	1	1	0	1	6776	6776	4294967295	6776	0	-41	0	0	0	9	1	0	0	1	0	7	4351	9
+75	2	1	23	190250	178696	124719	10547	26342	0	0	0	1	0	0	3	76	0	1	1	0	1	10477	10477	4294967295	10477	0	-44	0	0	0	9	1	0	0	1	0	7	3045	9
+76	2	1	24	210240	198683	145884	15277	20213	0	0	0	1	0	0	3	77	0	1	1	0	1	15204	15204	4294967295	15204	0	-39	0	0	0	9	1	0	0	1	0	7	4424	9
+79	2	1	27	260248	248646	191599	3817	33069	0	0	0	1	0	0	3	80	0	1	0	1	0	3752	4294967295	3752	3752	0	-46	0	0	0	9	1	0	0	1	0	7	3049	9
+80	2	1	28	210245	198949	128807	7320	39283	0	0	0	1	0	0	3	81	0	1	1	0	1	7244	7244	4294967295	7244	0	-45	0	0	0	9	1	0	0	1	0	7	3347	9
+81	2	1	29	210241	198943	160048	1834	14749	0	0	0	1	0	0	3	82	0	1	1	0	1	1754	1754	4294967295	1754	0	-45	0	0	0	9	1	0	0	1	0	7	3363	9
+82	2	1	30	240240	228938	192983	5188	11672	0	0	0	1	0	0	3	83	0	1	1	0	1	5108	5108	4294967295	5108	0	-42	0	0	0	9	1	0	0	1	0	6	3083	9
+83	2	1	31	250238	238932	197239	11274	14240	0	0	0	1	0	0	3	84	0	1	1	0	1	11200	11200	4294967295	11200	0	-46	0	0	0	9	1	0	0	1	0	7	4402	9
+85	2	1	33	310241	298930	209263	29739	37120	0	0	0	1	0	0	3	86	0	1	0	1	0	29680	4294967295	29680	29680	0	-45	0	0	0	9	1	0	0	1	0	7	3078	9
+86	2	1	34	200242	188928	137942	8070	28711	0	0	0	1	0	0	3	87	0	1	1	0	1	8000	8000	4294967295	8000	0	-41	0	0	0	9	1	0	0	1	0	7	3055	9
+87	2	1	35	230238	218921	185540	3911	12945	0	0	0	1	0	0	3	88	0	1	1	0	1	3835	3835	4294967295	3835	0	-41	0	0	0	9	1	0	0	1	0	7	3084	9
+88	2	1	36	250237	238917	194655	8739	18034	0	0	0	1	0	0	3	89	0	1	1	0	1	8669	8669	4294967295	8669	0	-40	0	0	0	9	1	0	0	1	0	6	3057	9
+90	2	1	38	190239	178913	139453	1479	15412	0	0	0	1	0	0	3	91	0	1	0	1	0	1425	4294967295	1425	1425	0	-39	0	0	0	9	1	0	0	1	0	6	3037	9
+92	2	1	40	1420244	1408912	1366598	5583	21314	0	0	0	1	0	0	3	93	0	1	0	1	0	5527	4294967295	5527	5527	0	-42	0	0	0	9	1	0	0	1	0	6	3036	9
+93	2	1	41	210250	198916	149973	147	26532	0	0	0	1	0	0	3	94	0	1	1	0	1	76	76	4294967295	76	0	-41	0	0	0	9	1	0	0	1	0	7	3256	9
+94	2	1	42	1420247	1408909	1370822	157	16731	0	0	0	1	0	0	3	95	0	1	1	0	1	84	84	4294967295	84	0	-39	0	0	0	9	1	0	0	1	0	6	3046	9
+95	2	1	43	210238	198898	158395	4694	12103	0	0	0	1	0	0	3	96	0	1	1	0	1	4618	4618	4294967295	4618	0	-40	0	0	0	9	1	0	0	1	0	7	3142	9
+96	2	1	44	180249	168907	125638	157	26732	0	0	0	1	0	0	3	97	0	1	1	0	1	87	87	4294967295	87	0	-39	0	0	0	9	1	0	0	1	0	6	3045	9
+97	2	1	45	220247	208902	151150	17025	19574	0	0	0	1	0	0	3	98	0	1	1	0	1	16955	16955	4294967295	16955	0	-38	0	0	0	9	1	0	0	1	0	6	3051	9
+99	2	1	47	340241	328889	225783	63246	22247	0	0	0	1	0	0	3	100	0	1	0	1	0	63191	4294967295	63191	63191	0	-41	0	0	0	9	1	0	0	1	0	7	4426	9
+100	2	1	48	190239	178885	140395	170	16711	0	0	0	1	0	0	3	101	0	1	1	0	1	95	95	4294967295	95	0	-47	0	0	0	9	1	0	0	1	0	7	3063	9
+101	2	1	49	170247	158889	128294	1377	15523	0	0	0	1	0	0	3	102	0	1	1	0	1	1306	1306	4294967295	1306	0	-46	0	0	0	9	1	0	0	1	0	7	3038	9
+102	2	1	50	210237	198876	151836	9532	17244	0	0	0	1	0	0	3	103	0	1	1	0	1	9461	9461	4294967295	9461	0	-40	0	0	0	9	1	0	0	1	0	7	3055	9
+103	1	2	0	3239804	3239804	151836	9532	17244	0	0	0	1	0	0	3	104	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	2	1	1	1	-1	0	3055	0
+105	2	2	2	310249	298880	226801	22160	34721	0	0	0	1	0	0	3	106	0	1	0	1	0	22108	4294967295	22108	22108	0	-42	0	0	0	9	2	1	1	1	0	7	3053	9
+106	2	2	3	190238	178866	132420	4321	21190	0	0	0	1	0	0	3	107	0	1	1	0	1	4243	4243	4294967295	4243	0	-40	0	0	0	9	2	1	1	1	0	7	4401	9
+107	2	2	4	280245	268871	199744	5706	41201	0	0	0	1	0	0	3	108	0	1	1	0	1	5632	5632	4294967295	5632	0	-41	0	0	0	9	2	1	1	1	0	6	3045	9
+108	2	2	5	220242	208865	161051	172	26628	0	0	0	1	0	0	3	109	0	1	1	0	1	96	96	4294967295	96	0	-43	0	0	0	9	2	1	1	1	0	7	3145	9
+109	2	2	6	170247	158866	122204	925	15972	0	0	0	1	0	0	3	110	0	1	1	0	1	850	850	4294967295	850	0	-43	0	0	0	9	2	1	1	1	0	6	3039	9
+110	2	2	7	180249	168865	129021	327	16576	0	0	0	1	0	0	3	111	0	1	1	0	1	255	255	4294967295	255	0	-39	0	0	0	9	2	1	1	1	0	7	3037	9
+111	2	2	8	270237	258850	217287	9121	17653	0	0	0	1	0	0	3	112	0	1	1	0	1	9050	9050	4294967295	9050	0	-43	0	0	0	9	2	1	1	1	0	7	3056	9
+112	2	2	9	260244	248854	197626	6659	28859	0	0	0	1	0	0	3	113	0	1	1	0	1	6585	6585	4294967295	6585	0	-47	0	0	0	9	2	1	1	1	0	7	4405	9
+113	2	2	10	1370244	1358852	1310468	169	26702	0	0	0	1	0	0	3	114	0	1	1	0	1	88	88	4294967295	88	0	-45	0	0	0	9	2	1	1	1	0	6	3078	9
+114	2	2	11	250239	238845	187706	10957	25745	0	0	0	1	0	0	3	115	0	1	1	0	1	10885	10885	4294967295	10885	0	-40	0	0	0	9	2	1	1	1	0	7	3131	9
+120	2	2	17	180249	168836	123864	392	26510	0	0	0	1	0	0	3	121	0	1	1	0	1	320	320	4294967295	320	0	-47	0	0	0	9	2	1	1	1	0	7	3038	9
+121	2	2	18	1450247	1438831	1384449	18638	18253	0	0	0	1	0	0	3	122	0	1	1	0	1	18561	18561	4294967295	18561	0	-44	0	0	0	9	2	1	1	1	0	7	3043	9
+123	2	2	20	250239	238818	195224	7833	17715	0	0	0	1	0	0	3	124	0	1	1	0	1	7760	7760	4294967295	7760	0	-45	0	0	0	9	2	1	1	1	0	6	4341	9
+124	2	2	21	310247	298822	245864	22203	14695	0	0	0	1	0	0	3	125	0	1	1	0	1	22131	22131	4294967295	22131	0	-40	0	0	0	9	2	1	1	1	0	6	3040	9
+125	2	2	22	190244	178817	133904	492	26312	0	0	0	1	0	0	3	126	0	1	1	0	1	416	416	4294967295	416	0	-40	0	0	0	9	2	1	1	1	0	6	3142	9
+126	2	2	23	200251	188820	127242	449	46457	0	0	0	1	0	0	3	127	0	1	1	0	1	377	377	4294967295	377	0	-41	0	0	0	9	2	1	1	1	0	6	3036	9
+127	2	2	24	180238	168805	126791	6851	19937	0	0	0	1	0	0	3	128	0	1	1	0	1	6775	6775	4294967295	6775	0	-40	0	0	0	9	2	1	1	1	0	7	3084	9
+128	2	2	25	250248	238812	182053	9596	27050	0	0	0	1	0	0	3	129	0	1	1	0	1	9477	9477	4294967295	9477	0	-42	0	0	0	9	2	1	1	1	0	6	3206	9
+129	2	2	26	200249	188810	145005	12072	14831	0	0	0	1	0	0	3	130	0	1	1	0	1	12000	12000	4294967295	12000	0	-42	0	0	0	9	2	1	1	1	0	6	3038	9
+130	2	2	27	180238	168796	126738	5694	19803	0	0	0	1	0	0	3	131	0	1	1	0	1	5620	5620	4294967295	5620	0	-40	0	0	0	9	2	1	1	1	0	7	4418	9
+131	2	2	28	190249	178804	132286	2524	24375	0	0	0	1	0	0	3	132	0	1	1	0	1	2453	2453	4294967295	2453	0	-42	0	0	0	9	2	1	1	1	0	6	3039	9
+135	2	2	32	180245	168789	118203	4449	32450	0	0	0	1	0	0	3	136	0	1	0	1	0	4395	4294967295	4395	4395	0	-38	0	0	0	9	2	1	1	1	0	7	3037	9
+136	2	2	33	310251	298792	244613	8680	28212	0	0	0	1	0	0	3	137	0	1	1	0	1	8610	8610	4294967295	8610	0	-37	0	0	0	9	2	1	1	1	0	6	3043	9
+137	2	2	34	1360247	1348785	1315917	2945	13954	0	0	0	1	0	0	3	138	0	1	1	0	1	2869	2869	4294967295	2869	0	-37	0	0	0	9	2	1	1	1	0	7	3038	9
+138	2	2	35	220238	208773	170681	476	16325	0	0	0	1	0	0	3	139	0	1	1	0	1	400	400	4294967295	400	0	-37	0	0	0	9	2	1	1	1	0	7	3138	9
+139	2	2	36	1420249	1408781	1366352	172	26721	0	0	0	1	0	0	3	140	0	1	1	0	1	99	99	4294967295	99	0	-38	0	0	0	9	2	1	1	1	0	7	3043	9
+140	2	2	37	200244	188774	141831	159	26738	0	0	0	1	0	0	3	141	0	1	1	0	1	87	87	4294967295	87	0	-37	0	0	0	9	2	1	1	1	0	7	3037	9
+141	2	2	38	320251	308777	195300	10460	86237	0	0	0	1	0	0	3	142	0	1	1	0	1	10386	10386	4294967295	10386	0	-37	0	0	0	9	2	1	1	1	0	6	3156	9
+142	2	2	39	180242	168945	126565	3986	22376	0	0	0	1	0	0	3	143	0	1	1	0	1	3906	3906	4294967295	3906	0	-37	0	0	0	9	2	1	1	1	0	7	3084	9
+143	2	2	40	240251	228950	176475	160	36750	0	0	0	1	0	0	3	144	0	1	1	0	1	87	87	4294967295	87	0	-38	0	0	0	9	2	1	1	1	0	6	3033	9
+144	2	2	41	210247	198943	145993	17001	19899	0	0	0	1	0	0	3	145	0	1	1	0	1	16929	16929	4294967295	16929	0	-38	0	0	0	9	2	1	1	1	0	7	3038	9
+146	2	2	43	200245	188936	146766	4692	22206	0	0	0	1	0	0	3	147	0	1	0	1	0	4637	4294967295	4637	4637	0	-38	0	0	0	9	2	1	1	1	0	7	3038	9
+147	2	2	44	180240	168928	135474	4658	12220	0	0	0	1	0	0	3	148	0	1	1	0	1	4591	4591	4294967295	4591	0	-37	0	0	0	9	2	1	1	1	0	7	3051	9
+148	2	2	45	250237	238921	191568	6812	18693	0	0	0	1	0	0	3	149	0	1	1	0	1	6739	6739	4294967295	6739	0	-38	0	0	0	9	2	1	1	1	0	7	4335	9
+149	2	2	46	210246	198928	151070	13629	13271	0	0	0	1	0	0	3	150	0	1	1	0	1	13558	13558	4294967295	13558	0	-39	0	0	0	9	2	1	1	1	0	7	3038	9
+150	2	2	47	310250	298929	225078	17289	39365	0	0	0	1	0	0	3	151	0	1	1	0	1	17212	17212	4294967295	17212	0	-37	0	0	0	9	2	1	1	1	0	7	3207	9
+151	2	2	48	220244	208921	169589	540	16369	0	0	0	1	0	0	3	152	0	1	1	0	1	459	459	4294967295	459	0	-36	0	0	0	9	2	1	1	1	0	7	3039	9
+153	2	2	50	200248	188918	139214	2307	24576	0	0	0	1	0	0	3	154	0	1	0	1	0	2252	4294967295	2252	2252	0	-38	0	0	0	9	2	1	1	1	0	7	3049	9
+154	1	3	0	3309804	3309804	139214	2307	24576	0	0	0	1	0	0	3	155	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	3	2	2	1	-1	0	3049	0
+155	2	3	1	190247	178911	139812	159	16739	0	0	0	1	0	0	3	156	0	1	1	0	1	87	87	4294967295	87	0	-37	0	0	0	9	3	2	2	1	0	7	3038	9
+157	2	3	3	250251	238909	186350	159	36742	0	0	0	1	0	0	3	158	0	1	1	0	1	87	87	4294967295	87	0	-38	0	0	0	9	3	2	2	1	0	6	3041	9
+161	2	3	7	280238	268885	222361	8592	16903	0	0	0	1	0	0	3	162	0	1	1	0	1	8518	8518	4294967295	8518	0	-39	0	0	0	9	3	2	2	1	0	6	4420	9
+162	2	3	8	250240	238885	192009	6667	20198	0	0	0	1	0	0	3	163	0	1	1	0	1	6597	6597	4294967295	6597	0	-38	0	0	0	9	3	2	2	1	0	6	3057	9
+166	2	3	12	250238	238871	200675	381	16416	0	0	0	1	0	0	3	167	0	1	1	0	1	304	304	4294967295	304	0	-42	0	0	0	9	3	2	2	1	0	7	3144	9
+167	2	3	13	200250	188880	128532	1162	35733	0	0	0	1	0	0	3	168	0	1	1	0	1	1092	1092	4294967295	1092	0	-42	0	0	0	9	3	2	2	1	0	7	3040	9
+168	2	3	14	250244	238871	135365	48391	37220	0	0	0	1	0	0	3	169	0	1	1	0	1	48317	48317	4294967295	48317	0	-40	0	0	0	9	3	2	2	1	0	7	3100	9
+169	2	3	15	190242	178866	130864	2316	24548	0	0	0	1	0	0	3	170	0	1	1	0	1	2238	2238	4294967295	2238	0	-42	0	0	0	9	3	2	2	1	0	7	3080	9
+170	2	3	16	240234	228854	178809	6556	18927	0	0	0	1	0	0	3	171	0	1	1	0	1	6481	6481	4294967295	6481	0	-38	0	0	0	9	3	2	2	1	0	6	4425	9
+172	2	3	18	160241	148856	116894	3885	13009	0	0	0	1	0	0	3	173	0	1	0	1	0	3832	4294967295	3832	3832	0	-40	0	0	0	9	3	2	2	1	0	6	3037	9
+174	2	3	20	200247	188857	140510	3265	23636	0	0	0	1	0	0	3	175	0	1	0	1	0	3209	4294967295	3209	3209	0	-40	0	0	0	9	3	2	2	1	0	7	3037	9
+175	2	3	21	250242	238849	126088	50821	45975	0	0	0	1	0	0	3	176	0	1	1	0	1	50750	50750	4294967295	50750	0	-39	0	0	0	9	3	2	2	1	0	7	3036	9
+176	2	3	22	1390250	1378853	1320076	11384	25513	0	0	0	1	0	0	3	177	0	1	1	0	1	11307	11307	4294967295	11307	0	-41	0	0	0	9	3	2	2	1	0	7	3042	9
+177	2	3	23	210251	198851	152377	2989	23902	0	0	0	1	0	0	3	178	0	1	1	0	1	2919	2919	4294967295	2919	0	-38	0	0	0	9	3	2	2	1	0	7	3046	9
+178	2	3	24	220247	208844	146232	4357	42387	0	0	0	1	0	0	3	179	0	1	1	0	1	4285	4285	4294967295	4285	0	-41	0	0	0	9	3	2	2	1	0	7	3037	9
+179	2	3	25	230244	218838	168535	10363	15146	0	0	0	1	0	0	3	180	0	1	1	0	1	10290	10290	4294967295	10290	0	-39	0	0	0	9	3	2	2	1	0	7	4348	9
+180	2	3	26	230251	218842	148014	25845	31058	0	0	0	1	0	0	3	181	0	1	1	0	1	25775	25775	4294967295	25775	0	-39	0	0	0	9	3	2	2	1	0	6	3035	9
+182	2	3	28	190251	178837	146094	316	16585	0	0	0	1	0	0	3	183	0	1	1	0	1	244	244	4294967295	244	0	-41	0	0	0	9	3	2	2	1	0	7	3041	9
+183	2	3	29	270251	258834	207264	1088	35814	0	0	0	1	0	0	3	184	0	1	1	0	1	1016	1016	4294967295	1016	0	-41	0	0	0	9	3	2	2	1	0	7	3040	9
+184	2	3	30	210246	198827	147503	20067	16834	0	0	0	1	0	0	3	185	0	1	1	0	1	19995	19995	4294967295	19995	0	-40	0	0	0	9	3	2	2	1	0	6	3037	9
+185	2	3	31	200238	188816	128137	17641	27910	0	0	0	1	0	0	3	186	0	1	1	0	1	17562	17562	4294967295	17562	0	-39	0	0	0	9	3	2	2	1	0	7	4364	9
+186	2	3	32	190247	178821	134613	6533	20357	0	0	0	1	0	0	3	187	0	1	1	0	1	6461	6461	4294967295	6461	0	-41	0	0	0	9	3	2	2	1	0	7	3039	9
+188	2	3	34	190241	178809	124270	23794	13099	0	0	0	1	0	0	3	189	0	1	0	1	0	23742	4294967295	23742	23742	0	-41	0	0	0	9	3	2	2	1	0	7	3037	9
+189	2	3	35	180240	168805	126536	9065	17714	0	0	0	1	0	0	3	190	0	1	1	0	1	8994	8994	4294967295	8994	0	-39	0	0	0	9	3	2	2	1	0	6	3055	9
+190	2	3	36	240251	228814	139033	32746	34157	0	0	0	1	0	0	3	191	0	1	1	0	1	32674	32674	4294967295	32674	0	-39	0	0	0	9	3	2	2	1	0	6	3038	9
+191	2	3	37	220250	208810	141092	24145	22744	0	0	0	1	0	0	3	192	0	1	1	0	1	24075	24075	4294967295	24075	0	-42	0	0	0	9	3	2	2	1	0	6	3045	9
+192	2	3	38	210255	198812	145581	8264	28631	0	0	0	1	0	0	3	193	0	1	1	0	1	8194	8194	4294967295	8194	0	-39	0	0	0	9	3	2	2	1	0	7	3049	9
+193	2	3	39	200234	188788	151741	4042	11538	0	0	0	1	0	0	3	194	0	1	1	0	1	3966	3966	4294967295	3966	0	-41	0	0	0	9	3	2	2	1	0	7	3118	9
+194	2	3	40	250234	238785	192607	14373	12244	0	0	0	1	0	0	3	195	0	1	1	0	1	14298	14298	4294967295	14298	0	-39	0	0	0	9	3	2	2	1	0	7	3206	9
+195	2	3	41	250250	238798	193814	147	26525	0	0	0	1	0	0	3	196	0	1	1	0	1	76	76	4294967295	76	0	-39	0	0	0	9	3	2	2	1	0	7	3261	9
+196	2	3	42	300251	288796	232732	162	36730	0	0	0	1	0	0	3	197	0	1	1	0	1	92	92	4294967295	92	0	-39	0	0	0	9	3	2	2	1	0	6	3043	9
+197	2	3	43	190238	178780	139669	3327	13533	0	0	0	1	0	0	3	198	0	1	1	0	1	3249	3249	4294967295	3249	0	-45	0	0	0	9	3	2	2	1	0	7	3080	9
+198	2	3	44	260242	248782	203374	1566	25300	0	0	0	1	0	0	3	199	0	1	1	0	1	1487	1487	4294967295	1487	0	-43	0	0	0	9	3	2	2	1	0	7	3080	9
+199	2	3	45	190247	178783	133762	14112	12789	0	0	0	1	0	0	3	200	0	1	1	0	1	14040	14040	4294967295	14040	0	-39	0	0	0	9	3	2	2	1	0	7	3037	9
+200	2	3	46	180238	168771	130784	3378	13478	0	0	0	1	0	0	3	201	0	1	1	0	1	3301	3301	4294967295	3301	0	-41	0	0	0	9	3	2	2	1	0	7	3084	9
+201	2	3	47	250240	238771	195226	8340	18436	0	0	0	1	0	0	3	202	0	1	1	0	1	8270	8270	4294967295	8270	0	-41	0	0	0	9	3	2	2	1	0	7	3057	9
+203	2	3	49	250243	238793	188170	24031	12863	0	0	0	1	0	0	3	204	0	1	0	1	0	23978	4294967295	23978	23978	0	-43	0	0	0	9	3	2	2	1	0	6	3040	9
+204	2	3	50	180240	168941	125344	6742	18756	0	0	0	1	0	0	3	205	0	1	1	0	1	6669	6669	4294967295	6669	0	-43	0	0	0	9	3	2	2	1	0	6	4422	9
+205	1	4	0	3529804	3529804	125344	6742	18756	0	0	0	1	0	0	3	206	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	4	3	3	1	-1	0	4422	0
+206	2	4	1	200247	188939	133303	25613	11287	0	0	0	1	0	0	3	207	0	1	1	0	1	25543	25543	4294967295	25543	0	-41	0	0	0	9	4	3	3	1	0	6	3036	9
+207	2	4	2	180238	168930	130130	2982	13875	0	0	0	1	0	0	3	208	0	1	1	0	1	2906	2906	4294967295	2906	0	-39	0	0	0	9	4	3	3	1	0	7	3083	9
+208	2	4	3	280246	268934	201760	15694	31107	0	0	0	1	0	0	3	209	0	1	1	0	1	15618	15618	4294967295	15618	0	-41	0	0	0	9	4	3	3	1	0	7	3144	9
+210	2	4	5	220247	208931	136830	27051	29850	0	0	0	1	0	0	3	211	0	1	0	1	0	26995	4294967295	26995	26995	0	-41	0	0	0	9	4	3	3	1	0	7	3037	9
+212	2	4	7	240242	228920	180280	2783	24077	0	0	0	1	0	0	3	213	0	1	1	0	1	2707	2707	4294967295	2707	0	-39	0	0	0	9	4	3	3	1	0	7	3084	9
+213	2	4	8	200251	188925	133592	7479	29422	0	0	0	1	0	0	3	214	0	1	1	0	1	7407	7407	4294967295	7407	0	-40	0	0	0	9	4	3	3	1	0	7	3039	9
+214	2	4	9	210245	198917	166349	2050	14836	0	0	0	1	0	0	3	215	0	1	1	0	1	1980	1980	4294967295	1980	0	-41	0	0	0	9	4	3	3	1	0	7	3044	9
+215	2	4	10	190253	178921	118018	20391	26519	0	0	0	1	0	0	3	216	0	1	1	0	1	20320	20320	4294967295	20320	0	-40	0	0	0	9	4	3	3	1	0	6	3034	9
+217	2	4	12	180253	168915	127180	3925	22978	0	0	0	1	0	0	3	218	0	1	1	0	1	3853	3853	4294967295	3853	0	-43	0	0	0	9	4	3	3	1	0	7	3039	9
+218	2	4	13	270250	258910	215423	231	26671	0	0	0	1	0	0	3	219	0	1	1	0	1	159	159	4294967295	159	0	-41	0	0	0	9	4	3	3	1	0	7	3041	9
+219	2	4	14	190254	178911	137227	240	26662	0	0	0	1	0	0	3	220	0	1	1	0	1	168	168	4294967295	168	0	-39	0	0	0	9	4	3	3	1	0	7	3038	9
+220	2	4	15	160249	148903	114012	5229	11678	0	0	0	1	0	0	3	221	0	1	1	0	1	5157	5157	4294967295	5157	0	-39	0	0	0	9	4	3	3	1	0	6	3034	9
+222	2	4	17	300250	288898	231039	5303	31588	0	0	0	1	0	0	3	223	0	1	0	1	0	5252	4294967295	5252	5252	0	-41	0	0	0	9	4	3	3	1	0	7	3042	9
+223	2	4	18	210248	198893	151640	174	26735	0	0	0	1	0	0	3	224	0	1	1	0	1	93	93	4294967295	93	0	-39	0	0	0	9	4	3	3	1	0	7	3040	9
+224	2	4	19	250244	238887	174223	20037	25455	0	0	0	1	0	0	3	225	0	1	1	0	1	19964	19964	4294967295	19964	0	-40	0	0	0	9	4	3	3	1	0	7	4426	9
+229	2	4	24	260244	248873	144772	17220	69678	0	0	0	1	0	0	3	230	0	1	1	0	1	17149	17149	4294967295	17149	0	-41	0	0	0	9	4	3	3	1	0	7	3037	9
+230	2	4	25	230238	218864	179996	1566	15290	0	0	0	1	0	0	3	231	0	1	1	0	1	1490	1490	4294967295	1490	0	-39	0	0	0	9	4	3	3	1	0	7	3084	9
+231	2	4	26	260252	248874	197629	15184	21620	0	0	0	1	0	0	3	232	0	1	1	0	1	15106	15106	4294967295	15106	0	-41	0	0	0	9	4	3	3	1	0	6	3055	9
+232	2	4	27	210247	198866	159019	1788	15116	0	0	0	1	0	0	3	233	0	1	1	0	1	1716	1716	4294967295	1716	0	-42	0	0	0	9	4	3	3	1	0	6	3034	9
+233	2	4	28	280251	268868	214757	8960	26576	0	0	0	1	0	0	3	234	0	1	1	0	1	8885	8885	4294967295	8885	0	-41	0	0	0	9	4	3	3	1	0	7	4332	9
+234	2	4	29	230238	218852	181565	4135	12739	0	0	0	1	0	0	3	235	0	1	1	0	1	4068	4068	4294967295	4068	0	-41	0	0	0	9	4	3	3	1	0	7	3053	9
+236	2	4	31	210243	198852	153217	6942	19955	0	0	0	1	0	0	3	237	0	1	0	1	0	6889	4294967295	6889	6889	0	-43	0	0	0	9	4	3	3	1	0	7	3036	9
+237	2	4	32	180238	168844	133090	343	16520	0	0	0	1	0	0	3	238	0	1	1	0	1	264	264	4294967295	264	0	-42	0	0	0	9	4	3	3	1	0	7	3080	9
+238	2	4	33	200245	188847	140580	11557	15340	0	0	0	1	0	0	3	239	0	1	1	0	1	11487	11487	4294967295	11487	0	-41	0	0	0	9	4	3	3	1	0	6	3037	9
+239	2	4	34	180242	168842	131565	4852	11682	0	0	0	1	0	0	3	240	0	1	1	0	1	4775	4775	4294967295	4775	0	-41	0	0	0	9	4	3	3	1	0	6	3402	9
+240	2	4	35	220251	208847	140807	15800	31104	0	0	0	1	0	0	3	241	0	1	1	0	1	15729	15729	4294967295	15729	0	-42	0	0	0	9	4	3	3	1	0	7	3037	9
+241	2	4	36	270236	258829	215582	5067	20432	0	0	0	1	0	0	3	242	0	1	1	0	1	4989	4989	4294967295	4989	0	-41	0	0	0	9	4	3	3	1	0	7	4412	9
+242	2	4	37	300241	288831	184573	46327	40460	0	0	0	1	0	0	3	243	0	1	1	0	1	46258	46258	4294967295	46258	0	-39	0	0	0	9	4	3	3	1	0	7	3037	9
+244	2	4	39	210245	198830	140981	21374	15521	0	0	0	1	0	0	3	245	0	1	0	1	0	21321	4294967295	21321	21321	0	-39	0	0	0	9	4	3	3	1	0	6	3039	9
+245	2	4	40	180236	168817	129345	2236	14622	0	0	0	1	0	0	3	246	0	1	1	0	1	2158	2158	4294967295	2158	0	-41	0	0	0	9	4	3	3	1	0	7	3079	9
+246	2	4	41	330239	318817	194318	7343	98158	0	0	0	1	0	0	3	247	0	1	1	0	1	7265	7265	4294967295	7265	0	-41	0	0	0	9	4	3	3	1	0	6	4408	9
+247	2	4	42	270236	258811	214723	5038	20463	0	0	0	1	0	0	3	248	0	1	1	0	1	4960	4960	4294967295	4960	0	-41	0	0	0	9	4	3	3	1	0	6	4411	9
+248	2	4	43	250242	238815	189159	5058	21798	0	0	0	1	0	0	3	249	0	1	1	0	1	4982	4982	4294967295	4982	0	-41	0	0	0	9	4	3	3	1	0	6	3083	9
+249	2	4	44	210251	198821	138127	23152	23754	0	0	0	1	0	0	3	250	0	1	1	0	1	23082	23082	4294967295	23082	0	-41	0	0	0	9	4	3	3	1	0	6	3034	9
+250	2	4	45	270240	258807	213166	10049	16591	0	0	0	1	0	0	3	251	0	1	1	0	1	9980	9980	4294967295	9980	0	-41	0	0	0	9	4	3	3	1	0	7	3056	9
+251	2	4	46	250240	238804	195564	9334	17443	0	0	0	1	0	0	3	252	0	1	1	0	1	9264	9264	4294967295	9264	0	-39	0	0	0	9	4	3	3	1	0	7	3056	9
+252	2	4	47	190251	178812	136350	215	26691	0	0	0	1	0	0	3	253	0	1	1	0	1	140	140	4294967295	140	0	-44	0	0	0	9	4	3	3	1	0	7	3037	9
+253	2	4	48	190246	178805	133804	159	26743	0	0	0	1	0	0	3	254	0	1	1	0	1	87	87	4294967295	87	0	-39	0	0	0	9	4	3	3	1	0	7	3036	9
+254	2	4	49	250249	238805	153265	27659	39233	0	0	0	1	0	0	3	255	0	1	1	0	1	27590	27590	4294967295	27590	0	-41	0	0	0	9	4	3	3	1	0	7	3043	9
+255	2	4	50	200246	188801	142791	2077	24823	0	0	0	1	0	0	3	256	0	1	1	0	1	2006	2006	4294967295	2006	0	-40	0	0	0	9	4	3	3	1	0	7	3038	9
+256	1	5	0	3239804	3239804	142791	2077	24823	0	0	0	1	0	0	3	257	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	5	1	7	1	-1	0	3038	0
+258	2	5	2	300246	288790	249012	1911	14988	0	0	0	1	0	0	3	259	0	1	0	1	0	1857	4294967295	1857	1857	0	-42	0	0	0	9	5	1	7	1	0	7	3037	9
+259	2	5	3	200245	188787	146439	3514	23355	0	0	0	1	0	0	3	260	0	1	1	0	1	3431	3431	4294967295	3431	0	-41	0	0	0	9	5	1	7	1	0	7	3080	9
+260	2	5	4	230251	218789	149909	7484	39415	0	0	0	1	0	0	3	261	0	1	1	0	1	7412	7412	4294967295	7412	0	-39	0	0	0	9	5	1	7	1	0	7	3044	9
+261	2	5	5	260239	248774	189389	6299	30550	0	0	0	1	0	0	3	262	0	1	0	1	0	6241	4294967295	6241	6241	0	-41	0	0	0	9	5	1	7	1	0	6	3083	9
+262	2	5	6	230238	218770	174352	4580	20914	0	0	0	1	0	0	3	263	0	1	1	0	1	4506	4506	4294967295	4506	0	-39	0	0	0	9	5	1	7	1	0	6	4421	9
+263	2	5	7	200244	188774	139627	3235	23660	0	0	0	1	0	0	3	264	0	1	1	0	1	3163	3163	4294967295	3163	0	-39	0	0	0	9	5	1	7	1	0	6	3039	9
+264	2	5	8	230245	218772	128566	621	66242	0	0	0	1	0	0	3	265	0	1	1	0	1	542	542	4294967295	542	0	-40	0	0	0	9	5	1	7	1	0	7	3084	9
+266	2	5	10	190232	178932	139588	1397	15447	0	0	0	1	0	0	3	267	0	1	0	1	0	1337	4294967295	1337	1337	0	-41	0	0	0	9	5	1	7	1	0	7	3084	9
+267	2	5	11	260244	248941	194340	9478	26071	0	0	0	1	0	0	3	268	0	1	1	0	1	9402	9402	4294967295	9402	0	-41	0	0	0	9	5	1	7	1	0	7	4374	9
+272	2	5	16	180242	168789	126356	7317	18185	0	0	0	1	0	0	3	273	0	1	1	0	1	7244	7244	4294967295	7244	0	-41	0	0	0	9	5	1	7	1	0	6	4347	9
+273	2	5	17	240237	228782	180699	11888	14741	0	0	0	1	0	0	3	274	0	1	1	0	1	11816	11816	4294967295	11816	0	-41	0	0	0	9	5	1	7	1	0	6	3203	9
+280	2	5	24	180240	168764	133297	5200	11657	0	0	0	1	0	0	3	281	0	1	1	0	1	5125	5125	4294967295	5125	0	-40	0	0	0	9	5	1	7	1	0	7	3083	9
+282	2	5	26	200245	188763	153352	1677	15208	0	0	0	1	0	0	3	283	0	1	0	1	0	1621	4294967295	1621	1621	0	-46	0	0	0	9	5	1	7	1	0	7	3047	9
+283	2	5	27	200249	188763	132527	8961	27938	0	0	0	1	0	0	3	284	0	1	1	0	1	8889	8889	4294967295	8889	0	-41	0	0	0	9	5	1	7	1	0	6	3041	9
+284	2	5	28	270237	258750	212834	11701	14947	0	0	0	1	0	0	3	285	0	1	1	0	1	11624	11624	4294967295	11624	0	-40	0	0	0	9	5	1	7	1	0	7	3184	9
+286	2	5	30	180249	168755	115961	1078	35825	0	0	0	1	0	0	3	287	0	1	1	0	1	1006	1006	4294967295	1006	0	-39	0	0	0	9	5	1	7	1	0	7	3037	9
+287	2	5	31	280238	268741	218845	5427	20132	0	0	0	1	0	0	3	288	0	1	1	0	1	5351	5351	4294967295	5351	0	-38	0	0	0	9	5	1	7	1	0	6	4355	9
+288	2	5	32	330250	318750	272845	1987	24910	0	0	0	1	0	0	3	289	0	1	1	0	1	1914	1914	4294967295	1914	0	-39	0	0	0	9	5	1	7	1	0	7	3041	9
+289	2	5	33	1440244	1428742	1386530	7289	19601	0	0	0	1	0	0	3	290	0	1	1	0	1	7215	7215	4294967295	7215	0	-39	0	0	0	9	5	1	7	1	0	6	3043	9
+303	2	5	47	190237	178864	127071	9867	26750	0	0	0	1	0	0	3	304	0	1	0	1	0	9810	4294967295	9810	9810	0	-38	0	0	0	9	5	1	7	1	0	6	3213	9
+304	2	5	48	200251	188875	131517	2590	34315	0	0	0	1	0	0	3	305	0	1	1	0	1	2518	2518	4294967295	2518	0	-38	0	0	0	9	5	1	7	1	0	7	3037	9
+305	2	5	49	190244	178865	125867	6785	28735	0	0	0	1	0	0	3	306	0	1	1	0	1	6711	6711	4294967295	6711	0	-36	0	0	0	9	5	1	7	1	0	6	4403	9
+308	2	6	1	180247	168858	124763	16	26563	0	0	0	1	0	0	3	309	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	6	7	1	1	0	6	3347	9
+309	2	6	2	210251	198861	148256	5565	31341	0	0	0	1	0	0	3	310	0	1	1	0	1	5492	5492	4294967295	5492	0	-39	0	0	0	9	6	7	1	1	0	7	3037	9
+310	2	6	3	220247	208854	144186	20400	26045	0	0	0	1	0	0	3	311	0	1	1	0	1	20321	20321	4294967295	20321	0	-37	0	0	0	9	6	7	1	1	0	7	3262	9
+311	2	6	4	220242	208847	159473	1308	25513	0	0	0	1	0	0	3	312	0	1	1	0	1	1232	1232	4294967295	1232	0	-38	0	0	0	9	6	7	1	1	0	7	3125	9
+312	2	6	5	260248	248850	192770	1096	35816	0	0	0	1	0	0	3	313	0	1	1	0	1	1018	1018	4294967295	1018	0	-37	0	0	0	9	6	7	1	1	0	7	3043	9
+313	2	6	6	240240	228839	166560	7156	38355	0	0	0	1	0	0	3	314	0	1	1	0	1	7079	7079	4294967295	7079	0	-38	0	0	0	9	6	7	1	1	0	7	4402	9
+314	2	6	7	230247	218842	160186	299	36582	0	0	0	1	0	0	3	315	0	1	1	0	1	87	87	4294967295	87	0	-48	0	0	0	9	6	7	1	1	0	6	3041	9
+315	2	6	8	210251	198843	135955	6871	40035	0	0	0	1	0	0	3	316	0	1	1	0	1	6799	6799	4294967295	6799	0	-48	0	0	0	9	6	7	1	1	0	7	3037	9
+316	2	6	9	280245	268834	213793	13923	22714	0	0	0	1	0	0	3	317	0	1	1	0	1	13847	13847	4294967295	13847	0	-40	0	0	0	9	6	7	1	1	0	6	3201	9
+318	2	6	11	410248	398832	278285	58869	47929	0	0	0	1	0	0	3	319	0	1	0	1	0	58815	4294967295	58815	58815	0	-40	0	0	0	9	6	7	1	1	0	7	3038	9
+319	2	6	12	190244	178825	120006	12031	24606	0	0	0	1	0	0	3	320	0	1	1	0	1	11959	11959	4294967295	11959	0	-49	0	0	0	9	6	7	1	1	0	7	3197	9
+321	2	6	14	230247	218823	145058	20728	36173	0	0	0	1	0	0	3	322	0	1	0	1	0	20675	4294967295	20675	20675	0	-46	0	0	0	9	6	7	1	1	0	7	3036	9
+323	2	6	16	230247	218817	151858	16483	30412	0	0	0	1	0	0	3	324	0	1	0	1	0	16430	4294967295	16430	16430	0	-47	0	0	0	9	6	7	1	1	0	7	3037	9
+324	2	6	17	270243	258810	215426	3898	22950	0	0	0	1	0	0	3	325	0	1	1	0	1	3819	3819	4294967295	3819	0	-43	0	0	0	9	6	7	1	1	0	7	3086	9
+325	2	6	18	250242	238808	196797	695	26107	0	0	0	1	0	0	3	326	0	1	1	0	1	619	619	4294967295	619	0	-46	0	0	0	9	6	7	1	1	0	7	3143	9
+326	2	6	19	200251	188811	126659	11052	35850	0	0	0	1	0	0	3	327	0	1	1	0	1	10977	10977	4294967295	10977	0	-44	0	0	0	9	6	7	1	1	0	7	3041	9
+327	2	6	20	200242	188800	132083	3595	33273	0	0	0	1	0	0	3	328	0	1	1	0	1	3514	3514	4294967295	3514	0	-46	0	0	0	9	6	7	1	1	0	7	3078	9
+328	2	6	21	250251	238805	142024	6457	70448	0	0	0	1	0	0	3	329	0	1	1	0	1	6385	6385	4294967295	6385	0	-45	0	0	0	9	6	7	1	1	0	7	3037	9
+329	2	6	22	240244	228797	164486	7842	39019	0	0	0	1	0	0	3	330	0	1	1	0	1	7768	7768	4294967295	7768	0	-45	0	0	0	9	6	7	1	1	0	7	3083	9
+330	2	6	23	200251	188800	129823	9539	27363	0	0	0	1	0	0	3	331	0	1	1	0	1	9468	9468	4294967295	9468	0	-45	0	0	0	9	6	7	1	1	0	7	3038	9
+331	2	6	24	300245	288791	209844	7969	47546	0	0	0	1	0	0	3	332	0	1	1	0	1	7891	7891	4294967295	7891	0	-44	0	0	0	9	6	7	1	1	0	7	4400	9
+332	2	6	25	240245	228788	162710	14354	32417	0	0	0	1	0	0	3	333	0	1	1	0	1	14278	14278	4294967295	14278	0	-41	0	0	0	9	6	7	1	1	0	6	3083	9
+333	2	6	26	200253	188793	130420	10758	26147	0	0	0	1	0	0	3	334	0	1	1	0	1	10686	10686	4294967295	10686	0	-45	0	0	0	9	6	7	1	1	0	7	3037	9
+334	2	6	27	250253	238790	194356	322	26589	0	0	0	1	0	0	3	335	0	1	1	0	1	250	250	4294967295	250	0	-48	0	0	0	9	6	7	1	1	0	7	3034	9
+338	2	6	31	210250	198775	149849	345	26544	0	0	0	1	0	0	3	339	0	1	1	0	1	275	275	4294967295	275	0	-43	0	0	0	9	6	7	1	1	0	7	3045	9
+339	2	6	32	230244	218768	151924	12229	34638	0	0	0	1	0	0	3	340	0	1	1	0	1	12149	12149	4294967295	12149	0	-44	0	0	0	9	6	7	1	1	0	7	3081	9
+340	2	6	33	190251	178771	120934	1027	35878	0	0	0	1	0	0	3	341	0	1	1	0	1	955	955	4294967295	955	0	-41	0	0	0	9	6	7	1	1	0	7	3038	9
+341	2	6	34	240251	228768	172371	1385	35506	0	0	0	1	0	0	3	342	0	1	1	0	1	1316	1316	4294967295	1316	0	-45	0	0	0	9	6	7	1	1	0	7	3046	9
+342	2	6	35	200242	188757	129969	291	36576	0	0	0	1	0	0	3	343	0	1	1	0	1	212	212	4294967295	212	0	-41	0	0	0	9	6	7	1	1	0	7	3079	9
+344	2	6	37	280242	268751	214643	921	35939	0	0	0	1	0	0	3	345	0	1	1	0	1	845	845	4294967295	845	0	-47	0	0	0	9	6	7	1	1	0	7	3084	9
+345	2	6	38	280249	268754	175198	6597	68912	0	0	0	1	0	0	3	346	0	1	1	0	1	6525	6525	4294967295	6525	0	-44	0	0	0	9	6	7	1	1	0	6	4352	9
+346	2	6	39	190251	178753	134286	159	26746	0	0	0	1	0	0	3	347	0	1	1	0	1	87	87	4294967295	87	0	-42	0	0	0	9	6	7	1	1	0	7	3038	9
+347	2	6	40	210251	198750	146526	7271	29635	0	0	0	1	0	0	3	348	0	1	1	0	1	7199	7199	4294967295	7199	0	-45	0	0	0	9	6	7	1	1	0	7	3036	9
+348	2	6	41	190242	178740	133393	4074	22786	0	0	0	1	0	0	3	349	0	1	1	0	1	3999	3999	4294967295	3999	0	-45	0	0	0	9	6	7	1	1	0	7	3084	9
+349	2	6	42	260240	248735	189514	738	36110	0	0	0	1	0	0	3	350	0	1	1	0	1	662	662	4294967295	662	0	-39	0	0	0	9	6	7	1	1	0	7	3092	9
+350	2	6	43	220251	208742	164105	159	26742	0	0	0	1	0	0	3	351	0	1	1	0	1	87	87	4294967295	87	0	-40	0	0	0	9	6	7	1	1	0	7	3041	9
+351	2	6	44	210251	198739	142341	1326	35582	0	0	0	1	0	0	3	352	0	1	1	0	1	1254	1254	4294967295	1254	0	-43	0	0	0	9	6	7	1	1	0	6	3034	9
+353	2	6	46	190249	178732	122374	10920	25978	0	0	0	1	0	0	3	354	0	1	0	1	0	10867	4294967295	10867	10867	0	-40	0	0	0	9	6	7	1	1	0	6	3041	9
+354	2	6	47	290242	278722	209995	5241	40247	0	0	0	1	0	0	3	355	0	1	1	0	1	5166	5166	4294967295	5166	0	-40	0	0	0	9	6	7	1	1	0	6	4426	9
+355	2	6	48	310244	298721	176969	5603	99935	0	0	0	1	0	0	3	356	0	1	1	0	1	5526	5526	4294967295	5526	0	-42	0	0	0	9	6	7	1	1	0	6	4383	9
+356	2	6	49	200253	188727	138553	325	26565	0	0	0	1	0	0	3	357	0	1	1	0	1	255	255	4294967295	255	0	-42	0	0	0	9	6	7	1	1	0	7	3050	9
diff --git a/experiments/run_mac_retry_diag.py b/experiments/run_mac_retry_diag.py
new file mode 100644
index 0000000..61c5c5d
--- /dev/null
+++ b/experiments/run_mac_retry_diag.py
@@ -0,0 +1,326 @@
+"""
+One-flash MAC retry campaign orchestrator.
+COM only for flash; after FLASH_OK never touch serial.
+Progress = receiver TSV/log only.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+BUILD = ROOT / "build-esp32c6-save-bench-smoke"
+AETHER = r"C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0"
+PY = Path(r"C:\Espressif\python_env\idf6.0_py3.11_env\Scripts\python.exe")
+CMAKE = Path(r"C:\Espressif\tools\cmake\3.30.2\bin\cmake.exe")
+NINJA = Path(r"C:\Espressif\tools\ninja\1.12.1\ninja.exe")
+RX_EXE = ROOT / "temperature_receiver" / "build-bisect" / "temperature_receiver.exe"
+RX_BUILD = ROOT / "temperature_receiver" / "build-bisect"
+RX_SESSION = ROOT / "experiments" / "prepared_wifi_cache_rx_session"
+IDF_PATH = r"C:\Espressif\frameworks\esp-idf-v6.0.2"
+CCACHE = r"C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64"
+PROGRESS = ROOT / "experiments" / "mac_retry_diag_progress.log"
+RX_LOG = ROOT / "experiments" / "prepared_mac_retry_diag_rx.log"
+TSV = ROOT / "experiments" / "prepared_mac_retry_diag.tsv"
+PORT = "COM7"
+VARIANTS = 7
+HOT_PER = 50
+
+
+def env() -> dict:
+    e = os.environ.copy()
+    e["IDF_PATH"] = IDF_PATH
+    e["IDF_TOOLS_PATH"] = r"C:\Espressif"
+    extra = [
+        CCACHE,
+        r"C:\Espressif\tools\ninja\1.12.1",
+        r"C:\Espressif\tools\cmake\3.30.2\bin",
+        r"C:\msys64\ucrt64\bin",
+    ]
+    e["Path"] = ";".join(extra) + ";" + e.get("Path", "")
+    e.pop("CCACHE_DISABLE", None)
+    return e
+
+
+def log(msg: str) -> None:
+    line = time.strftime("%H:%M:%S") + " " + msg
+    print(line, flush=True)
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(line + "\n")
+
+
+def force_sdk_fixes() -> None:
+    sdk = BUILD / "sdkconfig"
+    if not sdk.exists():
+        return
+    text = sdk.read_text(encoding="utf-8")
+    reps = [
+        ("CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_RTC_CLK_SRC_INT_RC=y", "# CONFIG_RTC_CLK_SRC_INT_RC is not set"),
+        ("# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set", "CONFIG_RTC_CLK_SRC_EXT_CRYS=y"),
+        ("CONFIG_ESP_BROWNOUT_DET=n", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("# CONFIG_ESP_BROWNOUT_DET is not set", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("CONFIG_PM_ENABLE=y", "# CONFIG_PM_ENABLE is not set"),
+    ]
+    for a, b in reps:
+        text = text.replace(a, b)
+    if "CONFIG_RTC_CLK_SRC_EXT_CRYS=y" not in text:
+        text += "\nCONFIG_RTC_CLK_SRC_EXT_CRYS=y\n"
+    sdk.write_text(text, encoding="utf-8")
+
+
+def kill_receiver() -> None:
+    subprocess.run(
+        ["taskkill", "/F", "/IM", "temperature_receiver.exe"],
+        capture_output=True,
+        text=True,
+    )
+    time.sleep(1)
+
+
+def rebuild_receiver() -> None:
+    log("rebuild temperature_receiver")
+    r = subprocess.run(
+        [str(CMAKE), "--build", str(RX_BUILD), "--parallel"],
+        env=env(),
+        capture_output=True,
+        text=True,
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "mac_retry_rx_build.err").write_text(
+            (r.stdout or "")[-8000:] + "\n" + (r.stderr or "")[-8000:],
+            encoding="utf-8",
+        )
+        raise RuntimeError("receiver build failed")
+    log("receiver build ok")
+
+
+def start_receiver() -> None:
+    kill_receiver()
+    RX_SESSION.mkdir(parents=True, exist_ok=True)
+    if TSV.exists():
+        TSV.unlink()
+    env2 = env()
+    env2["AE_RECEIVER_SESSION_DIR"] = str(RX_SESSION)
+    env2["AE_DS_TSV"] = str(TSV)
+    with RX_LOG.open("w", encoding="utf-8") as outf, (
+        ROOT / "experiments" / "prepared_mac_retry_diag_rx.log.err"
+    ).open("w", encoding="utf-8") as errf:
+        subprocess.Popen(
+            [str(RX_EXE)],
+            cwd=str(RX_SESSION),
+            env=env2,
+            stdout=outf,
+            stderr=errf,
+        )
+    # Wait until RECEIVER_UID appears (cloud ready) — no COM.
+    t0 = time.time()
+    while time.time() - t0 < 60:
+        text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+        if "RECEIVER_UID=" in text:
+            log("receiver ready")
+            return
+        time.sleep(1)
+    raise RuntimeError("receiver not ready")
+
+
+def cmake_configure() -> None:
+    args = [
+        str(CMAKE),
+        "-S",
+        str(ROOT),
+        "-B",
+        str(BUILD),
+        "-G",
+        "Ninja",
+        f"-DCPM_aether-client-cpp_SOURCE={AETHER}",
+        "-DAE_EXP_PREPARED_MAC_RETRY_DIAG=1",
+        "-DAE_EXP_PREPARED_TX_DONE_DIAG=",
+        "-DAE_EXP_PREPARED_DEEPSLEEP_5X50=",
+        "-DAE_EXP_PREPARED_WIFI_FASTEST=",
+        "-DAE_EXP_PREPARED_WIFI_BISECT=",
+        "-DAE_EXP_SKIP_DTOR_SAVE=1",
+        "-DSERVICE_UID=5aade50f-00d9-4624-b097-e203cdcf1e38",
+        "-DBENCH_CLIENT_ID=prepared_deepsleep_5x50_v1",
+        "-DAETHER_PREPARED_NONCE_RESERVE=60",
+        "-DWIFI_SSID=chirkov",
+        "-DWIFI_PASSWORD=kcdjepWz51",
+        "-DCMAKE_BUILD_TYPE=Release",
+    ]
+    log("cmake configure mac_retry_diag")
+    r = subprocess.run(args, cwd=ROOT, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "mac_retry_cmake.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("cmake failed")
+    force_sdk_fixes()
+    log("cmake ok")
+
+
+def ninja_build() -> None:
+    log("ninja build")
+    r = subprocess.run(
+        [str(NINJA), "-C", str(BUILD)], env=env(), capture_output=True, text=True
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "mac_retry_build.err").write_text(
+            (r.stdout or "")[-16000:] + "\n" + (r.stderr or "")[-8000:],
+            encoding="utf-8",
+        )
+        raise RuntimeError("ninja failed")
+    log("build ok")
+
+
+def verify_symbol() -> None:
+    """Prefer nm on linked ELF / map for esp_wifi_internal_set_retry_counter."""
+    elf = BUILD / "temperature_sensor.elf"
+    mapf = BUILD / "temperature_sensor.map"
+    found = False
+    detail = ""
+    if mapf.exists():
+        text = mapf.read_text(encoding="utf-8", errors="replace")
+        if "esp_wifi_internal_set_retry_counter" in text:
+            found = True
+            for line in text.splitlines():
+                if "esp_wifi_internal_set_retry_counter" in line:
+                    detail = line.strip()
+                    break
+    if not found and elf.exists():
+        # try llvm-nm / xtensa/riscv nm from IDF
+        for nm in [
+            Path(r"C:\Espressif\tools\riscv32-esp-elf\esp-14.2.0_20241119\riscv32-esp-elf\bin\riscv32-esp-elf-nm.exe"),
+        ]:
+            if not nm.exists():
+                continue
+            r = subprocess.run(
+                [str(nm), str(elf)], capture_output=True, text=True, env=env()
+            )
+            if "esp_wifi_internal_set_retry_counter" in (r.stdout or ""):
+                found = True
+                for line in (r.stdout or "").splitlines():
+                    if "esp_wifi_internal_set_retry_counter" in line:
+                        detail = line.strip()
+                        break
+                break
+    log(f"symbol_resolved={'yes' if found else 'no'} detail={detail[:160]}")
+    if not found:
+        raise RuntimeError("esp_wifi_internal_set_retry_counter not resolved")
+
+
+def wait_com_for_flash_only(timeout_s: float = 120.0) -> None:
+    """Pre-flash only: COM may be absent while ESP deep-sleeps. After FLASH_OK
+    this function must never be called again."""
+    log(f"pre-flash: waiting up to {int(timeout_s)}s for {PORT} (awake window)")
+    t0 = time.time()
+    while time.time() - t0 < timeout_s:
+        r = subprocess.run(
+            [
+                "powershell",
+                "-NoProfile",
+                "-Command",
+                f"Get-PnpDevice -Class Ports -Status OK | Where-Object {{ $_.FriendlyName -match '{PORT}' }} | Select-Object -ExpandProperty FriendlyName",
+            ],
+            capture_output=True,
+            text=True,
+        )
+        if PORT in (r.stdout or ""):
+            log(f"pre-flash: {PORT} present")
+            return
+        time.sleep(2.0)
+    raise RuntimeError(f"{PORT} not available for flash — wake/power-cycle ESP once")
+
+
+def flash_once() -> None:
+    wait_com_for_flash_only()
+    log(f"flash {PORT} (COM allowed only here)")
+    cmd = [
+        str(PY),
+        "-m",
+        "esptool",
+        "--chip",
+        "esp32c6",
+        "-p",
+        PORT,
+        "-b",
+        "460800",
+        "write-flash",
+        "--flash-size",
+        "4MB",
+        "0x0",
+        str(BUILD / "bootloader" / "bootloader.bin"),
+        "0x8000",
+        str(BUILD / "partition_table" / "partition-table.bin"),
+        "0x10000",
+        str(BUILD / "temperature_sensor.bin"),
+    ]
+    r = subprocess.run(cmd, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "mac_retry_flash.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("flash failed")
+    log("FLASH_OK — closing COM; further progress via Aether only")
+
+
+def progress_from_log() -> tuple[int, int, int]:
+    text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+    fulls = len(re.findall(r"^MAC_FULL ", text, re.M))
+    hots = len(re.findall(r"^RETRY ", text, re.M))
+    finals = len(re.findall(r"^MAC_FINAL|BENCH_DONE mac_retry", text, re.M))
+    return fulls, hots, finals
+
+
+def wait_campaign(timeout_s: float = 45 * 60) -> None:
+    log("wait Aether campaign (no COM)")
+    t0 = time.time()
+    last = (-1, -1, -1)
+    while time.time() - t0 < timeout_s:
+        f, h, fin = progress_from_log()
+        if (f, h, fin) != last:
+            last = (f, h, fin)
+            log(f"progress full={f} hot={h} final={fin}")
+            # print last RETRY line
+            text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+            lines = [ln for ln in text.splitlines() if ln.startswith("RETRY ")]
+            if lines:
+                log("  " + lines[-1][:200])
+        if fin > 0 or h >= VARIANTS * HOT_PER:
+            log(f"STOP campaign full={f} hot={h} final={fin}")
+            return
+        # Also stop if we have summaries for all variants via FULL prev=
+        if f >= VARIANTS + 1 and h >= VARIANTS * HOT_PER - 5:
+            log(f"STOP near-complete full={f} hot={h}")
+            return
+        time.sleep(2.0)
+    log(f"TIMEOUT full={last[0]} hot={last[1]} final={last[2]}")
+
+
+def main() -> int:
+    if PROGRESS.exists():
+        PROGRESS.write_text("", encoding="utf-8")
+    rebuild_receiver()
+    start_receiver()
+    cmake_configure()
+    ninja_build()
+    verify_symbol()
+    flash_once()
+    # IMPORTANT: do not touch COM after this point
+    wait_campaign()
+    kill_receiver()
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as e:
+        log(f"ERROR {e}")
+        kill_receiver()
+        sys.exit(1)
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 28ea426..39240b0 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -45,6 +45,12 @@ elseif(AE_EXP_PREPARED_TX_DONE_DIAG)
     "experiment_early_entry.cpp"
     "prepared_send/prepared_send.cpp"
   )
+elseif(AE_EXP_PREPARED_MAC_RETRY_DIAG)
+  list(APPEND src_list
+    "prepared_mac_retry_diag_bench.cpp"
+    "experiment_early_entry.cpp"
+    "prepared_send/prepared_send.cpp"
+  )
 elseif(AE_EXP_PREPARED_WIFI_BISECT)
   list(APPEND src_list
     "prepared_wifi_single_factor_bisect_bench.cpp"
@@ -186,6 +192,7 @@ set(AE_EXP_PREPARED_WIFI_BISECT "" CACHE STRING "Silent single-factor prepared W
 set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING "Silent fastest-path prepared campaign (set to 1)")
 set(AE_EXP_PREPARED_DEEPSLEEP_5X50 "" CACHE STRING "Silent deep-sleep 5x50 prepared E2E (set to 1)")
 set(AE_EXP_PREPARED_TX_DONE_DIAG "" CACHE STRING "Silent TX-done callback diagnostic 1x50 (set to 1)")
+set(AE_EXP_PREPARED_MAC_RETRY_DIAG "" CACHE STRING "Silent MAC retry-limit diagnostic 7x50 (set to 1)")
 set(AE_EXP_TX_DIAG_MODE "" CACHE STRING "TX-done diag mode 0=FIRST_ANY 1=FIRST_SUCCESS")
 set(AE_EXP_FAST_N "" CACHE STRING "Fastest-path prepared count")
 set(AE_EXP_FAST_TEST_ID "" CACHE STRING "Fastest-path test id")
@@ -242,6 +249,7 @@ ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_BISECT)
 ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_FASTEST)
 ae_exp_define_if_set(AE_EXP_PREPARED_DEEPSLEEP_5X50)
 ae_exp_define_if_set(AE_EXP_PREPARED_TX_DONE_DIAG)
+ae_exp_define_if_set(AE_EXP_PREPARED_MAC_RETRY_DIAG)
 ae_exp_define_if_set(AE_EXP_TX_DIAG_MODE)
 ae_exp_define_if_set(AE_EXP_FAST_N)
 ae_exp_define_if_set(AE_EXP_FAST_TEST_ID)
@@ -261,6 +269,7 @@ ae_exp_define_if_set(AE_EXP_BISECT_SMOKE)
    AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1" OR
    AE_EXP_PREPARED_DEEPSLEEP_5X50 STREQUAL "1" OR
    AE_EXP_PREPARED_TX_DONE_DIAG STREQUAL "1" OR
+   AE_EXP_PREPARED_MAC_RETRY_DIAG STREQUAL "1" OR
    (AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1" AND
     NOT AE_EXP_BISECT_CONSOLE STREQUAL "1"))
   target_compile_definitions(aether PUBLIC "AE_EXP_SILENT=1")
diff --git a/main/bench_payload.h b/main/bench_payload.h
index b74f6b0..2c8314b 100644
--- a/main/bench_payload.h
+++ b/main/bench_payload.h
@@ -519,6 +519,117 @@ inline bool DecodeTxDiag(Buffer const& data, TxDiagPayload& out) {
   return out.magic == kTxDiagMagic;
 }
 
+// MAC retry-limit diagnostic payload (experiment only).
+static constexpr std::uint8_t kMacRetryMagic = 0xD7;
+
+enum class MacRetryMsgType : std::uint8_t {
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+};
+
+enum class MacRetryVariant : std::uint8_t {
+  kControl = 0,
+  k0_0 = 1,
+  k1_1 = 2,
+  k2_2 = 3,
+  k3_3 = 4,
+  k1_7 = 5,
+  k7_1 = 6,
+  kCount = 7,
+};
+
+inline char const* MacRetryVariantName(std::uint8_t id) {
+  switch (static_cast(id)) {
+    case MacRetryVariant::kControl:
+      return "CONTROL";
+    case MacRetryVariant::k0_0:
+      return "0/0";
+    case MacRetryVariant::k1_1:
+      return "1/1";
+    case MacRetryVariant::k2_2:
+      return "2/2";
+    case MacRetryVariant::k3_3:
+      return "3/3";
+    case MacRetryVariant::k1_7:
+      return "1/7";
+    case MacRetryVariant::k7_1:
+      return "7/1";
+    default:
+      return "?";
+  }
+}
+
+#pragma pack(push, 1)
+struct MacRetryPayload {
+  std::uint8_t magic{kMacRetryMagic};
+  std::uint8_t type{0};
+  std::uint8_t variant_id{0};
+  std::uint8_t hot_index{0};
+  std::uint16_t sequence_global{0};
+  std::uint16_t record_id{0};
+  std::uint8_t short_retry{0};
+  std::uint8_t long_retry{0};
+  std::uint8_t retry_function_called{0};
+  std::int16_t retry_set_rc{-1};
+  std::uint32_t retry_cfg_us{0};
+  std::uint8_t reset_reason{0};
+  std::uint8_t wake_cause{0};
+  std::uint8_t brownout_count{0};
+  std::uint8_t flags{0};
+  std::uint32_t pending_user_cycle_us{0};
+  std::uint32_t pending_wifi_cycle_us{0};
+  std::uint32_t connect_us{0};
+  std::uint32_t encode_send_us{0};
+  std::uint32_t tx_done_wait_us{0};
+  std::uint32_t teardown_us{0};
+  std::uint8_t tx_cb_total{0};
+  std::uint8_t tx_cb_success{0};
+  std::uint8_t tx_cb_failed{0};
+  std::uint8_t first_status{0xff};
+  std::uint8_t cb_timeout{0};
+  std::uint32_t first_cb_delta_us{0xffffffffu};
+  std::uint32_t first_success_delta_us{0xffffffffu};
+  std::uint32_t first_failed_delta_us{0xffffffffu};
+  std::uint32_t last_cb_delta_us{0xffffffffu};
+  std::int8_t rssi{0};
+  std::uint8_t actual_channel{0};
+  std::uint8_t authmode{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t reconnect_count{0};
+  std::uint16_t prepared_message_left{0};
+  std::uint8_t prev_variant_id{0xff};
+  std::uint8_t prev_hot_send_count{0};
+  std::uint8_t prev_hot_attempt_count{0};
+  std::uint8_t prev_tx_success_count{0};
+  std::uint8_t prev_tx_fail_count{0};
+  std::uint8_t prev_cb_timeout_count{0};
+  std::uint32_t prev_txdone_sum_us{0};
+  std::uint8_t pending_kind{0};
+  std::uint8_t pending_variant{0};
+  std::uint8_t pending_hot_index{0};
+  std::uint8_t pad0{0};
+};
+#pragma pack(pop)
+
+static_assert(sizeof(MacRetryPayload) == 87, "macretry payload size");
+
+template 
+inline Buffer EncodeMacRetry(MacRetryPayload const& p) {
+  Buffer out(sizeof(MacRetryPayload));
+  std::memcpy(out.data(), &p, sizeof(MacRetryPayload));
+  return out;
+}
+
+template 
+inline bool DecodeMacRetry(Buffer const& data, MacRetryPayload& out) {
+  if (data.size() < sizeof(MacRetryPayload)) {
+    return false;
+  }
+  std::memcpy(&out, data.data(), sizeof(MacRetryPayload));
+  return out.magic == kMacRetryMagic;
+}
+
 }  // namespace temp_sensor::bench
 
 #endif  // TEMP_SENSOR_BENCH_PAYLOAD_H_
diff --git a/main/experiment_early_entry.cpp b/main/experiment_early_entry.cpp
index ebc312c..fb8f3c5 100644
--- a/main/experiment_early_entry.cpp
+++ b/main/experiment_early_entry.cpp
@@ -8,7 +8,8 @@
 
 #if defined(ESP_PLATFORM) && \
     (defined(AE_EXP_PREPARED_DEEPSLEEP_5X50) || \
-     defined(AE_EXP_PREPARED_TX_DONE_DIAG))
+     defined(AE_EXP_PREPARED_TX_DONE_DIAG) || \
+     defined(AE_EXP_PREPARED_MAC_RETRY_DIAG))
 
 #  include 
 #  include 
diff --git a/main/experiment_early_entry.h b/main/experiment_early_entry.h
index 3ce8f38..87601a7 100644
--- a/main/experiment_early_entry.h
+++ b/main/experiment_early_entry.h
@@ -20,7 +20,8 @@ struct ExperimentEarlyEntrySnapshot {
 
 #if defined(ESP_PLATFORM) && \
     (defined(AE_EXP_PREPARED_DEEPSLEEP_5X50) || \
-     defined(AE_EXP_PREPARED_TX_DONE_DIAG))
+     defined(AE_EXP_PREPARED_TX_DONE_DIAG) || \
+     defined(AE_EXP_PREPARED_MAC_RETRY_DIAG))
 extern "C" void ExperimentEarlyAppEntry();
 ExperimentEarlyEntrySnapshot const& GetExperimentEarlyEntrySnapshot();
 #else
@@ -30,5 +31,4 @@ inline ExperimentEarlyEntrySnapshot const& GetExperimentEarlyEntrySnapshot() {
   return empty;
 }
 #endif
-
 #endif  // TEMP_SENSOR_EXPERIMENT_EARLY_ENTRY_H_
diff --git a/main/prepared_mac_retry_diag_bench.cpp b/main/prepared_mac_retry_diag_bench.cpp
new file mode 100644
index 0000000..28de7ea
--- /dev/null
+++ b/main/prepared_mac_retry_diag_bench.cpp
@@ -0,0 +1,972 @@
+/*
+ * Copyright 2026 Aethernet Inc.
+ *
+ * Silent MAC retry-limit diagnostic (ESP32-C6).
+ * 7 variants x 50 HOT sendto; FULL between variants; 3 s deep sleep.
+ * FIRST_ANY only. esp_wifi_internal_set_retry_counter per variant (not CONTROL).
+ * Metrics travel in MacRetryPayload 0xD7; UART is silent.
+ */
+
+#include 
+#include 
+#include 
+
+#include "aether/all.h"
+#include "aether/ae_exp_wifi.h"
+#include "aether/config.h"
+#include "aether/env.h"
+#include "bench_payload.h"
+#include "experiment_early_entry.h"
+#include "prepared_send/prepared_send.h"
+
+#if defined(ESP_PLATFORM)
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#endif
+
+using namespace std::chrono_literals;
+
+#if defined(ESP_PLATFORM)
+extern "C" std::uint64_t esp_rtc_get_time_us(void);
+#endif
+
+namespace temp_sensor {
+namespace {
+
+static constexpr auto kParentUid =
+    ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165");
+
+#ifndef BENCH_CLIENT_ID
+#  define BENCH_CLIENT_ID "prepared_deepsleep_5x50_v1"
+#endif
+static constexpr char const* kBenchClientId = BENCH_CLIENT_ID;
+
+#if defined(SERVICE_UID)
+static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID);
+#else
+static constexpr auto kServiceUid =
+    ae::Uid::FromString("5aade50f-00d9-4624-b097-e203cdcf1e38");
+#endif
+
+static constexpr std::uint8_t kVariantCount = 7;
+static constexpr std::uint8_t kHotPerVariant = 50;
+static constexpr std::uint8_t kMaxHotAttempts = 60;
+static constexpr std::uint32_t kSleepUs = 3000000;
+static constexpr std::uint32_t kRtcMagic = 0x4D525431u;  // "MRT1"
+static constexpr std::uint16_t kRtcVersion = 1;
+
+enum class Phase : std::uint16_t {
+  kRegister = 0,
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+  kDone = 4,
+};
+
+struct VariantCfg {
+  bool set_limit;
+  std::uint8_t short_r;
+  std::uint8_t long_r;
+};
+
+static constexpr VariantCfg kVariants[kVariantCount] = {
+    {false, 0, 0},  // CONTROL
+    {true, 0, 0},
+    {true, 1, 1},
+    {true, 2, 2},
+    {true, 3, 3},
+    {true, 1, 7},
+    {true, 7, 1},
+};
+
+struct RtcState {
+  std::uint32_t magic;
+  std::uint16_t version;
+  std::uint16_t phase;
+  std::uint8_t variant_id;
+  std::uint8_t hot_index;
+  std::uint8_t hot_attempt_count;
+  std::uint8_t hot_send_count;
+  std::uint16_t sequence_global;
+  std::uint16_t next_record_id;
+  std::uint32_t requested_sleep_us;
+  std::uint64_t sleep_arm_rtc_us;
+  std::uint8_t pending_valid;
+  std::uint8_t pending_kind;
+  std::uint8_t pending_variant;
+  std::uint8_t pending_hot_index;
+  std::uint32_t pending_user_cycle_us;
+  std::uint32_t pending_wifi_cycle_us;
+  std::uint32_t pending_connect_us;
+  std::uint32_t pending_encode_us;
+  std::uint32_t pending_txdone_us;
+  std::uint32_t pending_teardown_us;
+  std::uint8_t pending_cb_seen;
+  std::uint8_t pending_cb_timeout;
+  std::uint8_t pending_auth;
+  std::uint8_t brownout_count;
+  std::uint8_t unexpected_reset_count;
+  std::uint8_t current_boot_brownout;
+  std::uint8_t registered;
+  std::uint8_t final_fail_count;
+  std::uint8_t var_tx_success;
+  std::uint8_t var_tx_fail;
+  std::uint8_t var_cb_timeout;
+  std::uint8_t pad0;
+  std::uint32_t var_txdone_sum_us;
+  std::uint8_t prev_variant_id;
+  std::uint8_t prev_hot_send_count;
+  std::uint8_t prev_hot_attempt_count;
+  std::uint8_t prev_tx_success_count;
+  std::uint8_t prev_tx_fail_count;
+  std::uint8_t prev_cb_timeout_count;
+  std::uint32_t prev_txdone_sum_us;
+  std::uint32_t crc;
+};
+
+struct PendingDiag {
+  std::uint8_t valid{0};
+  std::uint8_t tx_cb_total{0};
+  std::uint8_t tx_cb_success{0};
+  std::uint8_t tx_cb_failed{0};
+  std::uint8_t first_status{0xff};
+  std::uint8_t cb_timeout{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t reconnect_count{0};
+  std::int8_t rssi{0};
+  std::uint8_t actual_channel{0};
+  std::uint8_t retry_called{0};
+  std::int16_t retry_set_rc{-1};
+  std::uint8_t short_retry{0};
+  std::uint8_t long_retry{0};
+  std::uint32_t retry_cfg_us{0};
+  std::uint32_t first_cb_delta_us{0xffffffffu};
+  std::uint32_t first_success_delta_us{0xffffffffu};
+  std::uint32_t first_failed_delta_us{0xffffffffu};
+  std::uint32_t last_cb_delta_us{0xffffffffu};
+};
+
+#if defined(ESP_PLATFORM)
+RTC_DATA_ATTR static RtcState g_rtc{};
+RTC_DATA_ATTR static prepared_send::PreparedWifiRtcCache g_rtc_wifi_cache{};
+RTC_DATA_ATTR static PendingDiag g_pending_diag{};
+
+static const auto kWifiInit = ae::WiFiInit{
+    std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}},
+    {},
+};
+
+static bool g_had_aether_app = false;
+static std::shared_ptr g_app;
+static ae::Client::ptr g_client;
+static std::unique_ptr g_stream;
+static ae::Subscription g_select_sub;
+static ae::Subscription g_stream_sub;
+static ae::Subscription g_write_sub;
+
+static bool g_write_armed = false;
+static bool g_write_ok = false;
+static bool g_exit_success = false;
+static bool g_pending_register_finish = false;
+static bool g_pending_full_post_write = false;
+static bool g_pending_final_exit = false;
+static bool g_done = false;
+
+static ExperimentEarlyEntrySnapshot g_early{};
+static prepared_send::FastPathConfig g_cfg{};
+static prepared_send::BisectWifiCacheSnapshot g_wifi_snapshot{};
+
+static std::uint32_t Crc32Bytes(void const* data, std::size_t len) {
+  auto const* p = static_cast(data);
+  std::uint32_t crc = 0xffffffffu;
+  for (std::size_t i = 0; i < len; ++i) {
+    crc ^= p[i];
+    for (int b = 0; b < 8; ++b) {
+      std::uint32_t const mask = -(crc & 1u);
+      crc = (crc >> 1) ^ (0xedb88320u & mask);
+    }
+  }
+  return ~crc;
+}
+
+static std::uint32_t ComputeCrc(RtcState const& st) {
+  RtcState tmp = st;
+  tmp.crc = 0;
+  return Crc32Bytes(&tmp, sizeof(tmp));
+}
+
+static void SetCrc(RtcState& st) { st.crc = ComputeCrc(st); }
+
+static bool ValidateRtcState(RtcState const& st) {
+  if (st.magic != kRtcMagic || st.version != kRtcVersion) {
+    return false;
+  }
+  if (ComputeCrc(st) != st.crc) {
+    return false;
+  }
+  if (st.phase > static_cast(Phase::kDone)) {
+    return false;
+  }
+  if (st.variant_id >= kVariantCount &&
+      st.phase != static_cast(Phase::kFinal) &&
+      st.phase != static_cast(Phase::kDone)) {
+    return false;
+  }
+  if (st.hot_index > kHotPerVariant) {
+    return false;
+  }
+  return true;
+}
+
+static void ClearPending(RtcState& st) {
+  st.pending_valid = 0;
+  st.pending_kind = 0;
+  st.pending_variant = 0;
+  st.pending_hot_index = 0;
+  st.pending_user_cycle_us = 0;
+  st.pending_wifi_cycle_us = 0;
+  st.pending_connect_us = 0;
+  st.pending_encode_us = 0;
+  st.pending_txdone_us = 0;
+  st.pending_teardown_us = 0;
+  st.pending_cb_seen = 0;
+  st.pending_cb_timeout = 0;
+  st.pending_auth = 0;
+  g_pending_diag = PendingDiag{};
+}
+
+static void InitRtcFresh(Phase phase) {
+  g_rtc = RtcState{};
+  g_rtc.magic = kRtcMagic;
+  g_rtc.version = kRtcVersion;
+  g_rtc.phase = static_cast(phase);
+  g_rtc.variant_id = 0;
+  g_rtc.hot_index = 1;
+  g_rtc.next_record_id = 1;
+  g_rtc.prev_variant_id = 0xff;
+  ClearPending(g_rtc);
+  SetCrc(g_rtc);
+}
+
+[[noreturn]] static void PrepareRtcStateAndDeepSleep(std::uint32_t requested_us) {
+  g_rtc.requested_sleep_us = requested_us;
+  esp_sleep_enable_timer_wakeup(requested_us);
+  g_rtc.sleep_arm_rtc_us = esp_rtc_get_time_us();
+  SetCrc(g_rtc);
+#  if SOC_PM_SUPPORT_RTC_SLOW_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
+#  endif
+#  if SOC_PM_SUPPORT_RTC_FAST_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
+#  endif
+  (void)esp_deep_sleep_try_to_start();
+  esp_deep_sleep_start();
+  for (;;) {
+  }
+}
+
+static void ForceFullRecovery() {
+  ClearPending(g_rtc);
+  g_rtc.phase = static_cast(Phase::kFull);
+  if (g_rtc.variant_id >= kVariantCount) {
+    g_rtc.variant_id = 0;
+  }
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  g_rtc.var_tx_success = 0;
+  g_rtc.var_tx_fail = 0;
+  g_rtc.var_cb_timeout = 0;
+  g_rtc.var_txdone_sum_us = 0;
+  SetCrc(g_rtc);
+}
+
+static void SnapshotPrevVariant() {
+  g_rtc.prev_variant_id = g_rtc.variant_id;
+  g_rtc.prev_hot_send_count = g_rtc.hot_send_count;
+  g_rtc.prev_hot_attempt_count = g_rtc.hot_attempt_count;
+  g_rtc.prev_tx_success_count = g_rtc.var_tx_success;
+  g_rtc.prev_tx_fail_count = g_rtc.var_tx_fail;
+  g_rtc.prev_cb_timeout_count = g_rtc.var_cb_timeout;
+  g_rtc.prev_txdone_sum_us = g_rtc.var_txdone_sum_us;
+}
+
+static void AdvanceToNextVariantOrFinal() {
+  SnapshotPrevVariant();
+  if (g_rtc.variant_id + 1 < kVariantCount) {
+    ++g_rtc.variant_id;
+    g_rtc.phase = static_cast(Phase::kFull);
+    g_rtc.hot_index = 1;
+    g_rtc.hot_attempt_count = 0;
+    g_rtc.hot_send_count = 0;
+    g_rtc.var_tx_success = 0;
+    g_rtc.var_tx_fail = 0;
+    g_rtc.var_cb_timeout = 0;
+    g_rtc.var_txdone_sum_us = 0;
+  } else {
+    g_rtc.phase = static_cast(Phase::kFinal);
+    g_rtc.hot_index = 1;
+  }
+  SetCrc(g_rtc);
+}
+
+static std::uint16_t NextSeq() {
+  ++g_rtc.sequence_global;
+  return g_rtc.sequence_global;
+}
+
+static void AdvanceRecordIdAfterFlush() {
+  if (g_rtc.next_record_id < 0xffffu) {
+    ++g_rtc.next_record_id;
+  }
+}
+
+static VariantCfg const& CurrentVariant() {
+  auto id = g_rtc.variant_id;
+  if (id >= kVariantCount) {
+    id = 0;
+  }
+  return kVariants[id];
+}
+
+static ae::DataBuffer MakePayload(bench::MacRetryMsgType type) {
+  bench::MacRetryPayload p{};
+  p.type = static_cast(type);
+  p.variant_id = g_rtc.variant_id;
+  p.hot_index = g_rtc.hot_index;
+  p.sequence_global = NextSeq();
+  p.record_id = g_rtc.pending_valid ? g_rtc.next_record_id : 0;
+  auto const& v = CurrentVariant();
+  p.short_retry = v.short_r;
+  p.long_retry = v.long_r;
+  p.retry_function_called = v.set_limit ? 1 : 0;
+  p.reset_reason = g_early.reset_reason;
+  p.wake_cause = g_early.wakeup_cause;
+  p.brownout_count = g_rtc.brownout_count;
+  std::uint8_t flags = 0;
+  if (g_rtc.current_boot_brownout) {
+    flags |= 1;
+  }
+  if (g_rtc.pending_cb_seen) {
+    flags |= 2;
+  }
+  if (g_rtc.pending_cb_timeout) {
+    flags |= 4;
+  }
+  p.flags = flags;
+  p.prev_variant_id = g_rtc.prev_variant_id;
+  p.prev_hot_send_count = g_rtc.prev_hot_send_count;
+  p.prev_hot_attempt_count = g_rtc.prev_hot_attempt_count;
+  p.prev_tx_success_count = g_rtc.prev_tx_success_count;
+  p.prev_tx_fail_count = g_rtc.prev_tx_fail_count;
+  p.prev_cb_timeout_count = g_rtc.prev_cb_timeout_count;
+  p.prev_txdone_sum_us = g_rtc.prev_txdone_sum_us;
+  p.prepared_message_left = static_cast(
+      prepared_send::PreparedMessageLeft() > 0xffffu
+          ? 0xffffu
+          : prepared_send::PreparedMessageLeft());
+
+  if (g_rtc.pending_valid) {
+    p.pending_kind = g_rtc.pending_kind;
+    p.pending_variant = g_rtc.pending_variant;
+    p.pending_hot_index = g_rtc.pending_hot_index;
+    p.pending_user_cycle_us = g_rtc.pending_user_cycle_us;
+    p.pending_wifi_cycle_us = g_rtc.pending_wifi_cycle_us;
+    p.connect_us = g_rtc.pending_connect_us;
+    p.encode_send_us = g_rtc.pending_encode_us;
+    p.tx_done_wait_us = g_rtc.pending_txdone_us;
+    p.teardown_us = g_rtc.pending_teardown_us;
+    p.authmode = g_rtc.pending_auth;
+    if (g_pending_diag.valid) {
+      p.retry_function_called = g_pending_diag.retry_called;
+      p.retry_set_rc = g_pending_diag.retry_set_rc;
+      p.short_retry = g_pending_diag.short_retry;
+      p.long_retry = g_pending_diag.long_retry;
+      p.retry_cfg_us = g_pending_diag.retry_cfg_us;
+      p.tx_cb_total = g_pending_diag.tx_cb_total;
+      p.tx_cb_success = g_pending_diag.tx_cb_success;
+      p.tx_cb_failed = g_pending_diag.tx_cb_failed;
+      p.first_status = g_pending_diag.first_status;
+      p.cb_timeout = g_pending_diag.cb_timeout;
+      p.first_cb_delta_us = g_pending_diag.first_cb_delta_us;
+      p.first_success_delta_us = g_pending_diag.first_success_delta_us;
+      p.first_failed_delta_us = g_pending_diag.first_failed_delta_us;
+      p.last_cb_delta_us = g_pending_diag.last_cb_delta_us;
+      p.rssi = g_pending_diag.rssi;
+      p.actual_channel = g_pending_diag.actual_channel;
+      p.disconnect_count = g_pending_diag.disconnect_count;
+      p.reconnect_count = g_pending_diag.reconnect_count;
+    }
+  }
+  return bench::EncodeMacRetry(p);
+}
+
+static std::uint32_t UserCycleFromAppEntry() {
+  auto const now = esp_timer_get_time();
+  auto const entry = g_early.app_entry_esp_timer_us;
+  if (now < entry) {
+    return 0;
+  }
+  auto const delta = now - entry;
+  return delta > 0xffffffffll ? 0xffffffffu
+                              : static_cast(delta);
+}
+
+static void StorePendingHot(prepared_send::FastSendResult const& result,
+                            std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = 2;
+  g_rtc.pending_variant = g_rtc.variant_id;
+  g_rtc.pending_hot_index = g_rtc.hot_index;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = result.cycle_us;
+  g_rtc.pending_connect_us = result.connect_us;
+  g_rtc.pending_encode_us = result.encode_send_us;
+  g_rtc.pending_txdone_us = result.tx_done_wait_us;
+  g_rtc.pending_teardown_us = result.teardown_us;
+  g_rtc.pending_cb_seen = result.cb_any;
+  g_rtc.pending_cb_timeout = result.cb_timeout;
+  g_rtc.pending_auth = result.negotiated_auth;
+  g_pending_diag = PendingDiag{};
+  g_pending_diag.valid = 1;
+  g_pending_diag.retry_called = result.mac_retry_called;
+  g_pending_diag.retry_set_rc = result.mac_retry_set_rc;
+  g_pending_diag.short_retry = result.mac_short_retry;
+  g_pending_diag.long_retry = result.mac_long_retry;
+  g_pending_diag.retry_cfg_us = result.retry_cfg_us;
+  g_pending_diag.tx_cb_total = result.tx_cb_total;
+  g_pending_diag.tx_cb_success = result.tx_cb_success;
+  g_pending_diag.tx_cb_failed = result.tx_cb_failed;
+  g_pending_diag.first_status = result.first_status;
+  g_pending_diag.cb_timeout = result.cb_timeout;
+  g_pending_diag.first_cb_delta_us = result.first_cb_delta_us;
+  g_pending_diag.first_success_delta_us = result.first_success_delta_us;
+  g_pending_diag.first_failed_delta_us = result.first_failed_delta_us;
+  g_pending_diag.last_cb_delta_us = result.last_cb_delta_us;
+  g_pending_diag.rssi = result.rssi;
+  g_pending_diag.actual_channel = result.actual_channel;
+  g_pending_diag.disconnect_count = result.disconnect_count;
+  g_pending_diag.reconnect_count = result.reconnect_count;
+}
+
+static void StorePendingFull(std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = 1;
+  g_rtc.pending_variant = g_rtc.variant_id;
+  g_rtc.pending_hot_index = 0;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = user_cycle_us;
+  g_pending_diag = PendingDiag{};
+}
+
+static void ReleaseApp() {
+  g_select_sub.Reset();
+  g_stream_sub.Reset();
+  g_write_sub.Reset();
+  g_stream.reset();
+  g_client = {};
+  g_app.reset();
+}
+
+static void PreConstructCleanup() {
+  if (!g_had_aether_app) {
+    return;
+  }
+#  if !AE_WIFI_USE_FULL_DEINIT
+  esp_netif_deinit();
+  esp_event_loop_delete_default();
+#  endif
+}
+
+static void ConstructAether() {
+  PreConstructCleanup();
+  g_had_aether_app = true;
+  g_app = ae::AetherApp::Construct(
+      ae::AetherAppContext{}
+#  if AE_DISTILLATION
+          .AddAdapterFactory([&](ae::AetherAppContext const& ctx) {
+            return ae::WifiAdapter::ptr::Create(
+                ae::CreateWith{ctx.domain()}.with_id(
+                    ae::GlobalId::kWiFiAdapter),
+                ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit);
+          })
+#  endif
+  );
+}
+
+static prepared_send::FastPathConfig MakeFastConfig() {
+  prepared_send::FastPathConfig c{};
+  c.use_bssid = false;
+  c.use_channel = true;
+  c.use_fast_scan = false;
+  c.use_static_ip = true;
+  c.use_static_arp = true;
+  c.ampdu_tx_off = false;
+  c.wifi_storage_ram = false;
+  c.auth = prepared_send::FastAuthMode::kWpa2;
+  c.retry_max = 10;
+  c.pre_delay_ms = 25;
+  c.post_delay_ms = 0;
+  c.post_mode = prepared_send::FastPostMode::kTxDoneCb;
+  c.tx_done_wait = prepared_send::FastTxDoneWaitMode::kFirstAny;
+  auto const& v = CurrentVariant();
+  c.set_mac_retry_limit = v.set_limit;
+  c.mac_short_retry = v.short_r;
+  c.mac_long_retry = v.long_r;
+  return c;
+}
+
+static void DoFullWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto payload = MakePayload(bench::MacRetryMsgType::kFull);
+  auto& wa = g_stream->Write(std::move(payload));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_full_post_write = true;
+  });
+}
+
+static void MaybeFullWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFullWrite();
+}
+
+static void OnFullClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); });
+  MaybeFullWrite();
+}
+
+static void StartRegister() {
+  g_write_armed = false;
+  g_pending_register_finish = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       g_pending_register_finish = true;
+                     });
+}
+
+static void StartFull() {
+  g_write_armed = false;
+  g_write_ok = false;
+  g_pending_full_post_write = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFullClientReady(std::move(res).value());
+                     });
+}
+
+static void StartFinal() {
+  g_write_armed = false;
+  g_write_ok = false;
+  g_pending_final_exit = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       auto client = g_client.Load();
+                       g_stream = std::make_unique(
+                           *g_app, client, kServiceUid, ae::P2pPortHandle{});
+                       g_stream_sub = g_stream->stream_update_event().Subscribe(
+                           []() {
+                             if (!g_stream || g_write_armed) {
+                               return;
+                             }
+                             if (!g_stream->stream_info().is_writable) {
+                               return;
+                             }
+                             g_write_armed = true;
+                             auto& wa = g_stream->Write(
+                                 MakePayload(bench::MacRetryMsgType::kFinal));
+                             g_write_sub = wa.status_event().Subscribe(
+                                 [](ae::WriteAction::Status st) {
+                                   g_write_ok =
+                                       (st == ae::WriteAction::Status::kSuccess);
+                                   g_pending_final_exit = true;
+                                 });
+                           });
+                     });
+}
+
+static void FinishRegisterInLoop() {
+  auto client = g_client.Load();
+  if (!client) {
+    g_app->Exit(1);
+    return;
+  }
+  g_app->aether().Save();
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFullPostWriteInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  bool captured = false;
+  for (int i = 0; i < 10 && !captured; ++i) {
+    captured = prepared_send::CapturePreparedWifiRtcCache(&g_rtc_wifi_cache);
+    if (!captured) {
+      vTaskDelay(pdMS_TO_TICKS(200));
+    }
+  }
+  bool exported = false;
+  for (std::size_t n : {std::size_t{60}, std::size_t{50}, std::size_t{30}}) {
+    if (prepared_send::ExportPreparedSendBlock(g_client, kServiceUid, n)) {
+      exported = true;
+      break;
+    }
+  }
+  if (!exported || !captured || !prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() == 0) {
+    g_app->Exit(1);
+    return;
+  }
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFinalInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void AfterRegisterComplete() {
+  ReleaseApp();
+  g_rtc.registered = 1;
+  g_rtc.phase = static_cast(Phase::kFull);
+  g_rtc.variant_id = 0;
+  g_rtc.hot_index = 1;
+  ClearPending(g_rtc);
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFullComplete() {
+  ReleaseApp();
+  prepared_send::ReleaseFullAetherWifiForHotPath();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  StorePendingFull(UserCycleFromAppEntry());
+  g_rtc.phase = static_cast(Phase::kHot);
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  g_rtc.var_tx_success = 0;
+  g_rtc.var_tx_fail = 0;
+  g_rtc.var_cb_timeout = 0;
+  g_rtc.var_txdone_sum_us = 0;
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalComplete() {
+  ReleaseApp();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  ClearPending(g_rtc);
+  g_rtc.phase = static_cast(Phase::kDone);
+  SetCrc(g_rtc);
+  g_done = true;
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalFailed() {
+  ReleaseApp();
+  if (g_rtc.final_fail_count < 255) {
+    ++g_rtc.final_fail_count;
+  }
+  if (g_rtc.final_fail_count >= 3) {
+    ClearPending(g_rtc);
+    g_rtc.phase = static_cast(Phase::kDone);
+    SetCrc(g_rtc);
+    g_done = true;
+  }
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void RunHotOnce() {
+  if (!prepared_send::PreparedWifiRtcCacheIsValid(g_rtc_wifi_cache) ||
+      !prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() == 0) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (g_rtc.hot_attempt_count >= kMaxHotAttempts) {
+    AdvanceToNextVariantOrFinal();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (g_rtc.hot_attempt_count < 255) {
+    ++g_rtc.hot_attempt_count;
+  }
+  SetCrc(g_rtc);
+
+  g_cfg = MakeFastConfig();
+  g_wifi_snapshot =
+      prepared_send::SnapshotFromPreparedWifiRtcCache(g_rtc_wifi_cache);
+  auto payload = MakePayload(bench::MacRetryMsgType::kHot);
+  auto const result =
+      prepared_send::SendPreparedOnceWithFastPath(g_cfg, payload,
+                                                    &g_wifi_snapshot);
+
+  if (result.status == prepared_send::HotSendStatus::kWifiFailed) {
+    // Assoc failure: do not count as sendto; retry same wake index.
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (result.status != prepared_send::HotSendStatus::kSent) {
+    // Encode/send failure after Wi-Fi: still not a completed sendto count.
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  // sendto completed (fire-and-forget) — never app-retry this packet.
+  auto const user_cycle = UserCycleFromAppEntry();
+  bool const flushed_prior = g_rtc.pending_valid != 0;
+  StorePendingHot(result, user_cycle);
+  if (flushed_prior) {
+    AdvanceRecordIdAfterFlush();
+  }
+
+  if (g_rtc.hot_send_count < 255) {
+    ++g_rtc.hot_send_count;
+  }
+  if (result.first_status == 1) {
+    if (g_rtc.var_tx_success < 255) {
+      ++g_rtc.var_tx_success;
+    }
+  } else if (result.first_status == 0) {
+    if (g_rtc.var_tx_fail < 255) {
+      ++g_rtc.var_tx_fail;
+    }
+  }
+  if (result.cb_timeout) {
+    if (g_rtc.var_cb_timeout < 255) {
+      ++g_rtc.var_cb_timeout;
+    }
+  }
+  g_rtc.var_txdone_sum_us += result.tx_done_wait_us;
+
+  if (g_rtc.hot_index < 255) {
+    ++g_rtc.hot_index;
+  }
+
+  if (g_rtc.hot_send_count >= kHotPerVariant ||
+      g_rtc.hot_attempt_count >= kMaxHotAttempts) {
+    AdvanceToNextVariantOrFinal();
+  }
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void PrepareRtcOnBoot() {
+  g_early = GetExperimentEarlyEntrySnapshot();
+  auto const reset =
+      static_cast(g_early.reset_reason);
+  bool const valid = ValidateRtcState(g_rtc);
+  g_rtc.current_boot_brownout = 0;
+
+  if (reset == ESP_RST_BROWNOUT) {
+    if (valid) {
+      if (g_rtc.brownout_count < 255) {
+        ++g_rtc.brownout_count;
+      }
+      ClearPending(g_rtc);
+    } else {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.brownout_count = 1;
+    }
+    g_rtc.current_boot_brownout = 1;
+    ForceFullRecovery();
+  } else if (!g_early.valid || reset != ESP_RST_DEEPSLEEP || !valid) {
+    bool const first_poweron = (reset == ESP_RST_POWERON);
+    if (first_poweron && (!valid || !g_rtc.registered)) {
+      InitRtcFresh(Phase::kRegister);
+    } else if (!valid) {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.registered = 1;
+      SetCrc(g_rtc);
+    } else {
+      if (g_rtc.unexpected_reset_count < 255) {
+        ++g_rtc.unexpected_reset_count;
+      }
+      ForceFullRecovery();
+    }
+  }
+  SetCrc(g_rtc);
+}
+
+#endif  // ESP_PLATFORM
+
+}  // namespace
+}  // namespace temp_sensor
+
+#if defined(ESP_PLATFORM)
+
+void setup() {
+  using namespace temp_sensor;
+  nvs_flash_init();
+  g_done = false;
+  g_pending_register_finish = false;
+  g_pending_full_post_write = false;
+  g_pending_final_exit = false;
+  PrepareRtcOnBoot();
+  g_cfg = MakeFastConfig();
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kDone) {
+    g_done = true;
+    return;
+  }
+  if (phase == Phase::kRegister) {
+    StartRegister();
+    return;
+  }
+  if (phase == Phase::kFull) {
+    StartFull();
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    StartFinal();
+    return;
+  }
+}
+
+void loop() {
+  using namespace temp_sensor;
+  if (g_done) {
+    vTaskDelay(pdMS_TO_TICKS(1000));
+    return;
+  }
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kHot) {
+    RunHotOnce();
+    return;
+  }
+
+  auto process_deferred = []() {
+    if (g_app && g_pending_register_finish) {
+      g_pending_register_finish = false;
+      FinishRegisterInLoop();
+      return true;
+    }
+    if (g_app && g_pending_full_post_write) {
+      g_pending_full_post_write = false;
+      FinishFullPostWriteInLoop();
+      return true;
+    }
+    if (g_app && g_pending_final_exit) {
+      g_pending_final_exit = false;
+      FinishFinalInLoop();
+      return true;
+    }
+    return false;
+  };
+
+  if (process_deferred()) {
+    return;
+  }
+  if (!g_app) {
+    return;
+  }
+  if (!g_app->IsExited()) {
+    auto t = g_app->Update(ae::Now());
+    if (process_deferred()) {
+      return;
+    }
+    if (!g_app->IsExited()) {
+      g_app->WaitUntil(t);
+    }
+    return;
+  }
+
+  if (phase == Phase::kRegister) {
+    if (g_exit_success) {
+      AfterRegisterComplete();
+    } else {
+      ReleaseApp();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFull) {
+    if (g_exit_success) {
+      AfterFullComplete();
+    } else {
+      ReleaseApp();
+      ForceFullRecovery();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    if (g_exit_success) {
+      AfterFinalComplete();
+    } else {
+      AfterFinalFailed();
+    }
+    return;
+  }
+}
+
+#else
+
+void setup() {}
+void loop() {}
+
+#endif
diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp
index ba4ed32..0c49d31 100644
--- a/main/prepared_send/prepared_send.cpp
+++ b/main/prepared_send/prepared_send.cpp
@@ -790,9 +790,18 @@ HotSendStatus EncodeAndUdpSendTracked(ae::DataBuffer const& payload) {
 // MODE A (kFirstAny): wait first callback regardless of txStatus.
 // MODE B (kFirstSuccess): wait first txStatus==true, then 5 ms observe window.
 // Safety timeout: 100 ms from sendto return. Socket open until unregister.
+#if defined(ESP_PLATFORM)
+extern "C" esp_err_t esp_wifi_internal_set_retry_counter(uint8_t short_retry,
+                                                         uint8_t long_retry);
+#endif
+
+// Prefer: EncodePacket + socket, then optional MAC retry counter, then
+// ResetFastTxDone + tx-done cb + immediate sendto. No Wi-Fi ops between
+// set_retry_counter and sendto. CONTROL leaves set_mac_retry_limit=false.
 HotSendStatus EncodeAndUdpSendWithLateTxDone(ae::DataBuffer const& payload,
                                              FastSendResult* timing,
-                                             FastTxDoneWaitMode wait_mode) {
+                                             FastPathConfig const& cfg) {
+  auto const wait_mode = cfg.tx_done_wait;
   if (!g_prepared_send_message_block.is_valid()) {
     return HotSendStatus::kNoPreparedBlock;
   }
@@ -827,6 +836,26 @@ HotSendStatus EncodeAndUdpSendWithLateTxDone(ae::DataBuffer const& payload,
     return HotSendStatus::kSendFailed;
   }
 
+  if (timing != nullptr) {
+    timing->mac_retry_called = 0;
+    timing->mac_retry_set_rc = -1;
+    timing->mac_short_retry = cfg.mac_short_retry;
+    timing->mac_long_retry = cfg.mac_long_retry;
+    timing->retry_cfg_us = 0;
+  }
+  if (cfg.set_mac_retry_limit) {
+    auto const t_rc0 = esp_timer_get_time();
+    esp_err_t const rc = esp_wifi_internal_set_retry_counter(
+        cfg.mac_short_retry, cfg.mac_long_retry);
+    auto const t_rc1 = esp_timer_get_time();
+    if (timing != nullptr) {
+      timing->mac_retry_called = 1;
+      timing->mac_retry_set_rc = static_cast(rc);
+      auto const d = t_rc1 - t_rc0;
+      timing->retry_cfg_us = d < 0 ? 0 : static_cast(d);
+    }
+  }
+
   ResetFastTxDone();
   g_tx_wait_mode.store(static_cast(wait_mode), std::memory_order_relaxed);
   (void)esp_wifi_set_tx_done_cb(&FastTxDoneCb);
@@ -1753,7 +1782,7 @@ FastSendResult SendPreparedOnceWithFastPath(
   auto const t_post0 = esp_timer_get_time();
   if (cfg.post_mode != FastPostMode::kFixedDelay) {
     encode_status =
-        EncodeAndUdpSendWithLateTxDone(payload, &out, cfg.tx_done_wait);
+        EncodeAndUdpSendWithLateTxDone(payload, &out, cfg);
     std::uint16_t extra_ms = 0;
     if (cfg.post_mode == FastPostMode::kTxDoneCbPlus10) {
       extra_ms = 10;
diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h
index 750e5ba..2619ae5 100644
--- a/main/prepared_send/prepared_send.h
+++ b/main/prepared_send/prepared_send.h
@@ -148,6 +148,11 @@ struct FastPathConfig {
   std::uint16_t post_delay_ms{300};
   FastPostMode post_mode{FastPostMode::kFixedDelay};
   FastTxDoneWaitMode tx_done_wait{FastTxDoneWaitMode::kFirstAny};
+  // Experiment-only: MAC short/long retry via esp_wifi_internal_set_retry_counter.
+  // Association retry_max is independent and must stay unchanged.
+  bool set_mac_retry_limit{false};
+  std::uint8_t mac_short_retry{0};
+  std::uint8_t mac_long_retry{0};
 };
 
 struct FastSendResult {
@@ -181,6 +186,12 @@ struct FastSendResult {
   std::uint8_t disconnect_count{0};
   std::uint8_t last_disconnect_reason{0};
   std::uint8_t reconnect_count{0};
+  // MAC retry-limit diagnostics (experiment).
+  std::int16_t mac_retry_set_rc{-1};  // -1 = not called
+  std::uint8_t mac_short_retry{0};
+  std::uint8_t mac_long_retry{0};
+  std::uint8_t mac_retry_called{0};
+  std::uint32_t retry_cfg_us{0};
 };
 
 // BASE = cached channel + static IPv4/netmask/gw + static ARP. No BSSID.
diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp
index 5418b47..247170f 100644
--- a/temperature_receiver/main.cpp
+++ b/temperature_receiver/main.cpp
@@ -1,8 +1,8 @@
 /*
  * Copyright 2026 Aethernet Inc.
  *
- * Desktop Æther receiver for prepared TX-done diagnostics (TxDiagPayload 0xD6)
- * and deep-sleep E2E (DsPayload 0xD5). Deduplicates by record_id; appends TSV.
+ * Desktop Æther receiver for prepared MAC-retry diagnostics (MacRetryPayload 0xD7),
+ * TX-done (0xD6) and deep-sleep E2E (0xD5). Deduplicates by record_id; appends TSV.
  */
 
 #include 
@@ -65,6 +65,14 @@ struct Meas {
   std::uint8_t reconnect_count{0};
   std::uint8_t ap_primary{0};
   std::uint16_t seq{0};
+  std::uint8_t variant{0};
+  std::uint8_t short_retry{0};
+  std::uint8_t long_retry{0};
+  std::uint8_t retry_called{0};
+  std::int16_t retry_set_rc{-1};
+  std::uint32_t retry_cfg_us{0};
+  std::uint32_t encode_us{0};
+  std::uint8_t actual_channel{0};
 };
 
 std::mutex g_mu;
@@ -85,7 +93,7 @@ std::filesystem::path TsvPath() {
     return std::filesystem::path{env};
   }
 #endif
-  return std::filesystem::path{"prepared_tx_done_diag.tsv"};
+  return std::filesystem::path{"prepared_mac_retry_diag.tsv"};
 }
 
 std::uint32_t PercentileUs(std::vector v, int pct) {
@@ -109,7 +117,9 @@ void EnsureTsvHeader() {
          "tx_cb_success\ttx_cb_failed\tfirst_status\tfirst_cb_delta_us\t"
          "first_success_delta_us\tfirst_failed_delta_us\tlast_cb_delta_us\t"
          "callbacks_after_success\trssi\tdisconnect_count\t"
-         "last_disconnect_reason\treconnect_count\tap_primary\n";
+         "last_disconnect_reason\treconnect_count\tap_primary\t"
+         "variant\tshort_retry\tlong_retry\tretry_called\tretry_set_rc\t"
+         "retry_cfg_us\tencode_us\tactual_channel\n";
 }
 
 void AppendTsv(Meas const& m) {
@@ -135,7 +145,13 @@ void AppendTsv(Meas const& m) {
       << static_cast(m.disconnect_count) << '\t'
       << static_cast(m.last_disconnect_reason) << '\t'
       << static_cast(m.reconnect_count) << '\t'
-      << static_cast(m.ap_primary) << '\n';
+      << static_cast(m.ap_primary) << '\t'
+      << static_cast(m.variant) << '\t'
+      << static_cast(m.short_retry) << '\t'
+      << static_cast(m.long_retry) << '\t'
+      << static_cast(m.retry_called) << '\t'
+      << static_cast(m.retry_set_rc) << '\t' << m.retry_cfg_us << '\t'
+      << m.encode_us << '\t' << static_cast(m.actual_channel) << '\n';
 }
 
 void NoteRecord(Meas m) {
@@ -247,6 +263,97 @@ void PrintFinalStats(char const* tag) {
   std::cout.flush();
 }
 
+void OnMacRetry(temp_sensor::bench::MacRetryPayload const& p) {
+  auto const type = static_cast(p.type);
+  static int hot_by_var[8] = {};
+  if (type == temp_sensor::bench::MacRetryMsgType::kFull) {
+    ++g_full_recv;
+    std::cout << "MAC_FULL seq=" << p.sequence_global
+              << " variant=" << static_cast(p.variant_id)
+              << " name=" << temp_sensor::bench::MacRetryVariantName(p.variant_id)
+              << " prev_v=" << static_cast(p.prev_variant_id)
+              << " prev_sends=" << static_cast(p.prev_hot_send_count)
+              << " prev_tx_ok=" << static_cast(p.prev_tx_success_count)
+              << " prev_tx_fail=" << static_cast(p.prev_tx_fail_count)
+              << "\n";
+  } else if (type == temp_sensor::bench::MacRetryMsgType::kHot) {
+    ++g_hot_recv;
+    auto vid = p.pending_kind == 2 ? p.pending_variant : p.variant_id;
+    if (vid < 8) {
+      ++hot_by_var[vid];
+    }
+    char const* tx = "NA";
+    if (p.first_status == 1) {
+      tx = "OK";
+    } else if (p.first_status == 0) {
+      tx = "FAIL";
+    }
+    char const* cb = "none";
+    if (p.cb_timeout) {
+      cb = "timeout";
+    } else if (p.tx_cb_success) {
+      cb = "success";
+    } else if (p.tx_cb_failed) {
+      cb = "fail";
+    }
+    std::cout << "RETRY V" << static_cast(vid) << " "
+              << (vid < 8 ? hot_by_var[vid] : 0) << "/50"
+              << " s/l=" << static_cast(p.short_retry) << "/"
+              << static_cast(p.long_retry)
+              << " tx=" << tx << " cb=" << cb
+              << " txdone=" << (p.tx_done_wait_us / 1000.0) << "ms"
+              << " wifi=" << (p.pending_wifi_cycle_us / 1000.0) << "ms"
+              << " rssi=" << static_cast(p.rssi)
+              << " rc=" << p.retry_set_rc
+              << " recv=yes\n";
+  } else if (type == temp_sensor::bench::MacRetryMsgType::kFinal) {
+    ++g_final_recv;
+    std::cout << "MAC_FINAL seq=" << p.sequence_global << "\n";
+  }
+
+  Meas m{};
+  m.record_id = p.record_id;
+  m.kind = p.pending_kind;
+  m.outer = p.pending_variant;
+  m.hot = p.pending_hot_index;
+  m.user_us = p.pending_user_cycle_us;
+  m.wifi_us = p.pending_wifi_cycle_us;
+  m.connect_us = p.connect_us;
+  m.txdone_us = p.tx_done_wait_us;
+  m.teardown_us = p.teardown_us;
+  m.encode_us = p.encode_send_us;
+  m.cb_seen = (p.flags & 2) ? 1 : 0;
+  m.cb_timeout = p.cb_timeout;
+  m.brownout = (p.flags & 1) ? 1 : 0;
+  m.auth = p.authmode;
+  m.tx_cb_total = p.tx_cb_total;
+  m.tx_cb_success = p.tx_cb_success;
+  m.tx_cb_failed = p.tx_cb_failed;
+  m.first_status = p.first_status;
+  m.first_cb_delta_us = p.first_cb_delta_us;
+  m.first_success_delta_us = p.first_success_delta_us;
+  m.first_failed_delta_us = p.first_failed_delta_us;
+  m.last_cb_delta_us = p.last_cb_delta_us;
+  m.rssi = p.rssi;
+  m.disconnect_count = p.disconnect_count;
+  m.reconnect_count = p.reconnect_count;
+  m.actual_channel = p.actual_channel;
+  m.ap_primary = p.actual_channel;
+  m.seq = p.sequence_global;
+  m.variant = p.pending_kind == 2 ? p.pending_variant : p.variant_id;
+  m.short_retry = p.short_retry;
+  m.long_retry = p.long_retry;
+  m.retry_called = p.retry_function_called;
+  m.retry_set_rc = p.retry_set_rc;
+  m.retry_cfg_us = p.retry_cfg_us;
+  NoteRecord(m);
+
+  if (type == temp_sensor::bench::MacRetryMsgType::kFinal) {
+    PrintFinalStats("mac_retry");
+  }
+  std::cout.flush();
+}
+
 void OnTxDiag(temp_sensor::bench::TxDiagPayload const& p) {
   auto const type = static_cast(p.type);
   if (type == temp_sensor::bench::TxDiagMsgType::kFull) {
@@ -387,6 +494,11 @@ void OnDs(temp_sensor::bench::DsPayload const& p) {
 
 void OnMessage(ae::Uid, ae::DataBuffer const& data) {
   std::lock_guard lock{g_mu};
+  temp_sensor::bench::MacRetryPayload mr{};
+  if (temp_sensor::bench::DecodeMacRetry(data, mr)) {
+    OnMacRetry(mr);
+    return;
+  }
   temp_sensor::bench::TxDiagPayload td{};
   if (temp_sensor::bench::DecodeTxDiag(data, td)) {
     OnTxDiag(td);

From 451f52695c9b1aa7775113c86a11597bc006f6a0 Mon Sep 17 00:00:00 2001
From: aethernet-io 
Date: Sat, 29 Aug 2026 17:11:57 -0700
Subject: [PATCH 31/32] Add boot/Wi-Fi HOT path optimization campaign and
 VAL100.

Measure D/G/H/E runtime knobs (30 HOT each) plus combined winners validation without changing TX power or MAC retry policy.

Co-authored-by: Cursor 
---
 CMakeLists.txt                                |   16 +
 experiments/PREPARED_BOOT_WIFI_OPT_REPORT.md  |  110 ++
 experiments/analyze_boot_wifi_opt.py          |  164 +++
 .../boot_wifi_opt_sdkconfig_snapshot.txt      |   15 +
 experiments/prepared_boot_wifi_opt.tsv        |  309 +++++
 .../prepared_boot_wifi_opt_summary.tsv        |   12 +
 experiments/prepared_boot_wifi_val100.tsv     |   93 ++
 experiments/run_boot_wifi_opt.py              |  323 ++++++
 experiments/run_boot_wifi_val100.py           |  269 +++++
 main/CMakeLists.txt                           |   18 +
 main/bench_payload.h                          |  129 +++
 main/experiment_early_entry.cpp               |    4 +-
 main/experiment_early_entry.h                 |    4 +-
 main/prepared_boot_wifi_opt_bench.cpp         | 1013 +++++++++++++++++
 main/prepared_boot_wifi_val100_bench.cpp      |  946 +++++++++++++++
 main/prepared_send/prepared_send.cpp          |   44 +
 main/prepared_send/prepared_send.h            |   22 +-
 temperature_receiver/main.cpp                 |   94 +-
 18 files changed, 3574 insertions(+), 11 deletions(-)
 create mode 100644 experiments/PREPARED_BOOT_WIFI_OPT_REPORT.md
 create mode 100644 experiments/analyze_boot_wifi_opt.py
 create mode 100644 experiments/boot_wifi_opt_sdkconfig_snapshot.txt
 create mode 100644 experiments/prepared_boot_wifi_opt.tsv
 create mode 100644 experiments/prepared_boot_wifi_opt_summary.tsv
 create mode 100644 experiments/prepared_boot_wifi_val100.tsv
 create mode 100644 experiments/run_boot_wifi_opt.py
 create mode 100644 experiments/run_boot_wifi_val100.py
 create mode 100644 main/prepared_boot_wifi_opt_bench.cpp
 create mode 100644 main/prepared_boot_wifi_val100_bench.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 627972e..7e9d23a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -41,6 +41,10 @@ set(AE_EXP_PREPARED_TX_DONE_DIAG "" CACHE STRING
     "Silent TX-done callback diagnostic 1x50 (set to 1)")
 set(AE_EXP_PREPARED_MAC_RETRY_DIAG "" CACHE STRING
     "Silent MAC retry-limit diagnostic 7x50 (set to 1)")
+set(AE_EXP_PREPARED_BOOT_WIFI_OPT "" CACHE STRING
+    "Silent boot/wifi HOT opt campaign 11x30 (set to 1)")
+set(AE_EXP_PREPARED_BOOT_WIFI_VAL100 "" CACHE STRING
+    "Silent VAL100 combined boot/wifi winners (set to 1)")
 set(AE_EXP_TX_DIAG_MODE "" CACHE STRING
     "TX-done diag wait mode: 0=FIRST_ANY 1=FIRST_SUCCESS")
 set(AE_EXP_FAST_DISABLE_WPA3 "" CACHE STRING
@@ -71,6 +75,18 @@ elseif(AE_EXP_PREPARED_MAC_RETRY_DIAG STREQUAL "1")
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
+elseif(AE_EXP_PREPARED_BOOT_WIFI_OPT STREQUAL "1")
+  list(APPEND SDKCONFIG_DEFAULTS
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
+elseif(AE_EXP_PREPARED_BOOT_WIFI_VAL100 STREQUAL "1")
+  list(APPEND SDKCONFIG_DEFAULTS
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
 elseif(AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1")
   list(APPEND SDKCONFIG_DEFAULTS
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
diff --git a/experiments/PREPARED_BOOT_WIFI_OPT_REPORT.md b/experiments/PREPARED_BOOT_WIFI_OPT_REPORT.md
new file mode 100644
index 0000000..ff7cb81
--- /dev/null
+++ b/experiments/PREPARED_BOOT_WIFI_OPT_REPORT.md
@@ -0,0 +1,110 @@
+# Prepared Boot / Wi-Fi HOT Optimization Report
+
+ESP32-C6 · ESP-IDF v6.0.2 · branch `thermometer-prepared-send-v0`  
+aether-client-cpp **unchanged** `157aadbec8e7b852d0f89274307ff7cb8103e5f7`
+
+## Campaign
+
+- One autonomous flash, RTC state machine, **30 HOT / variant** (not 50).
+- Telemetry only via Æther (`BootWifiOptPayload` `0xD8`); no COM after flash.
+- Baseline locked: WPA2, cached channel, no BSSID, static IP/ARP, Wi-Fi 4, auto PHY,
+  `WIFI_PS_NONE`, default TX power, PRE=25 ms, late TX-done, no MAC retry setter,
+  deep sleep between sends.
+- Runtime matrix: **D0–D3, G1, H1–H2, E1–E4** (11 variants).
+- Stopped at **E4 ≈27/30** (COM stuck awake); **298 HOT** records analyzed. FINAL not received.
+
+## Effective compile-time config (this flash)
+
+| Item | Value | Notes |
+|------|-------|-------|
+| External RTC crystal | **yes** `CONFIG_RTC_CLK_SRC_EXT_CRYS=y` | Already enabled |
+| `RTC_CLK_CAL_CYCLES` | **1024** | Defaults file asks for `0`; forced `0` on first attempt left COM awake with zero telemetry — **reverted** |
+| Boot validation | `SKIP_VALIDATE_IN_DEEP_SLEEP=y` (+ `ON_POWER_ON=y`) | A1 already; A0 not a separate flash |
+| Bootloader opt | **SIZE** (not PERF) | A2 skipped (one-flash) |
+| Flash | **DIO 80 MHz** | QIO not forced (board/flash not verified for QIO) |
+| Secure Boot | **disabled** | |
+| Flash encryption | **disabled** | |
+| AMPDU TX/RX | **already off** in sdkconfig | F1–F6 skipped as identical to control |
+
+### A-matrix (one-flash limitation)
+
+| Variant | Status |
+|---------|--------|
+| A0 CONTROL (validate on deep-sleep wake) | Not flashed separately |
+| A1 SKIP_VALIDATE_IN_DEEP_SLEEP | **Effective** |
+| A2 + PERF | Not applied |
+| A3 QIO 80 | Not applied (DIO 80 kept) |
+| A4 CAL_CYCLES=0 | **Attempted → hang; kept 1024** |
+| A5 SKIP_VALIDATE_ALWAYS | Not applied (bench-only; Secure Boot off) |
+
+**Security note (A5):** `SKIP_VALIDATE_ALWAYS` disables image validation on every boot — incompatible with Secure Boot / anti-rollback / flash-encryption production posture. Effective here: Secure Boot off, flash encryption off.
+
+### B — External crystal
+
+Enabled. Wake overhead median ≈ **40.7 ms** on all variants (compile-time, not runtime).  
+3 s sleep drift / hardware deep-sleep current: not measured in software (scope separately).
+
+### C — PHY cal RTC reuse
+
+**`RTC_PHY_CAL_REUSE_NOT_SUPPORTED_BY_PUBLIC_API`**
+
+IDF 6.0.2 exposes `esp_phy_load_cal_data_from_nvs` / `esp_phy_store_cal_data_to_nvs` and deep-sleep path uses `PHY_RF_CAL_NONE`, but there is **no public API** to inject an RTC-held `esp_phy_calibration_data_t` blob into PHY init. No opaque driver memcpy. Measured path = stock IDF deep-sleep no-calibration + NVS cal storage left enabled.
+
+## Main results (median µs)
+
+See also `experiments/prepared_boot_wifi_opt_summary.tsv` and raw `experiments/prepared_boot_wifi_opt.tsv`.
+
+| variant | setting | delivery/30 | wake_ov_med | wifi_init_med | connect_med | txdone_med | hot_user_med | p90 | max | heap_delta | notes |
+|---------|---------|-------------|-------------|---------------|-------------|------------|--------------|-----|-----|------------|-------|
+| D0_CONTROL | baseline | 29/30 | 40732 | 16447 | 143814 | 6490 | 250269 | 320267 | 410269 | 46584 | n=29 |
+| D1_STORAGE_RAM | WIFI_STORAGE_RAM | 27/30 | 40736 | 16404 | 138933 | 5606 | **240270** | 310257 | 520259 | 46584 | n=27 |
+| D2_NVS_OFF | nvs_enable=0 | 25/30 | 40770 | **10623** | 233508 | 7721 | 340259 | 380256 | 470264 | 46480 | worse connect |
+| D3_RAM_NVS_OFF | RAM+nvs_off | 25/30 | 40772 | 10618 | 238533 | 6435 | 370256 | 420256 | 440249 | 46480 | worse |
+| G1_HT20 | force HT20 | 30/30 | 40752 | 16360 | 134263 | 6306 | 250271 | 320269 | 390269 | 46584 | connect↓ |
+| H1_CS_OFF | dynamic_cs=false | 26/30 | 40767 | 16441 | 138312 | 5037 | 250264 | 310271 | 380266 | 46584 | ≈D0 |
+| H2_CS_ON | dynamic_cs=true | 26/30 | 40776 | 16357 | 141900 | 5552 | 270274 | 320272 | 820265 | 46584 | worse tails |
+| E1_TX_HALF | dyn_tx=16 | 30/30 | 40772 | 16433 | 133819 | 3138 | 260266 | 310269 | 670262 | 46584 | time≈ |
+| E2_TX_MIN | dyn_tx=8 | 25/30 | 40766 | 16366 | 136230 | 4665 | 250269 | 310269 | 340269 | 46584 | time≈ |
+| E3_RX_HALF | rx 5/16 | 29/30 | 40768 | 16366 | **132388** | 5142 | 250269 | 310274 | 1040297 | **37984** | RAM↓ |
+| E4_RX_MIN | rx 3/8 | 26/30 | 40780 | 16272 | 146044 | 5960 | 270269 | 420267 | 770272 | **34544** | best RAM |
+
+## Winners
+
+| Category | Winner | Evidence |
+|----------|--------|----------|
+| **BOOT** | A1 + EXT_CRYS (wake_ov ≈40.7 ms) | Flat across runtime variants; A4=0 unsafe here |
+| **WIFI / hot_user** | **D1_STORAGE_RAM** | −10 ms median vs D0 |
+| **RAM** | **E4_RX_MIN** (then E3) | heap_delta 34544 / 37984 vs 46584 |
+| **PHY CAL** | N/A public RTC inject | stock IDF no-cal on deep-sleep wake |
+| **EXTERNAL XTAL** | enabled | CAL_CYCLES effective **1024** |
+| **Wi-Fi storage** | **D1 RAM** | D2/D3 hurt connect ≫ init win |
+| **NVS enable** | keep **enabled** | nvs_off regresses hot cycle |
+| **AMPDU/A-MSDU** | already off | no new variants |
+| **HT20** | neutral time, slightly faster connect | include in combined |
+| **dynamic CS** | **false** (H1) ≈ control; true worse tails | include false |
+| **buffers** | E3/E4 save RAM; little time win | E3 in combined |
+
+### Combined BEST candidate → VAL100
+
+`WIFI_STORAGE_RAM` + `force HT20` + `dynamic_cs=false` + RX half (`static_rx=5`, `dynamic_rx=16`) + `nvs_enable=1`.
+
+TX power / MAC retry policy unchanged.
+
+## VAL100
+
+Combined: `WIFI_STORAGE_RAM` + HT20 + `dynamic_cs=false` + RX half (5/16) + `nvs_enable=1`.
+
+| metric | value |
+|--------|-------|
+| delivery | **91/100** received (txok=91); FINAL received |
+| wake_overhead_med | **40710 µs** |
+| wifi_init_med | 16387 µs |
+| connect_med | **131618 µs** |
+| txdone_med | 11621 µs |
+| hot_user_med | **250269 µs** (p90 320268, max 470270) |
+| heap_delta_med | **37984** (E3-class RAM) |
+| brownouts | **0** |
+
+Notes: first post-flash wake needed a manual board reset (COM stuck awake until reset), same as the main campaign. Combined config matches D0 on hot_user median; **D1 alone** remains the best single time win (−10 ms). Combined keeps E3 RAM savings and faster connect vs D0.
+
+Raw: `experiments/prepared_boot_wifi_val100.tsv`
diff --git a/experiments/analyze_boot_wifi_opt.py b/experiments/analyze_boot_wifi_opt.py
new file mode 100644
index 0000000..17907fb
--- /dev/null
+++ b/experiments/analyze_boot_wifi_opt.py
@@ -0,0 +1,164 @@
+import csv
+from pathlib import Path
+from collections import defaultdict
+
+NAMES = {
+    0: "D0_CONTROL",
+    1: "D1_STORAGE_RAM",
+    2: "D2_NVS_OFF",
+    3: "D3_RAM_NVS_OFF",
+    4: "G1_HT20",
+    5: "H1_CS_OFF",
+    6: "H2_CS_ON",
+    7: "E1_TX_HALF",
+    8: "E2_TX_MIN",
+    9: "E3_RX_HALF",
+    10: "E4_RX_MIN",
+}
+SETTINGS = {
+    0: "baseline",
+    1: "WIFI_STORAGE_RAM",
+    2: "nvs_enable=0",
+    3: "RAM+nvs_off",
+    4: "force HT20",
+    5: "dynamic_cs=false",
+    6: "dynamic_cs=true",
+    7: "dyn_tx=16",
+    8: "dyn_tx=8",
+    9: "rx 5/16",
+    10: "rx 3/8",
+}
+
+p = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared\experiments\prepared_boot_wifi_opt.tsv")
+rows = list(csv.DictReader(p.open(encoding="utf-8"), delimiter="\t"))
+
+
+def iu(x):
+    try:
+        return int(x)
+    except Exception:
+        return 0
+
+
+hots = [r for r in rows if iu(r["kind"]) == 2]
+by = defaultdict(list)
+for r in hots:
+    by[iu(r["variant"])].append(r)
+
+
+def med(xs):
+    xs = sorted(xs)
+    if not xs:
+        return 0
+    return xs[len(xs) // 2]
+
+
+def p90(xs):
+    xs = sorted(xs)
+    if not xs:
+        return 0
+    return xs[int((len(xs) - 1) * 0.9)]
+
+
+lines = []
+hdr = (
+    "variant\tsetting\tdelivery/30\twake_overhead_med\twifi_init_med\t"
+    "connect_med\ttxdone_med\thot_user_med\tp90\tmax\theap_delta\tnotes"
+)
+lines.append(hdr)
+print(hdr)
+results = []
+for vid in range(11):
+    rs = by.get(vid, [])
+    n = len(rs)
+    ok = sum(1 for r in rs if iu(r["first_status"]) == 1)
+    wake = [iu(r["sleep_overhead_us"]) for r in rs if iu(r["sleep_overhead_us"]) > 0]
+    init = [iu(r["wifi_init_us"]) for r in rs if iu(r["wifi_init_us"]) > 0]
+    conn = [iu(r["connect_us"]) for r in rs if iu(r["connect_us"]) > 0]
+    tx = [iu(r["txdone_us"]) for r in rs]
+    user = [
+        iu(r["user_us"])
+        for r in rs
+        if iu(r["user_us"]) > 0 and iu(r["user_us"]) < 2000000
+    ]
+    heap = [
+        iu(r["heap_before"]) - iu(r["heap_after"])
+        for r in rs
+        if iu(r["heap_before"]) > 0
+    ]
+    bo = sum(1 for r in rs if iu(r["brownout"]))
+    notes = []
+    if bo:
+        notes.append(f"brownout={bo}")
+    if n < 30:
+        notes.append(f"n={n}")
+    notes.append(f"txok={ok}")
+    line = (
+        f"{NAMES[vid]}\t{SETTINGS[vid]}\t{n}/30\t{med(wake)}\t{med(init)}\t"
+        f"{med(conn)}\t{med(tx)}\t{med(user)}\t{p90(user)}\t"
+        f"{(max(user) if user else 0)}\t{med(heap)}\t{';'.join(notes) or '-'}"
+    )
+    lines.append(line)
+    print(line)
+    results.append(
+        dict(
+            vid=vid,
+            name=NAMES[vid],
+            setting=SETTINGS[vid],
+            n=n,
+            wake=med(wake),
+            init=med(init),
+            conn=med(conn),
+            tx=med(tx),
+            user=med(user),
+            p90=p90(user),
+            mx=max(user) if user else 0,
+            heap=med(heap),
+            ok=ok,
+        )
+    )
+
+by_user = sorted(results, key=lambda d: (d["user"] if d["user"] else 9e9, d["conn"]))
+by_wake = sorted(results, key=lambda d: d["wake"] if d["wake"] else 9e9)
+by_ram = sorted(results, key=lambda d: d["heap"] if d["heap"] else 9e9)
+by_conn = sorted(results, key=lambda d: d["conn"] if d["conn"] else 9e9)
+
+print("\n=== WINNERS ===")
+print("hot_user:", by_user[0]["name"], by_user[0]["user"], "us")
+print("wake_ov:", by_wake[0]["name"], by_wake[0]["wake"], "us")
+print("connect:", by_conn[0]["name"], by_conn[0]["conn"], "us")
+print("RAM(lowest heap_delta):", by_ram[0]["name"], by_ram[0]["heap"])
+
+out = Path(
+    r"C:\Users\nickc\Projects\temperature-sensor-prepared\experiments\prepared_boot_wifi_opt_summary.tsv"
+)
+out.write_text("\n".join(lines) + "\n", encoding="utf-8")
+print("wrote", out)
+
+# section deltas vs D0
+d0 = results[0]
+print("\nvs D0_CONTROL:")
+for d in results:
+    du = d["user"] - d0["user"]
+    dc = d["conn"] - d0["conn"]
+    print(
+        f"  {d['name']}: user {d['user']} ({du:+d}) conn {d['conn']} ({dc:+d}) "
+        f"init {d['init']} wake {d['wake']} heap_delta {d['heap']} n={d['n']}"
+    )
+
+# recommend combined
+cands = [d for d in results if d["n"] >= 24]
+best = min(cands, key=lambda d: d["user"])
+d_best = min(results[0:4], key=lambda d: d["user"] if d["user"] else 9e9)
+g = results[4]
+h_best = min(results[5:7], key=lambda d: d["user"] if d["user"] else 9e9)
+e_best = min(results[7:11], key=lambda d: d["user"] if d["user"] else 9e9)
+e_ram = min(results[7:11], key=lambda d: d["heap"] if d["heap"] else 9e9)
+
+print("\nsection picks:")
+print(" D:", d_best["name"], d_best["user"])
+print(" G:", g["name"], g["user"], f"vsD0 {g['user']-d0['user']:+d}")
+print(" H:", h_best["name"], h_best["user"])
+print(" E time:", e_best["name"], e_best["user"])
+print(" E ram:", e_ram["name"], e_ram["heap"])
+print(" overall time:", best["name"], best["user"])
diff --git a/experiments/boot_wifi_opt_sdkconfig_snapshot.txt b/experiments/boot_wifi_opt_sdkconfig_snapshot.txt
new file mode 100644
index 0000000..93f7bd3
--- /dev/null
+++ b/experiments/boot_wifi_opt_sdkconfig_snapshot.txt
@@ -0,0 +1,15 @@
+CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y
+# CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS is not set
+# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF is not set
+CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y
+# CONFIG_ESPTOOLPY_FLASHMODE_QIO is not set
+CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
+CONFIG_RTC_CLK_SRC_EXT_CRYS=y
+CONFIG_RTC_CLK_CAL_CYCLES=1024
+CONFIG_SECURE_BOOT_V2_RSA_SUPPORTED=y
+# CONFIG_FLASH_ENCRYPTION_ENABLED is not set
+# CONFIG_ESP_WIFI_AMPDU_TX_ENABLED is not set
+# CONFIG_ESP_WIFI_AMPDU_RX_ENABLED is not set
+CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10
+CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
+CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32
diff --git a/experiments/prepared_boot_wifi_opt.tsv b/experiments/prepared_boot_wifi_opt.tsv
new file mode 100644
index 0000000..642a6d8
--- /dev/null
+++ b/experiments/prepared_boot_wifi_opt.tsv
@@ -0,0 +1,309 @@
+record_id	kind	outer	hot	user_us	wifi_us	connect_us	txdone_us	teardown_us	sleep_elapsed_us	sleep_overhead_us	app_entry_us	cb_seen	cb_timeout	brownout	auth	seq	diag_mode	tx_cb_total	tx_cb_success	tx_cb_failed	first_status	first_cb_delta_us	first_success_delta_us	first_failed_delta_us	last_cb_delta_us	callbacks_after_success	rssi	disconnect_count	last_disconnect_reason	reconnect_count	ap_primary	variant	short_retry	long_retry	retry_called	retry_set_rc	retry_cfg_us	encode_us	actual_channel	wifi_init_us	heap_before	heap_after
+1	1	0	0	3199803	3199803	0	0	0	3040755	40755	5429	0	0	0	0	2	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	0	0	0	0	-1	0	0	0	0	0	0
+2	2	0	1	250258	238699	126876	17745	78841	3040736	40736	5429	1	0	0	3	3	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3242	9	16316	348292	301708
+3	2	0	2	410269	398711	235548	6490	140250	3040768	40768	5429	1	0	0	3	4	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3197	9	16317	348292	301708
+4	2	0	3	370267	358880	168325	83558	83244	3040750	40750	5429	1	0	0	3	5	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3133	9	16488	348292	301708
+5	2	0	4	260266	248876	159408	5041	61521	3040723	40723	5429	1	0	0	3	6	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3377	9	16485	348292	301708
+6	2	0	5	240270	228877	145899	1035	65775	3040681	40681	5429	1	0	0	3	7	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3133	9	16482	348292	301708
+7	2	0	6	250269	238874	139106	10128	66180	3040745	40745	5429	1	0	0	3	8	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3132	9	16479	348292	301708
+8	2	0	7	230260	218862	143814	3106	53698	3040743	40743	5429	1	0	0	3	9	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3131	9	16476	348292	301708
+9	2	0	8	270269	258868	158381	11660	65148	3040734	40734	5429	1	0	0	3	10	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3136	9	16473	348292	301708
+10	2	0	9	270266	258862	138886	10933	85637	3040707	40707	5429	1	0	0	3	11	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3376	9	16470	348292	301708
+11	2	0	10	240264	228857	126591	1350	85423	3040744	40744	5429	1	0	0	3	12	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3066	9	16468	348292	301708
+12	2	0	11	260269	248859	131454	1542	95268	3040723	40723	5429	1	0	0	3	13	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3133	9	16464	348292	301708
+13	2	0	12	210269	198858	118934	2716	54098	3040737	40737	5429	1	0	0	3	14	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-31	0	0	0	9	0	0	0	0	-1	0	3129	9	16462	348292	301708
+14	2	0	13	240267	228851	113723	14138	82708	3040711	40711	5429	1	0	0	3	15	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-31	0	0	0	9	0	0	0	0	-1	0	3089	9	16459	348292	301708
+15	2	0	14	260269	248851	154471	13563	62872	3040744	40744	5429	1	0	0	3	16	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3135	9	16456	348292	301708
+16	2	0	15	240275	228853	142033	8551	58264	3040724	40724	5429	1	0	0	3	17	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3133	9	16453	348292	301708
+17	2	0	16	270254	258830	144713	11088	85482	3040698	40698	5429	1	0	0	3	18	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	0	0	0	0	-1	0	3242	9	16450	348292	301708
+18	2	0	17	250269	238842	148752	1305	65550	3040711	40711	5429	1	0	0	3	19	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-31	0	0	0	9	0	0	0	0	-1	0	3082	9	16447	348292	301708
+19	2	0	18	230269	218839	120448	6219	70369	3040703	40703	5429	1	0	0	3	20	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3344	9	16444	348292	301708
+20	2	0	19	230280	218847	130416	3970	61509	3040702	40702	5429	1	0	0	3	21	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-31	0	0	0	9	0	0	0	0	-1	0	4482	9	16441	348292	301708
+21	2	0	20	240269	228833	133430	4309	72543	3040732	40732	5429	1	0	0	3	22	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3083	9	16438	348292	301708
+22	2	0	21	240259	228821	127125	10891	75953	3040732	40732	5429	1	0	0	3	23	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3084	9	16435	348292	301708
+23	2	0	22	360272	348830	269128	672	56097	3040758	40758	5429	1	0	0	3	24	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	0	0	0	0	-1	0	3178	9	16433	348292	301708
+24	2	0	23	270266	258822	164195	6202	70540	3040740	40740	5429	1	0	0	3	25	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	0	0	0	0	-1	0	3199	9	16430	348292	301708
+25	2	0	24	260263	248815	130362	11340	84116	3040694	40694	5429	1	0	0	3	26	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	4482	9	16427	348292	301708
+26	2	0	25	320267	308816	174266	40946	75896	3040730	40730	5429	1	0	0	3	27	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3082	9	16424	348292	301708
+27	2	0	26	270262	258808	146305	13090	83484	3040682	40682	5429	1	0	0	3	28	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3246	9	16421	348292	301708
+29	2	0	28	300261	288802	207923	163	66677	3040732	40732	5429	1	0	0	3	30	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	0	0	0	0	-1	0	3087	9	16415	348292	301708
+30	2	0	29	250269	238807	123502	9911	86937	3040745	40745	5429	1	0	0	3	31	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	0	0	0	0	-1	0	3087	9	16412	348292	301708
+31	2	0	30	250269	238804	147884	289	76559	3040745	40745	5429	1	0	0	3	32	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	0	0	0	0	-1	0	3087	9	16409	348292	301708
+32	1	1	0	3169808	3169808	147884	289	76559	3040745	40745	5429	1	0	0	3	33	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	1	0	0	0	-1	0	3087	0	16409	348292	301708
+33	2	1	1	390257	378782	270126	6402	80435	3040754	40754	5429	1	0	0	3	34	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3081	9	16399	348292	301708
+34	2	1	2	230260	218789	119133	5838	71011	3040712	40712	5429	1	0	0	3	35	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3083	9	16404	348292	301708
+35	2	1	3	270272	258801	169956	10075	56665	3040704	40704	5429	1	0	0	3	36	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3092	9	16404	348292	301708
+36	2	1	4	190257	178786	112618	1426	45418	3040705	40705	5429	1	0	0	3	37	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	1	0	0	0	-1	0	3080	9	16404	348292	301708
+37	2	1	5	320267	308797	209066	5356	71386	3040752	40752	5429	1	0	0	3	38	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	1	0	0	0	-1	0	3193	9	16404	348292	301708
+38	2	1	6	240270	228800	138933	5606	61204	3040720	40720	5429	1	0	0	3	39	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3128	9	16404	348292	301708
+39	2	1	7	220261	208791	110319	4643	72206	3040661	40661	5429	1	0	0	3	40	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	1	0	0	0	-1	0	3080	9	16404	348292	301708
+40	2	1	8	240264	228793	122601	12985	73868	3040735	40735	5429	1	0	0	3	41	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3083	9	16404	348292	301708
+41	2	1	9	260267	248797	170337	814	55929	3040743	40743	5429	1	0	0	3	42	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3195	9	16404	348292	301708
+42	2	1	10	250264	238793	147884	157	76697	3040738	40738	5429	1	0	0	3	43	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3082	9	16404	348292	301708
+43	2	1	11	240264	228793	146969	157	66702	3040749	40749	5429	1	0	0	3	44	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3075	9	16404	348292	301708
+44	2	1	12	270261	258790	152380	158	86541	3040731	40731	5429	1	0	0	3	45	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3081	9	16404	348292	301708
+45	2	1	13	240265	228794	132774	36522	40060	3040727	40727	5429	1	0	0	3	46	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3091	9	16404	348292	301708
+46	2	1	14	240250	228779	130075	157	76684	3040720	40720	5429	1	0	0	3	47	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3081	9	16404	348292	301708
+47	2	1	15	210263	198793	116964	8542	58315	3040742	40742	5429	1	0	0	3	48	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3079	9	16404	348292	301708
+48	2	1	16	280260	268789	172977	2829	74022	3040736	40736	5429	1	0	0	3	49	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3081	9	16404	348292	301708
+49	2	1	17	520259	508788	216837	35449	241394	3040781	40781	5429	1	0	0	3	50	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3077	9	16404	348292	301708
+50	2	1	18	220258	208787	124422	158	66696	3040745	40745	5429	1	0	0	3	51	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3074	9	16404	348292	301708
+51	2	1	19	310247	298776	172593	34359	72479	3040759	40759	5429	1	0	0	3	52	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	1	0	0	0	-1	0	3074	9	16404	348292	301708
+52	2	1	20	310257	298786	207443	309	76540	3040742	40742	5429	1	0	0	3	53	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3078	9	16404	348292	301708
+53	2	1	21	220258	208787	118019	13534	63306	3040717	40717	5429	1	0	0	3	54	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3092	9	16404	348292	301708
+54	2	1	22	220268	208797	123344	986	65752	3040747	40747	5429	1	0	0	3	55	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	1	0	0	0	-1	0	3196	9	16404	348292	301708
+55	2	1	23	220263	208793	112328	3681	73175	3040725	40725	5429	1	0	0	3	56	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	3080	9	16404	348292	301708
+56	2	1	24	280271	268800	173491	10746	66059	3040717	40717	5429	1	0	0	3	57	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	1	0	0	0	-1	0	3133	9	16404	348292	301708
+57	2	1	25	250263	238792	132796	13548	71881	3040739	40739	5429	1	0	0	3	58	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	1	0	0	0	-1	0	4507	9	16404	348292	301708
+61	2	1	29	260263	248792	160087	13825	51604	3040755	40755	5429	1	0	0	3	62	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	1	0	0	0	-1	0	4509	9	16404	348292	301708
+62	2	1	30	230263	218793	115483	13142	73715	3040736	40736	5429	1	0	0	3	63	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	1	0	0	0	-1	0	3080	9	16404	348292	301708
+66	2	2	3	330245	318771	223713	7619	69219	3040744	40744	5429	1	0	0	3	67	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3077	9	10623	348292	301812
+67	2	2	4	470264	458790	283622	46341	110469	3040810	40810	5429	1	0	0	3	68	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	2	0	0	0	-1	0	3128	9	10623	348292	301812
+68	2	2	5	350251	338777	219482	15013	81828	3040770	40770	5429	1	0	0	3	69	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3076	9	10623	348292	301812
+69	2	2	6	360254	348780	260801	19098	47699	3040770	40770	5429	1	0	0	3	70	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	2	0	0	0	-1	0	3131	9	10623	348292	301812
+70	2	2	7	330256	318782	218298	4324	72520	3040739	40739	5429	1	0	0	3	71	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	2	0	0	0	-1	0	3077	9	10623	348292	301812
+71	2	2	8	380265	368791	267186	20532	66048	3040703	40703	5429	1	0	0	3	72	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	2	0	0	0	-1	0	3233	9	10623	348292	301812
+72	2	2	9	330260	318786	239572	7721	48729	3040780	40780	5429	1	0	0	3	73	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3478	9	10623	348292	301812
+73	2	2	10	350258	338784	244905	153	76699	3040777	40777	5429	1	0	0	3	74	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3078	9	10623	348292	301812
+75	2	2	12	360256	348782	226529	23246	83599	3040757	40757	5429	1	0	0	3	76	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3076	9	10623	348292	301812
+76	2	2	13	340258	328784	231597	153	76700	3040772	40772	5429	1	0	0	3	77	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	2	0	0	0	-1	0	3077	9	10623	348292	301812
+77	2	2	14	380257	368783	277832	5412	71326	3040777	40777	5429	1	0	0	3	78	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3195	9	10623	348292	301812
+78	2	2	15	340259	328785	246789	10295	56435	3040717	40717	5429	1	0	0	3	79	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	2	0	0	0	-1	0	3197	9	10623	348292	301812
+79	2	2	16	310253	298780	211639	1614	65235	3040765	40765	5429	1	0	0	3	80	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	2	0	0	0	-1	0	3077	9	10623	348292	301812
+80	2	2	17	310257	298783	219883	156	56697	3040739	40739	5429	1	0	0	3	81	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	2	0	0	0	-1	0	3076	9	10623	348292	301812
+81	2	2	18	320256	308782	233508	985	55815	3040730	40730	5429	1	0	0	3	82	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3127	9	10623	348292	301812
+82	2	2	19	340253	328779	215793	15851	80997	3040754	40754	5429	1	0	0	3	83	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3077	9	10623	348292	301812
+83	2	2	20	380256	368782	252883	11193	85387	3040788	40788	5429	1	0	0	3	84	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3229	9	10623	348292	301812
+85	2	2	22	370259	358785	261130	9450	66006	3040750	40750	5429	1	0	0	3	86	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	2	0	0	0	-1	0	4390	9	10623	348292	301812
+86	2	2	23	320253	308779	214746	5815	71033	3040774	40774	5429	1	0	0	3	87	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	2	0	0	0	-1	0	3077	9	10623	348292	301812
+87	2	2	24	370254	358780	270686	11548	55071	3040790	40790	5429	1	0	0	3	88	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	2	0	0	0	-1	0	3189	9	10623	348292	301812
+88	2	2	25	360256	348782	261159	3303	63498	3040781	40781	5429	1	0	0	3	89	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3129	9	10623	348292	301812
+89	2	2	26	340258	328784	213770	19425	77430	3040783	40783	5429	1	0	0	3	90	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	2	0	0	0	-1	0	3076	9	10623	348292	301812
+90	2	2	27	370261	358788	272824	2089	64653	3040792	40792	5429	1	0	0	3	91	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	2	0	0	0	-1	0	3197	9	10623	348292	301812
+91	2	2	28	330253	318779	214740	14306	72542	3040759	40759	5429	1	0	0	3	92	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	2	0	0	0	-1	0	3077	9	10623	348292	301812
+93	2	2	30	320253	308779	227448	525	66321	3040771	40771	5429	1	0	0	3	94	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	2	0	0	0	-1	0	3077	9	10623	348292	301812
+94	1	3	0	3269808	3269808	227448	525	66321	3040809	40809	5429	1	0	0	3	95	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	3	0	0	0	-1	0	3077	0	10623	348292	301812
+95	2	3	1	330253	318770	228460	3200	63650	3040795	40795	5429	1	0	0	3	96	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3072	9	10613	348292	301812
+96	2	3	2	390249	378770	294876	8124	57314	3040773	40773	5429	1	0	0	3	97	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	4491	9	10618	348292	301812
+97	2	3	3	320258	308779	222162	5280	61574	3040769	40769	5429	1	0	0	3	98	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	3	0	0	0	-1	0	3077	9	10618	348292	301812
+98	2	3	4	390265	378786	279347	10538	66048	3040753	40753	5429	1	0	0	3	99	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3232	9	10618	348292	301812
+99	2	3	5	350265	338786	240216	10552	66048	3040766	40766	5429	1	0	0	3	100	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3219	9	10618	348292	301812
+100	2	3	6	380264	368785	238533	36080	70510	3040790	40790	5429	1	0	0	3	101	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3238	9	10618	348292	301812
+101	2	3	7	440249	428770	225476	92771	92703	3040790	40790	5429	1	0	0	3	102	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	4449	9	10618	348292	301812
+104	2	3	10	370253	358774	287610	5773	51072	3040752	40752	5429	1	0	0	3	105	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	3	0	0	0	-1	0	3080	9	10618	348292	301812
+105	2	3	11	400265	388786	230175	32122	104468	3040744	40744	5429	1	0	0	3	106	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	3	0	0	0	-1	0	3233	9	10618	348292	301812
+106	2	3	12	420256	408777	298707	17231	69572	3040758	40758	5429	1	0	0	3	107	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3130	9	10618	348292	301812
+107	2	3	13	350253	338774	212587	1658	105179	3040751	40751	5429	1	0	0	3	108	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	3	0	0	0	-1	0	3076	9	10618	348292	301812
+108	2	3	14	340260	328781	218652	13794	72822	3040772	40772	5429	1	0	0	3	109	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3130	9	10618	348292	301812
+109	2	3	15	410256	398777	234481	19366	126200	3040809	40809	5429	1	0	0	3	110	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	3	0	0	0	-1	0	3136	9	10618	348292	301812
+110	2	3	16	390255	378776	305933	5001	51845	3040790	40790	5429	1	0	0	3	111	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3083	9	10618	348292	301812
+111	2	3	17	340245	328766	233442	185	76655	3040794	40794	5429	1	0	0	3	112	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	3	0	0	0	-1	0	3077	9	10618	348292	301812
+112	2	3	18	390257	378778	290262	151	66701	3040772	40772	5429	1	0	0	3	113	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	3	0	0	0	-1	0	3077	9	10618	348292	301812
+113	2	3	19	420256	408777	319209	4826	61912	3040795	40795	5429	1	0	0	3	114	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	3	0	0	0	-1	0	3193	9	10618	348292	301812
+114	2	3	20	330258	318779	218947	2589	74265	3040767	40767	5429	1	0	0	3	115	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3076	9	10618	348292	301812
+115	2	3	21	370256	358777	278565	704	56096	3040765	40765	5429	1	0	0	3	116	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3131	9	10618	348292	301812
+116	2	3	22	320251	308772	224292	3278	63560	3040746	40746	5429	1	0	0	3	117	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	3	0	0	0	-1	0	3078	9	10618	348292	301812
+117	2	3	23	380256	368777	281555	7077	59721	3040794	40794	5429	1	0	0	3	118	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	3	0	0	0	-1	0	3132	9	10618	348292	301812
+118	2	3	24	350247	338768	251155	16436	49516	3040793	40793	5429	1	0	0	3	119	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	3	0	0	0	-1	0	3130	9	10618	348292	301812
+119	2	3	25	340253	328774	224475	6435	80406	3040761	40761	5429	1	0	0	3	120	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	3	0	0	0	-1	0	3078	9	10618	348292	301812
+123	2	3	29	420258	408779	326051	154	66695	3040795	40795	5429	1	0	0	3	124	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	3	0	0	0	-1	0	3079	9	10618	348292	301812
+124	2	3	30	330258	318779	220482	15349	61455	3040737	40737	5429	1	0	0	3	125	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	3	0	0	0	-1	0	3125	9	10618	348292	301812
+125	1	4	0	3459808	3459808	220482	15349	61455	3040746	40746	5429	1	0	0	3	126	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	4	0	0	0	-1	0	3125	0	10618	348292	301812
+126	2	4	1	240272	228788	134849	158	76698	3040729	40729	5429	1	0	0	3	127	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	4	0	0	0	-1	0	3082	9	16390	348292	301708
+127	2	4	2	320267	308784	192398	16338	80467	3040785	40785	5429	1	0	0	3	128	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	4	0	0	0	-1	0	3134	9	16392	348292	301708
+128	2	4	3	260269	248785	164998	371	66490	3040732	40732	5429	1	0	0	3	129	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3074	9	16391	348292	301708
+129	2	4	4	220265	208777	116441	1006	75503	3040737	40737	5429	1	0	0	3	130	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3426	9	16386	348292	301708
+130	2	4	5	360266	348775	235223	163	96678	3040752	40752	5429	1	0	0	3	131	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	4	0	0	0	-1	0	3092	9	16383	348292	301708
+131	2	4	6	220267	208773	127478	3886	62580	3040774	40774	5429	1	0	0	3	132	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3132	9	16380	348292	301708
+132	2	4	7	250269	238772	140761	22326	54276	3040751	40751	5429	1	0	0	3	133	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	4	0	0	0	-1	0	3239	9	16377	348292	301708
+133	2	4	8	230269	218770	113644	6306	80542	3040736	40736	5429	1	0	0	3	134	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	4	0	0	0	-1	0	3090	9	16374	348292	301708
+134	2	4	9	260261	248758	157400	185	76650	3040740	40740	5429	1	0	0	3	135	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	4	0	0	0	-1	0	3092	9	16372	348292	301708
+135	2	4	10	240269	228764	113882	7621	89227	3040749	40749	5429	1	0	0	3	136	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3090	9	16369	348292	301708
+136	2	4	11	240269	228760	122030	6179	80666	3040760	40760	5429	1	0	0	3	137	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	4	0	0	0	-1	0	3088	9	16366	348292	301708
+137	2	4	12	290270	278759	162867	6775	88716	3040780	40780	5429	1	0	0	3	138	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	4	0	0	0	-1	0	4460	9	16363	348292	301708
+138	2	4	13	260275	248760	160301	159	66696	3040748	40748	5429	1	0	0	3	139	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3086	9	16360	348292	301708
+139	2	4	14	390269	378752	261689	154	96707	3040759	40759	5429	1	0	0	3	140	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3074	9	16357	348292	301708
+140	2	4	15	300261	288742	173611	26936	69858	3040739	40739	5429	1	0	0	3	141	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3133	9	16355	348292	301708
+141	2	4	16	270266	258743	125184	9791	106952	3040764	40764	5429	1	0	0	3	142	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3198	9	16351	348292	301708
+142	2	4	17	320269	308744	201579	21238	65526	3040781	40781	5429	1	0	0	3	143	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	4	0	0	0	-1	0	3126	9	16348	348292	301708
+143	2	4	18	250267	238738	123994	4382	91206	3040779	40779	5429	1	0	0	3	144	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	4	0	0	0	-1	0	3130	9	16346	348292	301708
+144	2	4	19	240258	228726	134167	8243	68605	3040751	40751	5429	1	0	0	3	145	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3083	9	16343	348292	301708
+145	2	4	20	250271	238736	134263	20288	66457	3040773	40773	5429	1	0	0	3	146	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3197	9	16340	348292	301708
+146	2	4	21	250267	238729	132405	2609	84203	3040729	40729	5429	1	0	0	3	147	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3128	9	16337	348292	301708
+147	2	4	22	220267	208726	133493	1299	55520	3040715	40715	5429	1	0	0	3	148	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3119	9	16334	348292	301708
+148	2	4	23	280267	268724	140668	26836	79347	3040778	40778	5429	1	0	0	3	149	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	3129	9	16331	348292	301708
+149	2	4	24	240263	228717	122331	16014	70668	3040728	40728	5429	1	0	0	3	150	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	4	0	0	0	-1	0	3248	9	16328	348292	301708
+150	2	4	25	350266	338717	223457	5450	91293	3040801	40801	5429	1	0	0	3	151	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	4	0	0	0	-1	0	3194	9	16325	348292	301708
+151	2	4	26	240266	228714	123606	23563	63176	3040761	40761	5429	1	0	0	3	152	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	4	0	0	0	-1	0	3202	9	16322	348292	301708
+152	2	4	27	250256	238702	122554	29367	66075	3040747	40747	5429	1	0	0	3	153	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	4	0	0	0	-1	0	4499	9	16320	348292	301708
+153	2	4	28	320267	308709	126957	2780	164028	3040726	40726	5429	1	0	0	3	154	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	4	0	0	0	-1	0	3132	9	16317	348292	301708
+154	2	4	29	230274	218887	117546	5689	81160	3040765	40765	5429	1	0	0	3	155	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	4	0	0	0	-1	0	3090	9	16488	348292	301708
+155	2	4	30	280268	268878	172750	16460	60278	3040766	40766	5429	1	0	0	3	156	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	4	0	0	0	-1	0	3194	9	16485	348292	301708
+156	1	5	0	3169808	3169808	172750	16460	60278	3040787	40787	5429	1	0	0	3	157	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	5	0	0	0	-1	0	3194	0	16485	348292	301708
+157	2	5	1	240267	228867	128850	163	76683	3040727	40727	5429	1	0	0	3	158	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	5	0	0	0	-1	0	3088	9	16475	348292	301708
+158	2	5	2	300254	288856	186537	6939	78543	3040769	40769	5429	1	0	0	3	159	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	4406	9	16476	348292	301708
+159	2	5	3	250267	238866	137324	3942	81544	3040767	40767	5429	1	0	0	3	160	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	5	0	0	0	-1	0	4467	9	16474	348292	301708
+160	2	5	4	220274	208870	123400	163	66545	3040753	40753	5429	1	0	0	3	161	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	5	0	0	0	-1	0	3087	9	16471	348292	301708
+161	2	5	5	280262	268855	156239	10016	86601	3040766	40766	5429	1	0	0	3	162	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3203	9	16468	348292	301708
+162	2	5	6	230263	218854	137822	5037	61692	3040769	40769	5429	1	0	0	3	163	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3198	9	16465	348292	301708
+163	2	5	7	240263	228851	154604	5787	50834	3040768	40768	5429	1	0	0	3	164	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3313	9	16462	348292	301708
+165	2	5	9	230269	218850	134956	1809	65039	3040774	40774	5429	1	0	0	3	166	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	5	0	0	0	-1	0	3087	9	16456	348292	301708
+166	2	5	10	280269	268847	176242	163	76690	3040765	40765	5429	1	0	0	3	167	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	5	0	0	0	-1	0	3085	9	16453	348292	301708
+167	2	5	11	240271	228847	113082	13409	83450	3040762	40762	5429	1	0	0	3	168	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	5	0	0	0	-1	0	3078	9	16450	348292	301708
+168	2	5	12	280268	268841	166465	8845	77654	3040762	40762	5429	1	0	0	3	169	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3448	9	16447	348292	301708
+169	2	5	13	240270	228840	136102	6881	69937	3040765	40765	5429	1	0	0	3	170	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3123	9	16444	348292	301708
+170	2	5	14	230269	218837	130820	2118	64694	3040789	40789	5429	1	0	0	3	171	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3131	9	16441	348292	301708
+171	2	5	15	250267	238831	153197	2497	64306	3040790	40790	5429	1	0	0	3	172	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	5	0	0	0	-1	0	3133	9	16439	348292	301708
+172	2	5	16	330269	318830	209245	10179	76675	3040649	40649	5429	1	0	0	3	173	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	5	0	0	0	-1	0	3083	9	16436	348292	301708
+173	2	5	17	230267	218825	127074	4334	72512	3040742	40742	5429	1	0	0	3	174	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	5	0	0	0	-1	0	3089	9	16433	348292	301708
+174	2	5	18	380266	368821	270999	9186	67574	3040799	40799	5429	1	0	0	3	175	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3089	9	16430	348292	301708
+175	2	5	19	210267	198819	113798	185	66662	3040776	40776	5429	1	0	0	3	176	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	5	0	0	0	-1	0	3088	9	16427	348292	301708
+179	2	5	23	300270	288811	178625	4284	81382	3040788	40788	5429	1	0	0	3	180	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	5	0	0	0	-1	0	3136	9	16415	348292	301708
+180	2	5	24	230272	218810	138312	2448	54366	3040746	40746	5429	1	0	0	3	181	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	5	0	0	0	-1	0	3131	9	16412	348292	301708
+181	2	5	25	310271	298806	143329	55625	79839	3040749	40749	5429	1	0	0	3	182	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	5	0	0	0	-1	0	4409	9	16410	348292	301708
+182	2	5	26	310269	298802	189633	5572	81238	3040775	40775	5429	1	0	0	3	183	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3133	9	16407	348292	301708
+183	2	5	27	250264	238793	136447	18147	68704	3040776	40776	5429	1	0	0	3	184	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3086	9	16404	348292	301708
+184	2	5	28	240270	228796	127568	8387	78421	3040764	40764	5429	1	0	0	3	185	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3135	9	16401	348292	301708
+185	2	5	29	240269	228793	128314	180	76591	3040783	40783	5429	1	0	0	3	186	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	5	0	0	0	-1	0	3174	9	16398	348292	301708
+186	2	5	30	330271	318791	186664	4263	112600	3040725	40725	5429	1	0	0	3	187	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	5	0	0	0	-1	0	3073	9	16395	348292	301708
+187	1	6	0	3369808	3369808	186664	4263	112600	3040816	40816	5429	1	0	0	3	188	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	6	0	0	0	-1	0	3073	0	16395	348292	301708
+188	2	6	1	260272	248784	152032	162	76698	3040723	40723	5429	1	0	0	3	189	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3079	9	16387	348292	301708
+189	2	6	2	270274	258786	135560	161	106697	3040761	40761	5429	1	0	0	3	190	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	6	0	0	0	-1	0	3082	9	16386	348292	301708
+190	2	6	3	290269	278778	170125	318	86425	3040807	40807	5429	1	0	0	3	191	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3197	9	16383	348292	301708
+191	2	6	4	290271	278777	125574	12737	122737	3040788	40788	5429	1	0	0	3	192	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	4420	9	16380	348292	301708
+192	2	6	5	280258	268761	186056	6611	58816	3040790	40790	5429	1	0	0	3	193	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	4421	9	16377	348292	301708
+196	2	6	9	230263	218754	142110	4143	52605	3040776	40776	5429	1	0	0	3	197	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	6	0	0	0	-1	0	3190	9	16366	348292	301708
+197	2	6	10	200258	188746	112020	5597	51241	3040790	40790	5429	1	0	0	3	198	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3088	9	16363	348292	301708
+198	2	6	11	320272	308757	197568	18046	78565	3040784	40784	5429	1	0	0	3	199	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	6	0	0	0	-1	0	3098	9	16360	348292	301708
+199	2	6	12	260269	248752	158581	160	66696	3040746	40746	5429	1	0	0	3	200	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	6	0	0	0	-1	0	3079	9	16357	348292	301708
+200	2	6	13	290270	278751	159934	156	96705	3040769	40769	5429	1	0	0	3	201	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3075	9	16355	348292	301708
+202	2	6	15	820265	808739	223863	100003	466741	3040836	40836	5429	0	1	0	3	203	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3077	9	16349	348292	301708
+203	2	6	16	210260	198731	124987	155	56696	3040749	40749	5429	1	0	0	3	204	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	6	0	0	0	-1	0	3075	9	16346	348292	301708
+204	2	6	17	320262	308730	126974	92868	73852	3040790	40790	5429	1	0	0	3	205	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3096	9	16343	348292	301708
+205	2	6	18	220261	208726	123713	1440	65400	3040756	40756	5429	1	0	0	3	206	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	6	0	0	0	-1	0	3087	9	16340	348292	301708
+206	2	6	19	250267	238729	141705	12264	64587	3040776	40776	5429	1	0	0	3	207	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3084	9	16337	348292	301708
+207	2	6	20	350269	338728	229687	191	86671	3040806	40806	5429	1	0	0	3	208	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	6	0	0	0	-1	0	3073	9	16334	348292	301708
+208	2	6	21	280272	268729	182532	5257	60179	3040806	40806	5429	1	0	0	3	209	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	6	0	0	0	-1	0	4515	9	16331	348292	301708
+209	2	6	22	240269	228723	132336	163	76685	3040732	40732	5429	1	0	0	3	210	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3087	9	16328	348292	301708
+210	2	6	23	240269	228720	119767	10761	76090	3040776	40776	5429	1	0	0	3	211	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3085	9	16325	348292	301708
+211	2	6	24	270271	258719	163090	7353	68118	3040746	40746	5429	1	0	0	3	212	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	6	0	0	0	-1	0	4419	9	16322	348292	301708
+212	2	6	25	240271	228716	128306	214	76486	3040752	40752	5429	1	0	0	3	213	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	6	0	0	0	-1	0	3232	9	16320	348292	301708
+213	2	6	26	310266	298708	141900	7290	128167	3040759	40759	5429	1	0	0	3	214	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	6	0	0	0	-1	0	4410	9	16317	348292	301708
+214	2	6	27	310257	298871	185763	27408	68017	3040778	40778	5429	1	0	0	3	215	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	4514	9	16488	348292	301708
+215	2	6	28	240266	228876	128608	5552	71254	3040772	40772	5429	1	0	0	3	216	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	6	0	0	0	-1	0	3133	9	16485	348292	301708
+216	2	6	29	410269	398876	130097	20229	226574	3040780	40780	5429	1	0	0	3	217	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	6	0	0	0	-1	0	3133	9	16482	348292	301708
+217	2	6	30	220259	208864	120547	163	66676	3040766	40766	5429	1	0	0	3	218	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	6	0	0	0	-1	0	3088	9	16479	348292	301708
+218	1	7	0	2929808	2929808	120547	163	66676	3040831	40831	5429	1	0	0	3	219	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	7	0	0	0	-1	0	3088	0	16479	348292	301708
+219	2	7	1	300270	288864	188359	719	76091	3040788	40788	5429	1	0	0	3	220	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3133	9	16469	348292	301708
+220	2	7	2	360269	348866	236600	2580	93072	3040776	40776	5429	1	0	0	3	221	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	7	0	0	0	-1	0	3133	9	16470	348292	301708
+221	2	7	3	230269	218862	131851	14914	51902	3040768	40768	5429	1	0	0	3	222	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3125	9	16468	348292	301708
+222	2	7	4	240262	228852	134826	9685	66900	3040718	40718	5429	1	0	0	3	223	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3094	9	16464	348292	301708
+223	2	7	5	250270	238857	143998	1478	75333	3040789	40789	5429	1	0	0	3	224	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	7	0	0	0	-1	0	3132	9	16462	348292	301708
+224	2	7	6	240274	228858	145996	2380	64435	3040751	40751	5429	1	0	0	3	225	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	7	0	0	0	-1	0	3133	9	16459	348292	301708
+225	2	7	7	220267	208848	115147	1769	75078	3040739	40739	5429	1	0	0	3	226	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3088	9	16456	348292	301708
+226	2	7	8	270267	258845	126002	163	116683	3040763	40763	5429	1	0	0	3	227	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3088	9	16453	348292	301708
+227	2	7	9	220267	208842	113068	5324	71522	3040730	40730	5429	1	0	0	3	228	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	7	0	0	0	-1	0	3088	9	16450	348292	301708
+228	2	7	10	230267	218839	128201	162	76688	3040732	40732	5429	1	0	0	3	229	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3087	9	16447	348292	301708
+229	2	7	11	300266	288836	189823	179	76561	3040760	40760	5429	1	0	0	3	230	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3200	9	16444	348292	301708
+230	2	7	12	230270	218837	133819	5529	61100	3040772	40772	5429	1	0	0	3	231	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	7	0	0	0	-1	0	3134	9	16441	348292	301708
+231	2	7	13	210269	198833	113190	5385	61464	3040724	40724	5429	1	0	0	3	232	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3087	9	16439	348292	301708
+232	2	7	14	310269	298831	181450	5370	90098	3040760	40760	5429	1	0	0	3	233	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	7	0	0	0	-1	0	4486	9	16436	348292	301708
+233	2	7	15	230269	218828	127981	622	76186	3040794	40794	5429	1	0	0	3	234	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3135	9	16433	348292	301708
+234	2	7	16	230262	218817	134439	10614	55962	3040804	40804	5429	1	0	0	3	235	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	7	0	0	0	-1	0	3242	9	16430	348292	301708
+235	2	7	17	220267	208819	119498	1087	65759	3040747	40747	5429	1	0	0	3	236	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	7	0	0	0	-1	0	3088	9	16427	348292	301708
+236	2	7	18	300270	288819	185028	3138	82850	3040798	40798	5429	1	0	0	3	237	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	7	0	0	0	-1	0	3136	9	16424	348292	301708
+237	2	7	19	260269	248816	129690	2235	94577	3040773	40773	5429	1	0	0	3	238	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	7	0	0	0	-1	0	3131	9	16421	348292	301708
+238	2	7	20	320270	308813	221860	3103	63704	3040793	40793	5429	1	0	0	3	239	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	7	0	0	0	-1	0	3135	9	16418	348292	301708
+239	2	7	21	260267	248808	117686	18745	98101	3040777	40777	5429	1	0	0	3	240	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	7	0	0	0	-1	0	3089	9	16415	348292	301708
+240	2	7	22	260266	248804	135432	15142	80301	3040797	40797	5429	1	0	0	3	241	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	7	0	0	0	-1	0	4397	9	16412	348292	301708
+241	2	7	23	280266	268801	121882	8409	117037	3040809	40809	5429	1	0	0	3	242	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	7	0	0	0	-1	0	4410	9	16409	348292	301708
+242	2	7	24	670262	658794	195117	9755	436978	3040786	40786	5429	1	0	0	3	243	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	7	0	0	0	-1	0	3094	9	16406	348292	301708
+243	2	7	25	260267	248796	133114	199	96652	3040773	40773	5429	1	0	0	3	244	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3086	9	16404	348292	301708
+244	2	7	26	250267	238793	125183	12772	84078	3040793	40793	5429	1	0	0	3	245	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3089	9	16401	348292	301708
+245	2	7	27	260269	248792	176573	1268	55470	3040770	40770	5429	1	0	0	3	246	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	7	0	0	0	-1	0	3208	9	16398	348292	301708
+246	2	7	28	230261	218781	123121	1819	75025	3040739	40739	5429	1	0	0	3	247	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	7	0	0	0	-1	0	3083	9	16395	348292	301708
+247	2	7	29	250272	238789	131809	6896	79954	3040754	40754	5429	1	0	0	3	248	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	7	0	0	0	-1	0	3087	9	16392	348292	301708
+248	2	7	30	270266	258782	166415	6682	68774	3040771	40771	5429	1	0	0	3	249	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	7	0	0	0	-1	0	4399	9	16391	348292	301708
+249	1	8	0	3339808	3339808	166415	6682	68774	3040783	40783	5429	1	0	0	3	250	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	8	0	0	0	-1	0	4399	0	16391	348292	301708
+250	2	8	1	230272	218776	135176	4705	62109	3040766	40766	5429	1	0	0	3	251	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3131	9	16379	348292	301708
+251	2	8	2	270267	258773	144753	1565	95239	3040809	40809	5429	1	0	0	3	252	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	8	0	0	0	-1	0	3133	9	16380	348292	301708
+252	2	8	3	250269	238773	128215	12148	74659	3040785	40785	5429	1	0	0	3	253	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3133	9	16377	348292	301708
+253	2	8	4	340258	328758	217710	30309	66282	3040789	40789	5429	1	0	0	3	254	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3238	9	16375	348292	301708
+254	2	8	5	220258	208755	129344	1821	54915	3040682	40682	5429	1	0	0	3	255	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	8	0	0	0	-1	0	3195	9	16372	348292	301708
+255	2	8	6	210267	198761	113125	3067	63779	3040797	40797	5429	1	0	0	3	256	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3088	9	16369	348292	301708
+256	2	8	7	300262	288753	182998	10351	76239	3040759	40759	5429	1	0	0	3	257	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	8	0	0	0	-1	0	3229	9	16366	348292	301708
+257	2	8	8	330278	318767	236675	7968	58631	3040780	40780	5429	1	0	0	3	258	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3239	9	16363	348292	301708
+258	2	8	9	230259	218744	126035	6389	70440	3040753	40753	5429	1	0	0	3	259	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	8	0	0	0	-1	0	3087	9	16360	348292	301708
+259	2	8	10	270272	258755	174582	4665	60837	3040800	40800	5429	1	0	0	3	260	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	4455	9	16357	348292	301708
+260	2	8	11	250269	238750	148809	4876	61936	3040738	40738	5429	1	0	0	3	261	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	8	0	0	0	-1	0	3131	9	16356	348292	301708
+261	2	8	12	260272	248749	142712	4976	80475	3040787	40787	5429	1	0	0	3	262	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	4502	9	16351	348292	301708
+262	2	8	13	230267	218741	135763	3887	62416	3040730	40730	5429	1	0	0	3	263	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	8	0	0	0	-1	0	3133	9	16348	348292	301708
+263	2	8	14	270258	258729	149024	21827	64998	3040761	40761	5429	1	0	0	3	264	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	8	0	0	0	-1	0	3107	9	16346	348292	301708
+264	2	8	15	210269	198737	122859	3720	52174	3040787	40787	5429	1	0	0	3	265	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3135	9	16343	348292	301708
+265	2	8	16	340269	328734	135506	6855	169997	3040781	40781	5429	1	0	0	3	266	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	8	0	0	0	-1	0	3083	9	16340	348292	301708
+267	2	8	18	230267	218726	136230	161	66689	3040774	40774	5429	1	0	0	3	268	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3089	9	16334	348292	301708
+268	2	8	19	230261	218718	125739	3748	73092	3040766	40766	5429	1	0	0	3	269	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	8	0	0	0	-1	0	3087	9	16331	348292	301708
+273	2	8	24	310269	298711	158936	179	116630	3040760	40760	5429	1	0	0	3	274	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3133	9	16317	348292	301708
+274	2	8	25	290267	278880	193707	4594	60895	3040761	40761	5429	1	0	0	3	275	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	4467	9	16488	348292	301708
+275	2	8	26	220267	208877	116868	6688	70153	3040745	40745	5429	1	0	0	3	276	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3088	9	16485	348292	301708
+276	2	8	27	270271	258879	174021	4542	60947	3040780	40780	5429	1	0	0	3	277	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	4464	9	16482	348292	301708
+277	2	8	28	280267	268872	158032	24283	71807	3040738	40738	5429	1	0	0	3	278	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	8	0	0	0	-1	0	3135	9	16479	348292	301708
+278	2	8	29	230267	218869	123755	3073	73734	3040744	40744	5429	1	0	0	3	279	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3134	9	16476	348292	301708
+279	2	8	30	210267	198866	126499	162	56688	3040760	40760	5429	1	0	0	3	280	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	8	0	0	0	-1	0	3087	9	16473	348292	301708
+280	1	9	0	3329808	3329808	126499	162	56688	3040828	40828	5429	1	0	0	3	281	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	9	0	0	0	-1	0	3087	0	16473	348292	301708
+281	2	9	1	220275	208864	126264	2920	63934	3040738	40738	5429	1	0	0	3	282	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	3087	9	16402	348292	310308
+282	2	9	2	270269	258859	177097	163	66685	3040767	40767	5429	1	0	0	3	283	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	3086	9	16404	348292	310308
+283	2	9	3	220269	208857	126216	5142	61711	3040753	40753	5429	1	0	0	3	284	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	9	0	0	0	-1	0	3085	9	16401	348292	310308
+284	2	9	4	310274	298858	184670	6094	89358	3040833	40833	5429	1	0	0	3	285	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	4503	9	16398	348292	310308
+285	2	9	5	240268	228849	128654	975	75424	3040795	40795	5429	1	0	0	3	286	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	3548	9	16395	348292	310308
+286	2	9	6	260267	248845	131179	20649	74840	3040737	40737	5429	1	0	0	3	287	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	4465	9	16392	348292	310308
+287	2	9	7	210269	198844	128198	177	56564	3040768	40768	5429	1	0	0	3	288	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	9	0	0	0	-1	0	3205	9	16389	348292	310308
+288	2	9	8	220269	208841	112660	9684	67164	3040801	40801	5429	1	0	0	3	289	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	9	0	0	0	-1	0	3087	9	16386	348292	310308
+289	2	9	9	240267	228837	129490	11128	65718	3040761	40761	5429	1	0	0	3	290	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	3089	9	16383	348292	310308
+290	2	9	10	310262	298829	183119	11935	84636	3040788	40788	5429	1	0	0	3	291	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	3243	9	16380	348292	310308
+291	2	9	11	290262	278826	126899	22568	114166	3040760	40760	5429	1	0	0	3	292	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	9	0	0	0	-1	0	3093	9	16378	348292	310308
+292	2	9	12	290274	278835	186736	5103	70344	3040761	40761	5429	1	0	0	3	293	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	4503	9	16375	348292	310308
+293	2	9	13	250269	238828	139952	3721	73089	3040782	40782	5429	1	0	0	3	294	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	9	0	0	0	-1	0	3133	9	16372	348292	310308
+294	2	9	14	260269	248824	148187	180	86563	3040795	40795	5429	1	0	0	3	295	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	3203	9	16369	348292	310308
+295	2	9	15	220271	208823	125820	7960	57504	3040781	40781	5429	1	0	0	3	296	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	9	0	0	0	-1	0	4408	9	16366	348292	310308
+296	2	9	16	220267	208816	113261	8628	68218	3040782	40782	5429	1	0	0	3	297	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	9	0	0	0	-1	0	3089	9	16363	348292	310308
+297	2	9	17	380269	368815	235847	37332	79527	3040799	40799	5429	1	0	0	3	298	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	9	0	0	0	-1	0	3076	9	16360	348292	310308
+298	2	9	18	250267	238810	150462	3948	62802	3040812	40812	5429	1	0	0	3	299	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	9	0	0	0	-1	0	3132	9	16357	348292	310308
+299	2	9	19	230269	218810	113812	2612	84233	3040768	40768	5429	1	0	0	3	300	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	3090	9	16354	348292	310308
+300	2	9	20	240269	228807	133396	178	76670	3040760	40760	5429	1	0	0	3	301	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	9	0	0	0	-1	0	3087	9	16351	348292	310308
+301	2	9	21	300271	288806	173722	8734	86716	3040787	40787	5429	1	0	0	3	302	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	9	0	0	0	-1	0	4503	9	16348	348292	310308
+302	2	9	22	350269	338801	250675	732	66125	3040801	40801	5429	1	0	0	3	303	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	3081	9	16345	348292	310308
+303	2	9	23	290270	278799	124866	48522	86958	3040766	40766	5429	1	0	0	3	304	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	4473	9	16343	348292	310308
+304	2	9	24	290269	278795	178522	4804	72003	3040767	40767	5429	1	0	0	3	305	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	9	0	0	0	-1	0	3136	9	16340	348292	310308
+305	2	9	25	220269	208792	126522	374	66475	3040759	40759	5429	1	0	0	3	306	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	9	0	0	0	-1	0	3087	9	16337	348292	310308
+306	2	9	26	230269	218789	127279	6602	70240	3040752	40752	5429	1	0	0	3	307	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	9	0	0	0	-1	0	3087	9	16334	348292	310308
+307	2	9	27	270262	258779	178159	9634	56974	3040801	40801	5429	1	0	0	3	308	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	9	0	0	0	-1	0	3210	9	16331	348292	310308
+309	2	9	29	1040297	1028809	142135	100007	766579	3040859	40859	5429	0	1	0	3	310	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	9	0	0	0	-1	0	3133	9	16325	348292	310308
+310	2	9	30	230275	218784	132388	380	66474	3040760	40760	5429	1	0	0	3	311	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	9	0	0	0	-1	0	3087	9	16322	348292	310308
+311	1	10	0	3209808	3209808	132388	380	66474	3040787	40787	5429	1	0	0	3	312	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	10	0	0	0	-1	0	3087	0	16322	348292	310308
+312	2	10	1	420267	408766	139773	3127	243719	3040809	40809	5429	1	0	0	3	313	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3089	9	16288	348292	313748
+313	2	10	2	240270	228770	133009	5365	71446	3040747	40747	5429	1	0	0	3	314	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	10	0	0	0	-1	0	3132	9	16290	348292	313748
+314	2	10	3	220271	208768	124916	6121	59369	3040775	40775	5429	1	0	0	3	315	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	10	0	0	0	-1	0	4407	9	16287	348292	313748
+315	2	10	4	260274	248768	137564	3834	93018	3040775	40775	5429	1	0	0	3	316	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3089	9	16284	348292	313748
+316	2	10	5	270270	258761	144428	17030	79779	3040759	40759	5429	1	0	0	3	317	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3133	9	16281	348292	313748
+317	2	10	6	250270	238759	141098	163	76692	3040738	40738	5429	1	0	0	3	318	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	10	0	0	0	-1	0	3081	9	16278	348292	313748
+318	2	10	7	300269	288756	210337	2686	53782	3040767	40767	5429	1	0	0	3	319	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	10	0	0	0	-1	0	3136	9	16275	348292	313748
+319	2	10	8	210272	198755	112726	5960	60890	3040794	40794	5429	1	0	0	3	320	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3089	9	16272	348292	313748
+321	2	10	10	770272	758749	218499	100004	416735	3040852	40852	5429	0	1	0	3	322	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3201	9	16266	348292	313748
+322	2	10	11	470269	458743	354662	4811	82052	3040731	40731	5429	1	0	0	3	323	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	10	0	0	0	-1	0	3073	9	16263	348292	313748
+323	2	10	12	480270	468741	202889	69866	176952	3040752	40752	5429	1	0	0	3	324	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	10	0	0	0	-1	0	3122	9	16261	348292	313748
+324	2	10	13	230267	218735	122932	5622	71222	3040782	40782	5429	1	0	0	3	325	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3087	9	16258	348292	313748
+325	2	10	14	270269	258735	179105	177	56564	3040798	40798	5429	1	0	0	3	326	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	10	0	0	0	-1	0	3202	9	16255	348292	313748
+326	2	10	15	220267	208729	117446	11183	65662	3040752	40752	5429	1	0	0	3	327	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3089	9	16252	348292	313748
+327	2	10	16	270271	258730	182329	4180	51322	3040793	40793	5429	1	0	0	3	328	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3205	9	16249	348292	313748
+328	2	10	17	230261	218718	123992	6132	70708	3040746	40746	5429	1	0	0	3	329	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3087	9	16246	348292	313748
+329	2	10	18	300269	288723	179002	268	86469	3040802	40802	5429	1	0	0	3	330	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3208	9	16243	348292	313748
+330	2	10	19	320269	308720	156570	67591	69301	3040838	40838	5429	1	0	0	3	331	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	10	0	0	0	-1	0	3041	9	16240	348292	313748
+331	2	10	20	250262	238710	149263	10744	55833	3040780	40780	5429	1	0	0	3	332	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3243	9	16237	348292	313748
+332	2	10	21	220267	208713	119486	1782	65068	3040760	40760	5429	1	0	0	3	333	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-32	0	0	0	9	10	0	0	0	-1	0	3089	9	16235	348292	313748
+333	2	10	22	350267	338709	131054	21035	165810	3040804	40804	5429	1	0	0	3	334	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	10	0	0	0	-1	0	3086	9	16232	348292	313748
+334	2	10	23	300267	288880	185174	24101	61398	3040775	40775	5429	1	0	0	3	335	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	10	0	0	0	-1	0	3205	9	16403	348292	313748
+335	2	10	24	260264	248874	125886	11518	95064	3040768	40768	5429	1	0	0	3	336	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3238	9	16400	348292	313748
+336	2	10	25	320269	308876	222729	3140	63665	3040800	40800	5429	1	0	0	3	337	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	10	0	0	0	-1	0	3135	9	16397	348292	313748
+337	2	10	26	250262	238867	146044	8854	67731	3040788	40788	5429	1	0	0	3	338	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	10	0	0	0	-1	0	3094	9	16394	348292	313748
+339	2	10	28	270267	258866	157741	163	86683	3040787	40787	5429	1	0	0	3	340	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-33	0	0	0	9	10	0	0	0	-1	0	3089	9	16388	348292	313748
diff --git a/experiments/prepared_boot_wifi_opt_summary.tsv b/experiments/prepared_boot_wifi_opt_summary.tsv
new file mode 100644
index 0000000..fffe570
--- /dev/null
+++ b/experiments/prepared_boot_wifi_opt_summary.tsv
@@ -0,0 +1,12 @@
+variant	setting	delivery/30	wake_overhead_med	wifi_init_med	connect_med	txdone_med	hot_user_med	p90	max	heap_delta	notes
+D0_CONTROL	baseline	29/30	40732	16447	143814	6490	250269	320267	410269	46584	n=29;txok=29
+D1_STORAGE_RAM	WIFI_STORAGE_RAM	27/30	40736	16404	138933	5606	240270	310257	520259	46584	n=27;txok=27
+D2_NVS_OFF	nvs_enable=0	25/30	40770	10623	233508	7721	340259	380256	470264	46480	n=25;txok=25
+D3_RAM_NVS_OFF	RAM+nvs_off	25/30	40772	10618	238533	6435	370256	420256	440249	46480	n=25;txok=25
+G1_HT20	force HT20	30/30	40752	16360	134263	6306	250271	320269	390269	46584	txok=30
+H1_CS_OFF	dynamic_cs=false	26/30	40767	16441	138312	5037	250264	310271	380266	46584	n=26;txok=26
+H2_CS_ON	dynamic_cs=true	26/30	40776	16357	141900	5552	270274	320272	820265	46584	n=26;txok=25
+E1_TX_HALF	dyn_tx=16	30/30	40772	16433	133819	3138	260266	310269	670262	46584	txok=30
+E2_TX_MIN	dyn_tx=8	25/30	40766	16366	136230	4665	250269	310269	340269	46584	n=25;txok=25
+E3_RX_HALF	rx 5/16	29/30	40768	16366	132388	5142	250269	310274	1040297	37984	n=29;txok=28
+E4_RX_MIN	rx 3/8	26/30	40780	16272	146044	5960	270269	420267	770272	34544	n=26;txok=25
diff --git a/experiments/prepared_boot_wifi_val100.tsv b/experiments/prepared_boot_wifi_val100.tsv
new file mode 100644
index 0000000..d203420
--- /dev/null
+++ b/experiments/prepared_boot_wifi_val100.tsv
@@ -0,0 +1,93 @@
+record_id	kind	outer	hot	user_us	wifi_us	connect_us	txdone_us	teardown_us	sleep_elapsed_us	sleep_overhead_us	app_entry_us	cb_seen	cb_timeout	brownout	auth	seq	diag_mode	tx_cb_total	tx_cb_success	tx_cb_failed	first_status	first_cb_delta_us	first_success_delta_us	first_failed_delta_us	last_cb_delta_us	callbacks_after_success	rssi	disconnect_count	last_disconnect_reason	reconnect_count	ap_primary	variant	short_retry	long_retry	retry_called	retry_set_rc	retry_cfg_us	encode_us	actual_channel	wifi_init_us	heap_before	heap_after
+1	1	0	0	3199803	3199803	0	0	0	3040677	40677	5410	0	0	0	0	2	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	0	0	0	0	0	255	0	0	0	-1	0	0	0	0	0	0
+2	2	0	1	250268	238882	124073	3149	93609	3040735	40735	5410	1	0	0	3	3	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3178	9	16382	348292	310308
+3	2	0	2	320270	308888	211187	160	76701	3040692	40692	5410	1	0	0	3	4	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+5	2	0	4	220268	208886	127971	459	66399	3040653	40653	5410	1	0	0	3	6	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3079	9	16387	348292	310308
+6	2	0	5	230270	218888	135838	160	66702	3040658	40658	5410	1	0	0	3	7	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+7	2	0	6	310257	298876	192586	12995	72536	3040720	40720	5410	1	0	0	3	8	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	4360	9	16387	348292	310308
+8	2	0	7	250272	238890	131618	11669	73774	3040686	40686	5410	1	0	0	3	9	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	4504	9	16387	348292	310308
+9	2	0	8	250268	238887	128646	6867	79962	3040710	40710	5410	1	0	0	3	10	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3111	9	16387	348292	310308
+10	2	0	9	370265	358883	234388	23387	83344	3040718	40718	5410	1	0	0	3	11	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3085	9	16387	348292	310308
+11	2	0	10	340259	328877	210960	26283	69208	3040736	40736	5410	1	0	0	3	12	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	4450	9	16387	348292	310308
+12	2	0	11	350266	338885	230742	19417	67411	3040686	40686	5410	1	0	0	3	13	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3110	9	16387	348292	310308
+13	2	0	12	350262	338879	226130	19174	77152	3040694	40694	5410	1	0	0	3	14	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3111	9	16386	348292	310308
+14	2	0	13	240275	228893	128889	11621	65143	3040708	40708	5410	1	0	0	3	15	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3178	9	16387	348292	310308
+15	2	0	14	250267	238885	112269	292	106566	3040668	40668	5410	1	0	0	3	16	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+16	2	0	15	280270	268888	184860	3133	63729	3040676	40676	5410	1	0	0	3	17	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+17	2	0	16	320264	308882	180552	25718	80928	3040722	40722	5410	1	0	0	3	18	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3193	9	16387	348292	310308
+18	2	0	17	260270	248888	125547	608	106153	3040701	40701	5410	1	0	0	3	19	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3179	9	16387	348292	310308
+19	2	0	18	330269	318887	203440	32164	64582	3040716	40716	5410	1	0	0	3	20	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3082	9	16387	348292	310308
+20	2	0	19	230262	218880	124746	6970	68521	3040695	40695	5410	1	0	0	3	21	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	4375	9	16387	348292	310308
+21	2	0	20	240268	228887	138384	9075	57749	3040690	40690	5410	1	0	0	3	22	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3112	9	16387	348292	310308
+22	2	0	21	280260	268879	161137	30694	56173	3040681	40681	5410	1	0	0	3	23	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3063	9	16387	348292	310308
+23	2	0	22	280267	268885	152337	6047	90807	3040697	40697	5410	1	0	0	3	24	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3081	9	16387	348292	310308
+24	2	0	23	240268	228887	124486	5880	80974	3040651	40651	5410	1	0	0	3	25	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+25	2	0	24	320268	308886	175436	317	116542	3040717	40717	5410	1	0	0	3	26	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+26	2	0	25	230259	218877	116468	27156	58276	3040673	40673	5410	1	0	0	3	27	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	4501	9	16387	348292	310308
+27	2	0	26	260275	248893	124226	160	106553	3040693	40693	5410	1	0	0	3	28	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3079	9	16387	348292	310308
+29	2	0	28	230265	218883	146322	1514	55341	3040738	40738	5410	1	0	0	3	30	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+30	2	0	29	240275	228893	122140	26049	59549	3040717	40717	5410	1	0	0	3	31	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3112	9	16387	348292	310308
+31	2	0	30	230272	218891	135675	6315	60508	3040710	40710	5410	1	0	0	3	32	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3119	9	16387	348292	310308
+32	2	0	31	280266	268884	146748	30806	75783	3040716	40716	5410	1	0	0	3	33	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3229	9	16387	348292	310308
+33	2	0	32	270270	258888	163004	1367	75493	3040717	40717	5410	1	0	0	3	34	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-38	0	0	0	9	0	0	0	0	-1	0	3074	9	16387	348292	310308
+34	2	0	33	230269	218887	128785	162	66690	3040639	40639	5410	1	0	0	3	35	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3087	9	16387	348292	310308
+35	2	0	34	220266	208885	109803	16447	60372	3040668	40668	5410	1	0	0	3	36	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3109	9	16387	348292	310308
+36	2	0	35	250257	238876	116643	362	106484	3040746	40746	5410	1	0	0	3	37	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3080	9	16387	348292	310308
+37	2	0	36	260275	248893	131114	20616	76215	3040717	40717	5410	1	0	0	3	38	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3112	9	16387	348292	310308
+38	2	0	37	220270	208888	124091	4172	62594	3040746	40746	5410	1	0	0	3	39	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3172	9	16387	348292	310308
+39	2	0	38	260268	248887	149678	18639	58120	3040683	40683	5410	1	0	0	3	40	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3181	9	16387	348292	310308
+40	2	0	39	240268	228887	129956	22520	54303	3040746	40746	5410	1	0	0	3	41	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3113	9	16387	348292	310308
+41	2	0	40	260261	248879	136021	31333	65247	3040688	40688	5410	1	0	0	3	42	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3230	9	16387	348292	310308
+42	2	0	41	240267	228886	123192	5621	79838	3040683	40683	5410	1	0	0	3	43	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	4482	9	16387	348292	310308
+43	2	0	42	250270	238888	127881	33581	62977	3040691	40691	5410	1	0	0	3	44	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3280	9	16387	348292	310308
+44	2	0	43	260256	248874	121473	16812	88614	3040691	40691	5410	1	0	0	3	45	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	4502	9	16387	348292	310308
+45	2	0	44	250268	238887	115599	25825	81001	3040678	40678	5410	1	0	0	3	46	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3111	9	16387	348292	310308
+46	2	0	45	230269	218887	142417	166	56694	3040717	40717	5410	1	0	0	3	47	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+47	2	0	46	240275	228893	123878	24037	62794	3040678	40678	5410	1	0	0	3	48	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3111	9	16387	348292	310308
+48	2	0	47	230268	218887	129787	16867	49959	3040702	40702	5410	1	0	0	3	49	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3111	9	16387	348292	310308
+49	2	0	48	260270	248888	109905	4240	112621	3040727	40727	5410	1	0	0	3	50	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+50	2	0	49	300254	288872	167380	26425	80409	3040713	40713	5410	1	0	0	3	51	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3081	9	16387	348292	310308
+51	2	0	50	290259	278877	172485	18125	68476	3040701	40701	5410	1	0	0	3	52	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3227	9	16387	348292	310308
+52	2	0	51	250269	238887	122906	160	96701	3040720	40720	5410	1	0	0	3	53	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3075	9	16387	348292	310308
+53	2	0	52	250269	238887	130256	11704	73745	3040717	40717	5410	1	0	0	3	54	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	4494	9	16387	348292	310308
+55	2	0	54	240268	228887	130600	4913	71911	3040723	40723	5410	1	0	0	3	56	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3113	9	16387	348292	310308
+56	2	0	55	260259	248877	130245	26825	68606	3040679	40679	5410	1	0	0	3	57	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	4501	9	16387	348292	310308
+57	2	0	56	250266	238884	129959	10986	75839	3040704	40704	5410	1	0	0	3	58	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3109	9	16387	348292	310308
+59	2	0	58	220279	208897	127654	449	66419	3040694	40694	5410	1	0	0	3	60	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3080	9	16387	348292	310308
+60	2	0	59	280275	268894	193184	163	56702	3040678	40678	5410	1	0	0	3	61	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+63	2	0	62	210270	198888	123583	925	55865	3040710	40710	5410	1	0	0	3	64	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3154	9	16387	348292	310308
+64	2	0	63	280266	268884	146584	15154	91438	3040729	40729	5410	1	0	0	3	65	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3228	9	16387	348292	310308
+65	2	0	64	470270	458888	219071	22376	194443	3040742	40742	5410	1	0	0	3	66	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-37	0	0	0	9	0	0	0	0	-1	0	3118	9	16387	348292	310308
+66	2	0	65	260268	248886	154688	15407	61451	3040727	40727	5410	1	0	0	3	67	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-38	0	0	0	9	0	0	0	0	-1	0	3075	9	16387	348292	310308
+67	2	0	66	310268	298886	186749	31937	64666	3040720	40720	5410	1	0	0	3	68	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-38	0	0	0	9	0	0	0	0	-1	0	3229	9	16387	348292	310308
+68	2	0	67	240269	228887	116840	636	96221	3040722	40722	5410	1	0	0	3	69	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3080	9	16387	348292	310308
+69	2	0	68	260269	248887	126101	1413	105444	3040722	40722	5410	1	0	0	3	70	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3080	9	16387	348292	310308
+70	2	0	69	240267	228885	146713	1831	65021	3040708	40708	5410	1	0	0	3	71	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3080	9	16387	348292	310308
+71	2	0	70	260266	248884	133662	17785	79038	3040715	40715	5410	1	0	0	3	72	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3110	9	16387	348292	310308
+72	2	0	71	240247	228865	116961	13736	83067	3040699	40699	5410	1	0	0	3	73	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3111	9	16387	348292	310308
+74	2	0	73	210269	198887	135677	348	46509	3040715	40715	5410	1	0	0	3	75	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3081	9	16387	348292	310308
+75	2	0	74	320268	308887	198080	18449	78407	3040722	40722	5410	1	0	0	3	76	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3080	9	16387	348292	310308
+76	2	0	75	300270	288888	161975	34399	72191	3040749	40749	5410	1	0	0	3	77	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3229	9	16387	348292	310308
+77	2	0	76	310277	298895	183703	6585	90276	3040715	40715	5410	1	0	0	3	78	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+78	2	0	77	230268	218886	113432	10803	76056	3040703	40703	5410	1	0	0	3	79	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+79	2	0	78	240268	228886	124115	14594	72264	3040709	40709	5410	1	0	0	3	80	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+81	2	0	80	240268	228886	138026	621	76237	3040729	40729	5410	1	0	0	3	82	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+82	2	0	81	230268	218886	115317	6006	80852	3040717	40717	5410	1	0	0	3	83	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+83	2	0	82	250267	238885	142563	15535	61320	3040717	40717	5410	1	0	0	3	84	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3080	9	16387	348292	310308
+84	2	0	83	320264	308883	182601	26929	79821	3040730	40730	5410	1	0	0	3	85	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3085	9	16387	348292	310308
+85	2	0	84	290269	278887	178495	679	76178	3040710	40710	5410	1	0	0	3	86	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3080	9	16387	348292	310308
+86	2	0	85	240270	228888	125057	15965	70894	3040710	40710	5410	1	0	0	3	87	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3075	9	16387	348292	310308
+87	2	0	86	280262	268881	177666	27595	49018	3040696	40696	5410	1	0	0	3	88	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-36	0	0	0	9	0	0	0	0	-1	0	3222	9	16387	348292	310308
+88	2	0	87	280268	268886	187257	1911	64942	3040747	40747	5410	1	0	0	3	89	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3085	9	16387	348292	310308
+89	2	0	88	320270	308888	215875	25448	50009	3040758	40758	5410	1	0	0	3	90	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	4488	9	16387	348292	310308
+90	2	0	89	220268	208886	109153	1398	75461	3040717	40717	5410	1	0	0	3	91	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+91	2	0	90	280262	268880	159342	32360	54260	3040710	40710	5410	1	0	0	3	92	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3209	9	16387	348292	310308
+92	2	0	91	230269	218887	131321	15305	50147	3040709	40709	5410	1	0	0	3	93	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	4484	9	16387	348292	310308
+93	2	0	92	220268	208886	114037	3651	73208	3040717	40717	5410	1	0	0	3	94	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3077	9	16387	348292	310308
+94	2	0	93	370263	358881	169495	96005	70664	3040709	40709	5410	1	0	0	3	95	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+96	2	0	95	230266	218884	108214	2216	94637	3040724	40724	5410	1	0	0	3	97	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+97	2	0	96	240268	228886	124862	14711	72148	3040702	40702	5410	1	0	0	3	98	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
+98	2	0	97	270267	258886	176074	4295	61143	3040769	40769	5410	1	0	0	3	99	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-35	0	0	0	9	0	0	0	0	-1	0	4503	9	16387	348292	310308
+99	2	0	98	270270	258888	153913	24277	61211	3040730	40730	5410	1	0	0	3	100	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	4454	9	16387	348292	310308
+100	2	0	99	220269	208887	115189	2631	74227	3040651	40651	5410	1	0	0	3	101	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3080	9	16387	348292	310308
+101	2	0	100	230276	218894	111430	7009	79855	3040725	40725	5410	1	0	0	3	102	0	1	1	0	1	4294967295	4294967295	4294967295	4294967295	0	-34	0	0	0	9	0	0	0	0	-1	0	3078	9	16387	348292	310308
diff --git a/experiments/run_boot_wifi_opt.py b/experiments/run_boot_wifi_opt.py
new file mode 100644
index 0000000..f79bf4a
--- /dev/null
+++ b/experiments/run_boot_wifi_opt.py
@@ -0,0 +1,323 @@
+"""
+One-flash boot/Wi-Fi HOT opt campaign orchestrator.
+COM only for flash; after FLASH_OK never touch serial.
+Progress = receiver TSV/log only.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+BUILD = ROOT / "build-esp32c6-save-bench-smoke"
+AETHER = r"C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0"
+PY = Path(r"C:\Espressif\python_env\idf6.0_py3.11_env\Scripts\python.exe")
+CMAKE = Path(r"C:\Espressif\tools\cmake\3.30.2\bin\cmake.exe")
+NINJA = Path(r"C:\Espressif\tools\ninja\1.12.1\ninja.exe")
+RX_EXE = ROOT / "temperature_receiver" / "build-bisect" / "temperature_receiver.exe"
+RX_BUILD = ROOT / "temperature_receiver" / "build-bisect"
+RX_SESSION = ROOT / "experiments" / "prepared_wifi_cache_rx_session"
+IDF_PATH = r"C:\Espressif\frameworks\esp-idf-v6.0.2"
+CCACHE = r"C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64"
+PROGRESS = ROOT / "experiments" / "boot_wifi_opt_progress.log"
+RX_LOG = ROOT / "experiments" / "prepared_boot_wifi_opt_rx.log"
+TSV = ROOT / "experiments" / "prepared_boot_wifi_opt.tsv"
+PORT = "COM7"
+VARIANTS = 11
+HOT_PER = 30
+
+
+def env() -> dict:
+    e = os.environ.copy()
+    e["IDF_PATH"] = IDF_PATH
+    e["IDF_TOOLS_PATH"] = r"C:\Espressif"
+    extra = [
+        CCACHE,
+        r"C:\Espressif\tools\ninja\1.12.1",
+        r"C:\Espressif\tools\cmake\3.30.2\bin",
+        r"C:\msys64\ucrt64\bin",
+    ]
+    e["Path"] = ";".join(extra) + ";" + e.get("Path", "")
+    e.pop("CCACHE_DISABLE", None)
+    return e
+
+
+def log(msg: str) -> None:
+    line = time.strftime("%H:%M:%S") + " " + msg
+    print(line, flush=True)
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(line + "\n")
+
+
+def force_sdk_fixes() -> None:
+    sdk = BUILD / "sdkconfig"
+    if not sdk.exists():
+        return
+    text = sdk.read_text(encoding="utf-8")
+    reps = [
+        ("CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_RTC_CLK_SRC_INT_RC=y", "# CONFIG_RTC_CLK_SRC_INT_RC is not set"),
+        ("# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set", "CONFIG_RTC_CLK_SRC_EXT_CRYS=y"),
+        ("CONFIG_ESP_BROWNOUT_DET=n", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("# CONFIG_ESP_BROWNOUT_DET is not set", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("CONFIG_PM_ENABLE=y", "# CONFIG_PM_ENABLE is not set"),
+    ]
+    for a, b in reps:
+        text = text.replace(a, b)
+    if "CONFIG_RTC_CLK_SRC_EXT_CRYS=y" not in text:
+        text += "\nCONFIG_RTC_CLK_SRC_EXT_CRYS=y\n"
+    # A4 CONFIG_RTC_CLK_CAL_CYCLES=0: first flash with forced 0 left COM awake
+    # and produced zero Aether telemetry; keep IDF effective default (1024).
+    if "CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y" not in text:
+        text += "\nCONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y\n"
+    sdk.write_text(text, encoding="utf-8")
+
+
+def kill_receiver() -> None:
+    subprocess.run(
+        ["taskkill", "/F", "/IM", "temperature_receiver.exe"],
+        capture_output=True,
+        text=True,
+    )
+    time.sleep(1)
+
+
+def rebuild_receiver() -> None:
+    log("rebuild temperature_receiver")
+    r = subprocess.run(
+        [str(CMAKE), "--build", str(RX_BUILD), "--parallel"],
+        env=env(),
+        capture_output=True,
+        text=True,
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "boot_wifi_opt_rx_build.err").write_text(
+            (r.stdout or "")[-8000:] + "\n" + (r.stderr or "")[-8000:],
+            encoding="utf-8",
+        )
+        raise RuntimeError("receiver build failed")
+    log("receiver build ok")
+
+
+def start_receiver() -> None:
+    kill_receiver()
+    RX_SESSION.mkdir(parents=True, exist_ok=True)
+    if TSV.exists():
+        TSV.unlink()
+    env2 = env()
+    env2["AE_RECEIVER_SESSION_DIR"] = str(RX_SESSION)
+    env2["AE_DS_TSV"] = str(TSV)
+    with RX_LOG.open("w", encoding="utf-8") as outf, (
+        ROOT / "experiments" / "prepared_boot_wifi_opt_rx.log.err"
+    ).open("w", encoding="utf-8") as errf:
+        subprocess.Popen(
+            [str(RX_EXE)],
+            cwd=str(RX_SESSION),
+            env=env2,
+            stdout=outf,
+            stderr=errf,
+        )
+    t0 = time.time()
+    while time.time() - t0 < 60:
+        text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+        if "RECEIVER_UID=" in text:
+            log("receiver ready")
+            return
+        time.sleep(1)
+    raise RuntimeError("receiver not ready")
+
+
+def cmake_configure() -> None:
+    args = [
+        str(CMAKE),
+        "-S",
+        str(ROOT),
+        "-B",
+        str(BUILD),
+        "-G",
+        "Ninja",
+        f"-DCPM_aether-client-cpp_SOURCE={AETHER}",
+        "-DAE_EXP_PREPARED_BOOT_WIFI_OPT=1",
+        "-DAE_EXP_PREPARED_MAC_RETRY_DIAG=",
+        "-DAE_EXP_PREPARED_TX_DONE_DIAG=",
+        "-DAE_EXP_PREPARED_DEEPSLEEP_5X50=",
+        "-DAE_EXP_PREPARED_WIFI_FASTEST=",
+        "-DAE_EXP_PREPARED_WIFI_BISECT=",
+        "-DAE_EXP_SKIP_DTOR_SAVE=1",
+        "-DSERVICE_UID=5aade50f-00d9-4624-b097-e203cdcf1e38",
+        "-DBENCH_CLIENT_ID=prepared_deepsleep_5x50_v1",
+        "-DAETHER_PREPARED_NONCE_RESERVE=40",
+        "-DWIFI_SSID=chirkov",
+        "-DWIFI_PASSWORD=kcdjepWz51",
+        "-DCMAKE_BUILD_TYPE=Release",
+    ]
+    log("cmake configure boot_wifi_opt")
+    r = subprocess.run(args, cwd=ROOT, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "boot_wifi_opt_cmake.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("cmake failed")
+    force_sdk_fixes()
+    # Re-run cmake so Kconfig picks forced sdkconfig values when needed.
+    r2 = subprocess.run(args, cwd=ROOT, env=env(), capture_output=True, text=True)
+    if r2.returncode != 0:
+        raise RuntimeError("cmake reconfigure failed")
+    force_sdk_fixes()
+    log("cmake ok")
+
+
+def ninja_build() -> None:
+    log("ninja build")
+    r = subprocess.run(
+        [str(NINJA), "-C", str(BUILD)], env=env(), capture_output=True, text=True
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "boot_wifi_opt_build.err").write_text(
+            (r.stdout or "")[-16000:] + "\n" + (r.stderr or "")[-8000:],
+            encoding="utf-8",
+        )
+        raise RuntimeError("ninja failed")
+    log("build ok")
+    # Snapshot effective boot/rtc flags
+    sdk = BUILD / "sdkconfig"
+    keys = [
+        "CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP",
+        "CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS",
+        "CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF",
+        "CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE",
+        "CONFIG_ESPTOOLPY_FLASHMODE",
+        "CONFIG_ESPTOOLPY_FLASHFREQ",
+        "CONFIG_RTC_CLK_SRC_EXT_CRYS",
+        "CONFIG_RTC_CLK_CAL_CYCLES",
+        "CONFIG_SECURE_BOOT",
+        "CONFIG_FLASH_ENCRYPTION_ENABLED",
+        "CONFIG_ESP_WIFI_AMPDU_TX_ENABLED",
+        "CONFIG_ESP_WIFI_AMPDU_RX_ENABLED",
+        "CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM",
+        "CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM",
+        "CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM",
+    ]
+    lines = []
+    text = sdk.read_text(encoding="utf-8", errors="replace") if sdk.exists() else ""
+    for k in keys:
+        for ln in text.splitlines():
+            if k in ln and not ln.strip().startswith("#") or ln.startswith(f"# {k}"):
+                if k in ln:
+                    lines.append(ln)
+                    break
+    (ROOT / "experiments" / "boot_wifi_opt_sdkconfig_snapshot.txt").write_text(
+        "\n".join(lines) + "\n", encoding="utf-8"
+    )
+
+
+def wait_com_for_flash_only(timeout_s: float = 120.0) -> None:
+    log(f"pre-flash: waiting up to {int(timeout_s)}s for {PORT} (awake window)")
+    t0 = time.time()
+    while time.time() - t0 < timeout_s:
+        r = subprocess.run(
+            [
+                "powershell",
+                "-NoProfile",
+                "-Command",
+                f"Get-PnpDevice -Class Ports -Status OK | Where-Object {{ $_.FriendlyName -match '{PORT}' }} | Select-Object -ExpandProperty FriendlyName",
+            ],
+            capture_output=True,
+            text=True,
+        )
+        if PORT in (r.stdout or ""):
+            log(f"pre-flash: {PORT} present")
+            return
+        time.sleep(2.0)
+    raise RuntimeError(f"{PORT} not available for flash — wake/power-cycle ESP once")
+
+
+def flash_once() -> None:
+    wait_com_for_flash_only()
+    log(f"flash {PORT} (COM allowed only here)")
+    cmd = [
+        str(PY),
+        "-m",
+        "esptool",
+        "--chip",
+        "esp32c6",
+        "-p",
+        PORT,
+        "-b",
+        "460800",
+        "write-flash",
+        "--flash-size",
+        "4MB",
+        "0x0",
+        str(BUILD / "bootloader" / "bootloader.bin"),
+        "0x8000",
+        str(BUILD / "partition_table" / "partition-table.bin"),
+        "0x10000",
+        str(BUILD / "temperature_sensor.bin"),
+    ]
+    r = subprocess.run(cmd, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "boot_wifi_opt_flash.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("flash failed")
+    log("FLASH_OK — closing COM; further progress via Aether only")
+
+
+def progress_from_log() -> tuple[int, int, int]:
+    text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+    fulls = len(re.findall(r"^BWO_FULL ", text, re.M))
+    hots = len(re.findall(r"^BWO V", text, re.M))
+    finals = len(re.findall(r"^BWO_FINAL|BENCH_DONE boot_wifi_opt", text, re.M))
+    return fulls, hots, finals
+
+
+def wait_campaign(timeout_s: float = 55 * 60) -> None:
+    log("wait Aether campaign (no COM)")
+    t0 = time.time()
+    last = (-1, -1, -1)
+    while time.time() - t0 < timeout_s:
+        f, h, fin = progress_from_log()
+        if (f, h, fin) != last:
+            last = (f, h, fin)
+            log(f"progress full={f} hot={h} final={fin}")
+            text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+            lines = [ln for ln in text.splitlines() if ln.startswith("BWO V")]
+            if lines:
+                log("  " + lines[-1][:220])
+        if fin > 0 or h >= VARIANTS * HOT_PER:
+            log(f"STOP campaign full={f} hot={h} final={fin}")
+            return
+        if f >= VARIANTS + 1 and h >= VARIANTS * HOT_PER - 5:
+            log(f"STOP near-complete full={f} hot={h}")
+            return
+        time.sleep(2.0)
+    log(f"TIMEOUT full={last[0]} hot={last[1]} final={last[2]}")
+
+
+def main() -> int:
+    if PROGRESS.exists():
+        PROGRESS.write_text("", encoding="utf-8")
+    rebuild_receiver()
+    start_receiver()
+    cmake_configure()
+    ninja_build()
+    flash_once()
+    wait_campaign()
+    kill_receiver()
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as e:
+        log(f"ERROR {e}")
+        kill_receiver()
+        sys.exit(1)
diff --git a/experiments/run_boot_wifi_val100.py b/experiments/run_boot_wifi_val100.py
new file mode 100644
index 0000000..2b4fc40
--- /dev/null
+++ b/experiments/run_boot_wifi_val100.py
@@ -0,0 +1,269 @@
+"""VAL100 combined winners — one flash, no COM after FLASH_OK."""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+BUILD = ROOT / "build-esp32c6-save-bench-smoke"
+AETHER = r"C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0"
+PY = Path(r"C:\Espressif\python_env\idf6.0_py3.11_env\Scripts\python.exe")
+CMAKE = Path(r"C:\Espressif\tools\cmake\3.30.2\bin\cmake.exe")
+NINJA = Path(r"C:\Espressif\tools\ninja\1.12.1\ninja.exe")
+RX_EXE = ROOT / "temperature_receiver" / "build-bisect" / "temperature_receiver.exe"
+RX_BUILD = ROOT / "temperature_receiver" / "build-bisect"
+RX_SESSION = ROOT / "experiments" / "prepared_wifi_cache_rx_session"
+IDF_PATH = r"C:\Espressif\frameworks\esp-idf-v6.0.2"
+CCACHE = r"C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64"
+PROGRESS = ROOT / "experiments" / "boot_wifi_val100_progress.log"
+RX_LOG = ROOT / "experiments" / "prepared_boot_wifi_val100_rx.log"
+TSV = ROOT / "experiments" / "prepared_boot_wifi_val100.tsv"
+PORT = "COM7"
+HOT_TARGET = 100
+
+
+def env() -> dict:
+    e = os.environ.copy()
+    e["IDF_PATH"] = IDF_PATH
+    e["IDF_TOOLS_PATH"] = r"C:\Espressif"
+    extra = [
+        CCACHE,
+        r"C:\Espressif\tools\ninja\1.12.1",
+        r"C:\Espressif\tools\cmake\3.30.2\bin",
+        r"C:\msys64\ucrt64\bin",
+    ]
+    e["Path"] = ";".join(extra) + ";" + e.get("Path", "")
+    e.pop("CCACHE_DISABLE", None)
+    return e
+
+
+def log(msg: str) -> None:
+    line = time.strftime("%H:%M:%S") + " " + msg
+    print(line, flush=True)
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(line + "\n")
+
+
+def force_sdk_fixes() -> None:
+    sdk = BUILD / "sdkconfig"
+    if not sdk.exists():
+        return
+    text = sdk.read_text(encoding="utf-8")
+    reps = [
+        ("CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_RTC_CLK_SRC_INT_RC=y", "# CONFIG_RTC_CLK_SRC_INT_RC is not set"),
+        ("# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set", "CONFIG_RTC_CLK_SRC_EXT_CRYS=y"),
+        ("# CONFIG_ESP_BROWNOUT_DET is not set", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("CONFIG_PM_ENABLE=y", "# CONFIG_PM_ENABLE is not set"),
+    ]
+    for a, b in reps:
+        text = text.replace(a, b)
+    if "CONFIG_RTC_CLK_SRC_EXT_CRYS=y" not in text:
+        text += "\nCONFIG_RTC_CLK_SRC_EXT_CRYS=y\n"
+    sdk.write_text(text, encoding="utf-8")
+
+
+def kill_receiver() -> None:
+    subprocess.run(
+        ["taskkill", "/F", "/IM", "temperature_receiver.exe"],
+        capture_output=True,
+        text=True,
+    )
+    time.sleep(1)
+
+
+def rebuild_receiver() -> None:
+    log("rebuild temperature_receiver")
+    r = subprocess.run(
+        [str(CMAKE), "--build", str(RX_BUILD), "--parallel"],
+        env=env(),
+        capture_output=True,
+        text=True,
+    )
+    if r.returncode != 0:
+        raise RuntimeError("receiver build failed")
+    log("receiver build ok")
+
+
+def start_receiver() -> None:
+    kill_receiver()
+    RX_SESSION.mkdir(parents=True, exist_ok=True)
+    if TSV.exists():
+        TSV.unlink()
+    env2 = env()
+    env2["AE_RECEIVER_SESSION_DIR"] = str(RX_SESSION)
+    env2["AE_DS_TSV"] = str(TSV)
+    with RX_LOG.open("w", encoding="utf-8") as outf, (
+        ROOT / "experiments" / "prepared_boot_wifi_val100_rx.log.err"
+    ).open("w", encoding="utf-8") as errf:
+        subprocess.Popen(
+            [str(RX_EXE)],
+            cwd=str(RX_SESSION),
+            env=env2,
+            stdout=outf,
+            stderr=errf,
+        )
+    t0 = time.time()
+    while time.time() - t0 < 60:
+        text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+        if "RECEIVER_UID=" in text:
+            log("receiver ready")
+            return
+        time.sleep(1)
+    raise RuntimeError("receiver not ready")
+
+
+def cmake_configure() -> None:
+    args = [
+        str(CMAKE),
+        "-S",
+        str(ROOT),
+        "-B",
+        str(BUILD),
+        "-G",
+        "Ninja",
+        f"-DCPM_aether-client-cpp_SOURCE={AETHER}",
+        "-DAE_EXP_PREPARED_BOOT_WIFI_VAL100=1",
+        "-DAE_EXP_PREPARED_BOOT_WIFI_OPT=",
+        "-DAE_EXP_PREPARED_MAC_RETRY_DIAG=",
+        "-DAE_EXP_PREPARED_TX_DONE_DIAG=",
+        "-DAE_EXP_PREPARED_DEEPSLEEP_5X50=",
+        "-DAE_EXP_SKIP_DTOR_SAVE=1",
+        "-DSERVICE_UID=5aade50f-00d9-4624-b097-e203cdcf1e38",
+        "-DBENCH_CLIENT_ID=prepared_deepsleep_5x50_v1",
+        "-DAETHER_PREPARED_NONCE_RESERVE=110",
+        "-DWIFI_SSID=chirkov",
+        "-DWIFI_PASSWORD=kcdjepWz51",
+        "-DCMAKE_BUILD_TYPE=Release",
+    ]
+    log("cmake configure VAL100")
+    r = subprocess.run(args, cwd=ROOT, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "boot_wifi_val100_cmake.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("cmake failed")
+    force_sdk_fixes()
+    log("cmake ok")
+
+
+def ninja_build() -> None:
+    log("ninja build")
+    r = subprocess.run(
+        [str(NINJA), "-C", str(BUILD)], env=env(), capture_output=True, text=True
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "boot_wifi_val100_build.err").write_text(
+            (r.stdout or "")[-16000:] + "\n" + (r.stderr or "")[-8000:],
+            encoding="utf-8",
+        )
+        raise RuntimeError("ninja failed")
+    log("build ok")
+
+
+def wait_com_for_flash_only(timeout_s: float = 180.0) -> None:
+    log(f"pre-flash: waiting up to {int(timeout_s)}s for {PORT}")
+    t0 = time.time()
+    while time.time() - t0 < timeout_s:
+        r = subprocess.run(
+            [
+                "powershell",
+                "-NoProfile",
+                "-Command",
+                f"Get-PnpDevice -Class Ports -Status OK | Where-Object {{ $_.FriendlyName -match '{PORT}' }} | Select-Object -ExpandProperty FriendlyName",
+            ],
+            capture_output=True,
+            text=True,
+        )
+        if PORT in (r.stdout or ""):
+            log(f"pre-flash: {PORT} present")
+            return
+        time.sleep(2.0)
+    raise RuntimeError(f"{PORT} not available — reset ESP once")
+
+
+def flash_once() -> None:
+    wait_com_for_flash_only()
+    log(f"flash {PORT}")
+    cmd = [
+        str(PY),
+        "-m",
+        "esptool",
+        "--chip",
+        "esp32c6",
+        "-p",
+        PORT,
+        "-b",
+        "460800",
+        "write-flash",
+        "--flash-size",
+        "4MB",
+        "0x0",
+        str(BUILD / "bootloader" / "bootloader.bin"),
+        "0x8000",
+        str(BUILD / "partition_table" / "partition-table.bin"),
+        "0x10000",
+        str(BUILD / "temperature_sensor.bin"),
+    ]
+    r = subprocess.run(cmd, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "boot_wifi_val100_flash.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("flash failed")
+    log("FLASH_OK — no further COM")
+
+
+def progress() -> tuple[int, int, int]:
+    text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+    fulls = len(re.findall(r"^BWO_FULL ", text, re.M))
+    hots = len(re.findall(r"^BWO V", text, re.M))
+    finals = len(re.findall(r"^BWO_FINAL|BENCH_DONE boot_wifi_opt", text, re.M))
+    return fulls, hots, finals
+
+
+def wait_campaign(timeout_s: float = 25 * 60) -> None:
+    log("wait VAL100 (no COM)")
+    t0 = time.time()
+    last = (-1, -1, -1)
+    while time.time() - t0 < timeout_s:
+        f, h, fin = progress()
+        if (f, h, fin) != last:
+            last = (f, h, fin)
+            log(f"progress full={f} hot={h} final={fin}")
+            text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+            lines = [ln for ln in text.splitlines() if ln.startswith("BWO V")]
+            if lines:
+                log("  " + lines[-1][:220])
+        if fin > 0 or h >= HOT_TARGET:
+            log(f"STOP full={f} hot={h} final={fin}")
+            return
+        time.sleep(2.0)
+    log(f"TIMEOUT full={last[0]} hot={last[1]} final={last[2]}")
+
+
+def main() -> int:
+    if PROGRESS.exists():
+        PROGRESS.write_text("", encoding="utf-8")
+    rebuild_receiver()
+    start_receiver()
+    cmake_configure()
+    ninja_build()
+    flash_once()
+    wait_campaign()
+    kill_receiver()
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as e:
+        log(f"ERROR {e}")
+        kill_receiver()
+        sys.exit(1)
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 39240b0..6bb3f20 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -51,6 +51,18 @@ elseif(AE_EXP_PREPARED_MAC_RETRY_DIAG)
     "experiment_early_entry.cpp"
     "prepared_send/prepared_send.cpp"
   )
+elseif(AE_EXP_PREPARED_BOOT_WIFI_OPT)
+  list(APPEND src_list
+    "prepared_boot_wifi_opt_bench.cpp"
+    "experiment_early_entry.cpp"
+    "prepared_send/prepared_send.cpp"
+  )
+elseif(AE_EXP_PREPARED_BOOT_WIFI_VAL100)
+  list(APPEND src_list
+    "prepared_boot_wifi_val100_bench.cpp"
+    "experiment_early_entry.cpp"
+    "prepared_send/prepared_send.cpp"
+  )
 elseif(AE_EXP_PREPARED_WIFI_BISECT)
   list(APPEND src_list
     "prepared_wifi_single_factor_bisect_bench.cpp"
@@ -193,6 +205,8 @@ set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING "Silent fastest-path prepared c
 set(AE_EXP_PREPARED_DEEPSLEEP_5X50 "" CACHE STRING "Silent deep-sleep 5x50 prepared E2E (set to 1)")
 set(AE_EXP_PREPARED_TX_DONE_DIAG "" CACHE STRING "Silent TX-done callback diagnostic 1x50 (set to 1)")
 set(AE_EXP_PREPARED_MAC_RETRY_DIAG "" CACHE STRING "Silent MAC retry-limit diagnostic 7x50 (set to 1)")
+set(AE_EXP_PREPARED_BOOT_WIFI_OPT "" CACHE STRING "Silent boot/wifi HOT opt campaign 11x30 (set to 1)")
+set(AE_EXP_PREPARED_BOOT_WIFI_VAL100 "" CACHE STRING "Silent VAL100 combined boot/wifi winners (set to 1)")
 set(AE_EXP_TX_DIAG_MODE "" CACHE STRING "TX-done diag mode 0=FIRST_ANY 1=FIRST_SUCCESS")
 set(AE_EXP_FAST_N "" CACHE STRING "Fastest-path prepared count")
 set(AE_EXP_FAST_TEST_ID "" CACHE STRING "Fastest-path test id")
@@ -250,6 +264,8 @@ ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_FASTEST)
 ae_exp_define_if_set(AE_EXP_PREPARED_DEEPSLEEP_5X50)
 ae_exp_define_if_set(AE_EXP_PREPARED_TX_DONE_DIAG)
 ae_exp_define_if_set(AE_EXP_PREPARED_MAC_RETRY_DIAG)
+ae_exp_define_if_set(AE_EXP_PREPARED_BOOT_WIFI_OPT)
+ae_exp_define_if_set(AE_EXP_PREPARED_BOOT_WIFI_VAL100)
 ae_exp_define_if_set(AE_EXP_TX_DIAG_MODE)
 ae_exp_define_if_set(AE_EXP_FAST_N)
 ae_exp_define_if_set(AE_EXP_FAST_TEST_ID)
@@ -270,6 +286,8 @@ ae_exp_define_if_set(AE_EXP_BISECT_SMOKE)
    AE_EXP_PREPARED_DEEPSLEEP_5X50 STREQUAL "1" OR
    AE_EXP_PREPARED_TX_DONE_DIAG STREQUAL "1" OR
    AE_EXP_PREPARED_MAC_RETRY_DIAG STREQUAL "1" OR
+   AE_EXP_PREPARED_BOOT_WIFI_OPT STREQUAL "1" OR
+   AE_EXP_PREPARED_BOOT_WIFI_VAL100 STREQUAL "1" OR
    (AE_EXP_PREPARED_WIFI_BISECT STREQUAL "1" AND
     NOT AE_EXP_BISECT_CONSOLE STREQUAL "1"))
   target_compile_definitions(aether PUBLIC "AE_EXP_SILENT=1")
diff --git a/main/bench_payload.h b/main/bench_payload.h
index 2c8314b..7ad7d6d 100644
--- a/main/bench_payload.h
+++ b/main/bench_payload.h
@@ -630,6 +630,135 @@ inline bool DecodeMacRetry(Buffer const& data, MacRetryPayload& out) {
   return out.magic == kMacRetryMagic;
 }
 
+
+// Boot / Wi-Fi HOT-path optimization campaign payload (experiment only).
+static constexpr std::uint8_t kBootWifiOptMagic = 0xD8;
+
+enum class BootWifiOptMsgType : std::uint8_t {
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+};
+
+enum class BootWifiOptVariant : std::uint8_t {
+  kD0Control = 0,
+  kD1StorageRam = 1,
+  kD2NvsOff = 2,
+  kD3RamNvsOff = 3,
+  kG1Ht20 = 4,
+  kH1CsOff = 5,
+  kH2CsOn = 6,
+  kE1TxHalf = 7,
+  kE2TxMin = 8,
+  kE3RxHalf = 9,
+  kE4RxMin = 10,
+  kCount = 11,
+};
+
+inline char const* BootWifiOptVariantName(std::uint8_t id) {
+  switch (static_cast(id)) {
+    case BootWifiOptVariant::kD0Control:
+      return "D0_CONTROL";
+    case BootWifiOptVariant::kD1StorageRam:
+      return "D1_STORAGE_RAM";
+    case BootWifiOptVariant::kD2NvsOff:
+      return "D2_NVS_OFF";
+    case BootWifiOptVariant::kD3RamNvsOff:
+      return "D3_RAM_NVS_OFF";
+    case BootWifiOptVariant::kG1Ht20:
+      return "G1_HT20";
+    case BootWifiOptVariant::kH1CsOff:
+      return "H1_CS_OFF";
+    case BootWifiOptVariant::kH2CsOn:
+      return "H2_CS_ON";
+    case BootWifiOptVariant::kE1TxHalf:
+      return "E1_TX_HALF";
+    case BootWifiOptVariant::kE2TxMin:
+      return "E2_TX_MIN";
+    case BootWifiOptVariant::kE3RxHalf:
+      return "E3_RX_HALF";
+    case BootWifiOptVariant::kE4RxMin:
+      return "E4_RX_MIN";
+    default:
+      if (id == 0xff) {
+        return "VAL100_COMBINED";
+      }
+      return "?";
+  }
+}
+
+#pragma pack(push, 1)
+struct BootWifiOptPayload {
+  std::uint8_t magic{kBootWifiOptMagic};
+  std::uint8_t type{0};
+  std::uint8_t variant_id{0};
+  std::uint8_t hot_index{0};
+  std::uint16_t sequence_global{0};
+  std::uint16_t record_id{0};
+  std::uint8_t reset_reason{0};
+  std::uint8_t wake_cause{0};
+  std::uint8_t brownout_count{0};
+  std::uint8_t flags{0};
+  std::uint32_t sleep_elapsed_to_app_us{0};
+  std::uint32_t sleep_to_app_overhead_us{0};
+  std::int64_t app_entry_esp_timer_us{0};
+  std::uint32_t pending_user_cycle_us{0};
+  std::uint32_t pending_wifi_cycle_us{0};
+  std::uint32_t wifi_init_us{0};
+  std::uint32_t connect_us{0};
+  std::uint32_t encode_send_us{0};
+  std::uint32_t tx_done_wait_us{0};
+  std::uint32_t teardown_us{0};
+  std::uint32_t heap_before_wifi{0};
+  std::uint32_t heap_after_wifi{0};
+  std::uint8_t tx_cb_total{0};
+  std::uint8_t tx_cb_success{0};
+  std::uint8_t tx_cb_failed{0};
+  std::uint8_t first_status{0xff};
+  std::uint8_t cb_timeout{0};
+  std::int8_t rssi{0};
+  std::uint8_t actual_channel{0};
+  std::uint8_t authmode{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t reconnect_count{0};
+  std::uint16_t prepared_message_left{0};
+  std::uint8_t prev_variant_id{0xff};
+  std::uint8_t prev_hot_send_count{0};
+  std::uint8_t prev_hot_attempt_count{0};
+  std::uint8_t prev_tx_success_count{0};
+  std::uint8_t prev_tx_fail_count{0};
+  std::uint8_t prev_cb_timeout_count{0};
+  std::uint32_t prev_txdone_sum_us{0};
+  std::uint8_t pending_kind{0};
+  std::uint8_t pending_variant{0};
+  std::uint8_t pending_hot_index{0};
+  std::uint8_t setting_flags{0};  // bit0 storage_ram bit1 nvs_off bit2 ht20 ...
+  std::uint8_t static_rx_buf{0};
+  std::uint8_t dynamic_rx_buf{0};
+  std::uint8_t dynamic_tx_buf{0};
+  std::int8_t dynamic_cs{-1};
+};
+#pragma pack(pop)
+
+static_assert(sizeof(BootWifiOptPayload) >= 80, "bootwifiopt payload size");
+
+template 
+inline Buffer EncodeBootWifiOpt(BootWifiOptPayload const& p) {
+  Buffer out(sizeof(BootWifiOptPayload));
+  std::memcpy(out.data(), &p, sizeof(BootWifiOptPayload));
+  return out;
+}
+
+template 
+inline bool DecodeBootWifiOpt(Buffer const& data, BootWifiOptPayload& out) {
+  if (data.size() < sizeof(BootWifiOptPayload)) {
+    return false;
+  }
+  std::memcpy(&out, data.data(), sizeof(BootWifiOptPayload));
+  return out.magic == kBootWifiOptMagic;
+}
+
+
 }  // namespace temp_sensor::bench
 
 #endif  // TEMP_SENSOR_BENCH_PAYLOAD_H_
diff --git a/main/experiment_early_entry.cpp b/main/experiment_early_entry.cpp
index fb8f3c5..a0f6012 100644
--- a/main/experiment_early_entry.cpp
+++ b/main/experiment_early_entry.cpp
@@ -9,7 +9,9 @@
 #if defined(ESP_PLATFORM) && \
     (defined(AE_EXP_PREPARED_DEEPSLEEP_5X50) || \
      defined(AE_EXP_PREPARED_TX_DONE_DIAG) || \
-     defined(AE_EXP_PREPARED_MAC_RETRY_DIAG))
+     defined(AE_EXP_PREPARED_MAC_RETRY_DIAG) || \
+     defined(AE_EXP_PREPARED_BOOT_WIFI_OPT) || \
+     defined(AE_EXP_PREPARED_BOOT_WIFI_VAL100))
 
 #  include 
 #  include 
diff --git a/main/experiment_early_entry.h b/main/experiment_early_entry.h
index 87601a7..c2a069a 100644
--- a/main/experiment_early_entry.h
+++ b/main/experiment_early_entry.h
@@ -21,7 +21,9 @@ struct ExperimentEarlyEntrySnapshot {
 #if defined(ESP_PLATFORM) && \
     (defined(AE_EXP_PREPARED_DEEPSLEEP_5X50) || \
      defined(AE_EXP_PREPARED_TX_DONE_DIAG) || \
-     defined(AE_EXP_PREPARED_MAC_RETRY_DIAG))
+     defined(AE_EXP_PREPARED_MAC_RETRY_DIAG) || \
+     defined(AE_EXP_PREPARED_BOOT_WIFI_OPT) || \
+     defined(AE_EXP_PREPARED_BOOT_WIFI_VAL100))
 extern "C" void ExperimentEarlyAppEntry();
 ExperimentEarlyEntrySnapshot const& GetExperimentEarlyEntrySnapshot();
 #else
diff --git a/main/prepared_boot_wifi_opt_bench.cpp b/main/prepared_boot_wifi_opt_bench.cpp
new file mode 100644
index 0000000..be64055
--- /dev/null
+++ b/main/prepared_boot_wifi_opt_bench.cpp
@@ -0,0 +1,1013 @@
+/*
+ * Copyright 2026 Aethernet Inc.
+ *
+ * Silent boot / Wi-Fi HOT-path optimization campaign (ESP32-C6).
+ * Runtime variants D/G/H/E x 30 HOT; FULL between variants; 3 s deep sleep.
+ * Compile-time A/B/C documented in report (one-flash campaign).
+ * Metrics travel in BootWifiOptPayload 0xD8; UART is silent.
+ */
+
+#include 
+#include 
+#include 
+
+#include "aether/all.h"
+#include "aether/ae_exp_wifi.h"
+#include "aether/config.h"
+#include "aether/env.h"
+#include "bench_payload.h"
+#include "experiment_early_entry.h"
+#include "prepared_send/prepared_send.h"
+
+#if defined(ESP_PLATFORM)
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#endif
+
+using namespace std::chrono_literals;
+
+#if defined(ESP_PLATFORM)
+extern "C" std::uint64_t esp_rtc_get_time_us(void);
+#endif
+
+namespace temp_sensor {
+namespace {
+
+static constexpr auto kParentUid =
+    ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165");
+
+#ifndef BENCH_CLIENT_ID
+#  define BENCH_CLIENT_ID "prepared_deepsleep_5x50_v1"
+#endif
+static constexpr char const* kBenchClientId = BENCH_CLIENT_ID;
+
+#if defined(SERVICE_UID)
+static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID);
+#else
+static constexpr auto kServiceUid =
+    ae::Uid::FromString("5aade50f-00d9-4624-b097-e203cdcf1e38");
+#endif
+
+static constexpr std::uint8_t kVariantCount = 11;
+static constexpr std::uint8_t kHotPerVariant = 30;
+static constexpr std::uint8_t kMaxHotAttempts = 40;
+static constexpr std::uint32_t kSleepUs = 3000000;
+static constexpr std::uint32_t kRtcMagic = 0x42574F31u;  // "BWO1"
+static constexpr std::uint16_t kRtcVersion = 1;
+
+enum class Phase : std::uint16_t {
+  kRegister = 0,
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+  kDone = 4,
+};
+
+struct VariantCfg {
+  bool storage_ram;
+  bool nvs_enable;
+  bool force_ht20;
+  std::int8_t dynamic_cs;  // -1 default, 0 off, 1 on
+  std::uint8_t static_rx;
+  std::uint8_t dynamic_rx;
+  std::uint8_t dynamic_tx;
+};
+
+static constexpr VariantCfg kVariants[kVariantCount] = {
+    {false, true, false, -1, 0, 0, 0},   // D0
+    {true, true, false, -1, 0, 0, 0},    // D1
+    {false, false, false, -1, 0, 0, 0},  // D2
+    {true, false, false, -1, 0, 0, 0},   // D3
+    {false, true, true, -1, 0, 0, 0},    // G1
+    {false, true, false, 0, 0, 0, 0},    // H1
+    {false, true, false, 1, 0, 0, 0},    // H2
+    {false, true, false, -1, 0, 0, 16},  // E1 TX half (32->16)
+    {false, true, false, -1, 0, 0, 8},   // E2 TX min-ish
+    {false, true, false, -1, 5, 16, 0},  // E3 RX half
+    {false, true, false, -1, 3, 8, 0},   // E4 RX min-ish
+};
+
+struct RtcState {
+  std::uint32_t magic;
+  std::uint16_t version;
+  std::uint16_t phase;
+  std::uint8_t variant_id;
+  std::uint8_t hot_index;
+  std::uint8_t hot_attempt_count;
+  std::uint8_t hot_send_count;
+  std::uint16_t sequence_global;
+  std::uint16_t next_record_id;
+  std::uint32_t requested_sleep_us;
+  std::uint64_t sleep_arm_rtc_us;
+  std::uint8_t pending_valid;
+  std::uint8_t pending_kind;
+  std::uint8_t pending_variant;
+  std::uint8_t pending_hot_index;
+  std::uint32_t pending_user_cycle_us;
+  std::uint32_t pending_wifi_cycle_us;
+  std::uint32_t pending_wifi_init_us;
+  std::uint32_t pending_connect_us;
+  std::uint32_t pending_encode_us;
+  std::uint32_t pending_txdone_us;
+  std::uint32_t pending_teardown_us;
+  std::uint32_t pending_heap_before;
+  std::uint32_t pending_heap_after;
+  std::uint8_t pending_cb_seen;
+  std::uint8_t pending_cb_timeout;
+  std::uint8_t pending_auth;
+  std::uint8_t brownout_count;
+  std::uint8_t unexpected_reset_count;
+  std::uint8_t current_boot_brownout;
+  std::uint8_t registered;
+  std::uint8_t final_fail_count;
+  std::uint8_t var_tx_success;
+  std::uint8_t var_tx_fail;
+  std::uint8_t var_cb_timeout;
+  std::uint8_t pad0;
+  std::uint32_t var_txdone_sum_us;
+  std::uint8_t prev_variant_id;
+  std::uint8_t prev_hot_send_count;
+  std::uint8_t prev_hot_attempt_count;
+  std::uint8_t prev_tx_success_count;
+  std::uint8_t prev_tx_fail_count;
+  std::uint8_t prev_cb_timeout_count;
+  std::uint32_t prev_txdone_sum_us;
+  std::uint32_t crc;
+};
+
+struct PendingDiag {
+  std::uint8_t valid{0};
+  std::uint8_t tx_cb_total{0};
+  std::uint8_t tx_cb_success{0};
+  std::uint8_t tx_cb_failed{0};
+  std::uint8_t first_status{0xff};
+  std::uint8_t cb_timeout{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t reconnect_count{0};
+  std::int8_t rssi{0};
+  std::uint8_t actual_channel{0};
+};
+
+#if defined(ESP_PLATFORM)
+RTC_DATA_ATTR static RtcState g_rtc{};
+RTC_DATA_ATTR static prepared_send::PreparedWifiRtcCache g_rtc_wifi_cache{};
+RTC_DATA_ATTR static PendingDiag g_pending_diag{};
+
+static const auto kWifiInit = ae::WiFiInit{
+    std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}},
+    {},
+};
+
+static bool g_had_aether_app = false;
+static std::shared_ptr g_app;
+static ae::Client::ptr g_client;
+static std::unique_ptr g_stream;
+static ae::Subscription g_select_sub;
+static ae::Subscription g_stream_sub;
+static ae::Subscription g_write_sub;
+
+static bool g_write_armed = false;
+static bool g_write_ok = false;
+static bool g_exit_success = false;
+static bool g_pending_register_finish = false;
+static bool g_pending_full_post_write = false;
+static bool g_pending_final_exit = false;
+static bool g_done = false;
+
+static ExperimentEarlyEntrySnapshot g_early{};
+static std::uint32_t g_sleep_elapsed_us = 0;
+static std::uint32_t g_sleep_overhead_us = 0;
+static prepared_send::FastPathConfig g_cfg{};
+static prepared_send::BisectWifiCacheSnapshot g_wifi_snapshot{};
+
+static std::uint32_t Crc32Bytes(void const* data, std::size_t len) {
+  auto const* p = static_cast(data);
+  std::uint32_t crc = 0xffffffffu;
+  for (std::size_t i = 0; i < len; ++i) {
+    crc ^= p[i];
+    for (int b = 0; b < 8; ++b) {
+      std::uint32_t const mask = -(crc & 1u);
+      crc = (crc >> 1) ^ (0xedb88320u & mask);
+    }
+  }
+  return ~crc;
+}
+
+static std::uint32_t ComputeCrc(RtcState const& st) {
+  RtcState tmp = st;
+  tmp.crc = 0;
+  return Crc32Bytes(&tmp, sizeof(tmp));
+}
+
+static void SetCrc(RtcState& st) { st.crc = ComputeCrc(st); }
+
+static bool ValidateRtcState(RtcState const& st) {
+  if (st.magic != kRtcMagic || st.version != kRtcVersion) {
+    return false;
+  }
+  if (ComputeCrc(st) != st.crc) {
+    return false;
+  }
+  if (st.phase > static_cast(Phase::kDone)) {
+    return false;
+  }
+  if (st.variant_id >= kVariantCount &&
+      st.phase != static_cast(Phase::kFinal) &&
+      st.phase != static_cast(Phase::kDone)) {
+    return false;
+  }
+  if (st.hot_index > kHotPerVariant) {
+    return false;
+  }
+  return true;
+}
+
+static void ClearPending(RtcState& st) {
+  st.pending_valid = 0;
+  st.pending_kind = 0;
+  st.pending_variant = 0;
+  st.pending_hot_index = 0;
+  st.pending_user_cycle_us = 0;
+  st.pending_wifi_cycle_us = 0;
+  st.pending_wifi_init_us = 0;
+  st.pending_connect_us = 0;
+  st.pending_encode_us = 0;
+  st.pending_txdone_us = 0;
+  st.pending_teardown_us = 0;
+  st.pending_heap_before = 0;
+  st.pending_heap_after = 0;
+  st.pending_cb_seen = 0;
+  st.pending_cb_timeout = 0;
+  st.pending_auth = 0;
+  g_pending_diag = PendingDiag{};
+}
+
+static void InitRtcFresh(Phase phase) {
+  g_rtc = RtcState{};
+  g_rtc.magic = kRtcMagic;
+  g_rtc.version = kRtcVersion;
+  g_rtc.phase = static_cast(phase);
+  g_rtc.variant_id = 0;
+  g_rtc.hot_index = 1;
+  g_rtc.next_record_id = 1;
+  g_rtc.prev_variant_id = 0xff;
+  ClearPending(g_rtc);
+  SetCrc(g_rtc);
+}
+
+[[noreturn]] static void PrepareRtcStateAndDeepSleep(std::uint32_t requested_us) {
+  g_rtc.requested_sleep_us = requested_us;
+  esp_sleep_enable_timer_wakeup(requested_us);
+  g_rtc.sleep_arm_rtc_us = esp_rtc_get_time_us();
+  SetCrc(g_rtc);
+#  if SOC_PM_SUPPORT_RTC_SLOW_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
+#  endif
+#  if SOC_PM_SUPPORT_RTC_FAST_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
+#  endif
+  (void)esp_deep_sleep_try_to_start();
+  esp_deep_sleep_start();
+  for (;;) {
+  }
+}
+
+static void ForceFullRecovery() {
+  ClearPending(g_rtc);
+  g_rtc.phase = static_cast(Phase::kFull);
+  if (g_rtc.variant_id >= kVariantCount) {
+    g_rtc.variant_id = 0;
+  }
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  g_rtc.var_tx_success = 0;
+  g_rtc.var_tx_fail = 0;
+  g_rtc.var_cb_timeout = 0;
+  g_rtc.var_txdone_sum_us = 0;
+  SetCrc(g_rtc);
+}
+
+static void SnapshotPrevVariant() {
+  g_rtc.prev_variant_id = g_rtc.variant_id;
+  g_rtc.prev_hot_send_count = g_rtc.hot_send_count;
+  g_rtc.prev_hot_attempt_count = g_rtc.hot_attempt_count;
+  g_rtc.prev_tx_success_count = g_rtc.var_tx_success;
+  g_rtc.prev_tx_fail_count = g_rtc.var_tx_fail;
+  g_rtc.prev_cb_timeout_count = g_rtc.var_cb_timeout;
+  g_rtc.prev_txdone_sum_us = g_rtc.var_txdone_sum_us;
+}
+
+static void AdvanceToNextVariantOrFinal() {
+  SnapshotPrevVariant();
+  if (g_rtc.variant_id + 1 < kVariantCount) {
+    ++g_rtc.variant_id;
+    g_rtc.phase = static_cast(Phase::kFull);
+    g_rtc.hot_index = 1;
+    g_rtc.hot_attempt_count = 0;
+    g_rtc.hot_send_count = 0;
+    g_rtc.var_tx_success = 0;
+    g_rtc.var_tx_fail = 0;
+    g_rtc.var_cb_timeout = 0;
+    g_rtc.var_txdone_sum_us = 0;
+  } else {
+    g_rtc.phase = static_cast(Phase::kFinal);
+    g_rtc.hot_index = 1;
+  }
+  SetCrc(g_rtc);
+}
+
+static std::uint16_t NextSeq() {
+  ++g_rtc.sequence_global;
+  return g_rtc.sequence_global;
+}
+
+static void AdvanceRecordIdAfterFlush() {
+  if (g_rtc.next_record_id < 0xffffu) {
+    ++g_rtc.next_record_id;
+  }
+}
+
+static VariantCfg const& CurrentVariant() {
+  auto id = g_rtc.variant_id;
+  if (id >= kVariantCount) {
+    id = 0;
+  }
+  return kVariants[id];
+}
+
+static std::uint8_t SettingFlags(VariantCfg const& v) {
+  std::uint8_t f = 0;
+  if (v.storage_ram) {
+    f |= 1;
+  }
+  if (!v.nvs_enable) {
+    f |= 2;
+  }
+  if (v.force_ht20) {
+    f |= 4;
+  }
+  if (v.dynamic_cs == 0) {
+    f |= 8;
+  }
+  if (v.dynamic_cs == 1) {
+    f |= 16;
+  }
+  if (v.dynamic_tx != 0) {
+    f |= 32;
+  }
+  if (v.static_rx != 0 || v.dynamic_rx != 0) {
+    f |= 64;
+  }
+  return f;
+}
+
+static ae::DataBuffer MakePayload(bench::BootWifiOptMsgType type) {
+  bench::BootWifiOptPayload p{};
+  p.type = static_cast(type);
+  p.variant_id = g_rtc.variant_id;
+  p.hot_index = g_rtc.hot_index;
+  p.sequence_global = NextSeq();
+  p.record_id = g_rtc.pending_valid ? g_rtc.next_record_id : 0;
+  auto const& v = CurrentVariant();
+  p.reset_reason = g_early.reset_reason;
+  p.wake_cause = g_early.wakeup_cause;
+  p.brownout_count = g_rtc.brownout_count;
+  p.sleep_elapsed_to_app_us = g_sleep_elapsed_us;
+  p.sleep_to_app_overhead_us = g_sleep_overhead_us;
+  p.app_entry_esp_timer_us = g_early.app_entry_esp_timer_us;
+  p.setting_flags = SettingFlags(v);
+  p.static_rx_buf = v.static_rx;
+  p.dynamic_rx_buf = v.dynamic_rx;
+  p.dynamic_tx_buf = v.dynamic_tx;
+  p.dynamic_cs = v.dynamic_cs;
+  std::uint8_t flags = 0;
+  if (g_rtc.current_boot_brownout) {
+    flags |= 1;
+  }
+  if (g_rtc.pending_cb_seen) {
+    flags |= 2;
+  }
+  if (g_rtc.pending_cb_timeout) {
+    flags |= 4;
+  }
+  p.flags = flags;
+  p.prev_variant_id = g_rtc.prev_variant_id;
+  p.prev_hot_send_count = g_rtc.prev_hot_send_count;
+  p.prev_hot_attempt_count = g_rtc.prev_hot_attempt_count;
+  p.prev_tx_success_count = g_rtc.prev_tx_success_count;
+  p.prev_tx_fail_count = g_rtc.prev_tx_fail_count;
+  p.prev_cb_timeout_count = g_rtc.prev_cb_timeout_count;
+  p.prev_txdone_sum_us = g_rtc.prev_txdone_sum_us;
+  p.prepared_message_left = static_cast(
+      prepared_send::PreparedMessageLeft() > 0xffffu
+          ? 0xffffu
+          : prepared_send::PreparedMessageLeft());
+
+  if (g_rtc.pending_valid) {
+    p.pending_kind = g_rtc.pending_kind;
+    p.pending_variant = g_rtc.pending_variant;
+    p.pending_hot_index = g_rtc.pending_hot_index;
+    p.pending_user_cycle_us = g_rtc.pending_user_cycle_us;
+    p.pending_wifi_cycle_us = g_rtc.pending_wifi_cycle_us;
+    p.wifi_init_us = g_rtc.pending_wifi_init_us;
+    p.connect_us = g_rtc.pending_connect_us;
+    p.encode_send_us = g_rtc.pending_encode_us;
+    p.tx_done_wait_us = g_rtc.pending_txdone_us;
+    p.teardown_us = g_rtc.pending_teardown_us;
+    p.heap_before_wifi = g_rtc.pending_heap_before;
+    p.heap_after_wifi = g_rtc.pending_heap_after;
+    p.authmode = g_rtc.pending_auth;
+    if (g_pending_diag.valid) {
+      p.tx_cb_total = g_pending_diag.tx_cb_total;
+      p.tx_cb_success = g_pending_diag.tx_cb_success;
+      p.tx_cb_failed = g_pending_diag.tx_cb_failed;
+      p.first_status = g_pending_diag.first_status;
+      p.cb_timeout = g_pending_diag.cb_timeout;
+      p.rssi = g_pending_diag.rssi;
+      p.actual_channel = g_pending_diag.actual_channel;
+      p.disconnect_count = g_pending_diag.disconnect_count;
+      p.reconnect_count = g_pending_diag.reconnect_count;
+    }
+  }
+  return bench::EncodeBootWifiOpt(p);
+}
+
+static std::uint32_t UserCycleFromAppEntry() {
+  auto const now = esp_timer_get_time();
+  auto const entry = g_early.app_entry_esp_timer_us;
+  if (now < entry) {
+    return 0;
+  }
+  auto const delta = now - entry;
+  return delta > 0xffffffffll ? 0xffffffffu
+                              : static_cast(delta);
+}
+
+static void StorePendingHot(prepared_send::FastSendResult const& result,
+                            std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = 2;
+  g_rtc.pending_variant = g_rtc.variant_id;
+  g_rtc.pending_hot_index = g_rtc.hot_index;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = result.cycle_us;
+  g_rtc.pending_wifi_init_us = result.wifi_init_us;
+  g_rtc.pending_connect_us = result.connect_us;
+  g_rtc.pending_encode_us = result.encode_send_us;
+  g_rtc.pending_txdone_us = result.tx_done_wait_us;
+  g_rtc.pending_teardown_us = result.teardown_us;
+  g_rtc.pending_heap_before = result.heap_before_wifi;
+  g_rtc.pending_heap_after = result.heap_after_wifi;
+  g_rtc.pending_cb_seen = result.cb_any;
+  g_rtc.pending_cb_timeout = result.cb_timeout;
+  g_rtc.pending_auth = result.negotiated_auth;
+  g_pending_diag = PendingDiag{};
+  g_pending_diag.valid = 1;
+  g_pending_diag.tx_cb_total = result.tx_cb_total;
+  g_pending_diag.tx_cb_success = result.tx_cb_success;
+  g_pending_diag.tx_cb_failed = result.tx_cb_failed;
+  g_pending_diag.first_status = result.first_status;
+  g_pending_diag.cb_timeout = result.cb_timeout;
+  g_pending_diag.rssi = result.rssi;
+  g_pending_diag.actual_channel = result.actual_channel;
+  g_pending_diag.disconnect_count = result.disconnect_count;
+  g_pending_diag.reconnect_count = result.reconnect_count;
+}
+
+static void StorePendingFull(std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = 1;
+  g_rtc.pending_variant = g_rtc.variant_id;
+  g_rtc.pending_hot_index = 0;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = user_cycle_us;
+  g_pending_diag = PendingDiag{};
+}
+
+static void ReleaseApp() {
+  g_select_sub.Reset();
+  g_stream_sub.Reset();
+  g_write_sub.Reset();
+  g_stream.reset();
+  g_client = {};
+  g_app.reset();
+}
+
+static void PreConstructCleanup() {
+  if (!g_had_aether_app) {
+    return;
+  }
+#  if !AE_WIFI_USE_FULL_DEINIT
+  esp_netif_deinit();
+  esp_event_loop_delete_default();
+#  endif
+}
+
+static void ConstructAether() {
+  PreConstructCleanup();
+  g_had_aether_app = true;
+  g_app = ae::AetherApp::Construct(
+      ae::AetherAppContext{}
+#  if AE_DISTILLATION
+          .AddAdapterFactory([&](ae::AetherAppContext const& ctx) {
+            return ae::WifiAdapter::ptr::Create(
+                ae::CreateWith{ctx.domain()}.with_id(
+                    ae::GlobalId::kWiFiAdapter),
+                ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit);
+          })
+#  endif
+  );
+}
+
+static prepared_send::FastPathConfig MakeFastConfig() {
+  prepared_send::FastPathConfig c{};
+  c.use_bssid = false;
+  c.use_channel = true;
+  c.use_fast_scan = false;
+  c.use_static_ip = true;
+  c.use_static_arp = true;
+  c.ampdu_tx_off = false;
+  c.ampdu_rx_off = false;
+  c.amsdu_tx_off = false;
+  c.auth = prepared_send::FastAuthMode::kWpa2;
+  c.retry_max = 10;
+  c.pre_delay_ms = 25;
+  c.post_delay_ms = 0;
+  c.post_mode = prepared_send::FastPostMode::kTxDoneCb;
+  c.tx_done_wait = prepared_send::FastTxDoneWaitMode::kFirstAny;
+  c.set_mac_retry_limit = false;
+  auto const& v = CurrentVariant();
+  c.wifi_storage_ram = v.storage_ram;
+  c.wifi_nvs_enable = v.nvs_enable;
+  c.force_ht20 = v.force_ht20;
+  c.dynamic_cs = v.dynamic_cs;
+  c.static_rx_buf_num = v.static_rx;
+  c.dynamic_rx_buf_num = v.dynamic_rx;
+  c.dynamic_tx_buf_num = v.dynamic_tx;
+  return c;
+}
+
+static void DoFullWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto payload = MakePayload(bench::BootWifiOptMsgType::kFull);
+  auto& wa = g_stream->Write(std::move(payload));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_full_post_write = true;
+  });
+}
+
+static void MaybeFullWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFullWrite();
+}
+
+static void OnFullClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); });
+  MaybeFullWrite();
+}
+
+static void StartRegister() {
+  g_write_armed = false;
+  g_pending_register_finish = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       g_pending_register_finish = true;
+                     });
+}
+
+static void StartFull() {
+  g_write_armed = false;
+  g_write_ok = false;
+  g_pending_full_post_write = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFullClientReady(std::move(res).value());
+                     });
+}
+
+static void StartFinal() {
+  g_write_armed = false;
+  g_write_ok = false;
+  g_pending_final_exit = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       auto client = g_client.Load();
+                       g_stream = std::make_unique(
+                           *g_app, client, kServiceUid, ae::P2pPortHandle{});
+                       g_stream_sub = g_stream->stream_update_event().Subscribe(
+                           []() {
+                             if (!g_stream || g_write_armed) {
+                               return;
+                             }
+                             if (!g_stream->stream_info().is_writable) {
+                               return;
+                             }
+                             g_write_armed = true;
+                             auto& wa = g_stream->Write(
+                                 MakePayload(bench::BootWifiOptMsgType::kFinal));
+                             g_write_sub = wa.status_event().Subscribe(
+                                 [](ae::WriteAction::Status st) {
+                                   g_write_ok =
+                                       (st == ae::WriteAction::Status::kSuccess);
+                                   g_pending_final_exit = true;
+                                 });
+                           });
+                     });
+}
+
+static void FinishRegisterInLoop() {
+  auto client = g_client.Load();
+  if (!client) {
+    g_app->Exit(1);
+    return;
+  }
+  g_app->aether().Save();
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFullPostWriteInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  bool captured = false;
+  for (int i = 0; i < 10 && !captured; ++i) {
+    captured = prepared_send::CapturePreparedWifiRtcCache(&g_rtc_wifi_cache);
+    if (!captured) {
+      vTaskDelay(pdMS_TO_TICKS(200));
+    }
+  }
+  bool exported = false;
+  for (std::size_t n : {std::size_t{40}, std::size_t{35}, std::size_t{30}}) {
+    if (prepared_send::ExportPreparedSendBlock(g_client, kServiceUid, n)) {
+      exported = true;
+      break;
+    }
+  }
+  if (!exported || !captured || !prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() == 0) {
+    g_app->Exit(1);
+    return;
+  }
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFinalInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void AfterRegisterComplete() {
+  ReleaseApp();
+  g_rtc.registered = 1;
+  g_rtc.phase = static_cast(Phase::kFull);
+  g_rtc.variant_id = 0;
+  g_rtc.hot_index = 1;
+  ClearPending(g_rtc);
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFullComplete() {
+  ReleaseApp();
+  prepared_send::ReleaseFullAetherWifiForHotPath();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  StorePendingFull(UserCycleFromAppEntry());
+  g_rtc.phase = static_cast(Phase::kHot);
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  g_rtc.var_tx_success = 0;
+  g_rtc.var_tx_fail = 0;
+  g_rtc.var_cb_timeout = 0;
+  g_rtc.var_txdone_sum_us = 0;
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalComplete() {
+  ReleaseApp();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  ClearPending(g_rtc);
+  g_rtc.phase = static_cast(Phase::kDone);
+  SetCrc(g_rtc);
+  g_done = true;
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalFailed() {
+  ReleaseApp();
+  if (g_rtc.final_fail_count < 255) {
+    ++g_rtc.final_fail_count;
+  }
+  if (g_rtc.final_fail_count >= 3) {
+    ClearPending(g_rtc);
+    g_rtc.phase = static_cast(Phase::kDone);
+    SetCrc(g_rtc);
+    g_done = true;
+  }
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void RunHotOnce() {
+  if (!prepared_send::PreparedWifiRtcCacheIsValid(g_rtc_wifi_cache) ||
+      !prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() == 0) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (g_rtc.hot_attempt_count >= kMaxHotAttempts) {
+    AdvanceToNextVariantOrFinal();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (g_rtc.hot_attempt_count < 255) {
+    ++g_rtc.hot_attempt_count;
+  }
+  SetCrc(g_rtc);
+
+  g_cfg = MakeFastConfig();
+  g_wifi_snapshot =
+      prepared_send::SnapshotFromPreparedWifiRtcCache(g_rtc_wifi_cache);
+  auto payload = MakePayload(bench::BootWifiOptMsgType::kHot);
+  auto const result =
+      prepared_send::SendPreparedOnceWithFastPath(g_cfg, payload,
+                                                    &g_wifi_snapshot);
+
+  if (result.status == prepared_send::HotSendStatus::kWifiFailed) {
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (result.status != prepared_send::HotSendStatus::kSent) {
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  auto const user_cycle = UserCycleFromAppEntry();
+  bool const flushed_prior = g_rtc.pending_valid != 0;
+  StorePendingHot(result, user_cycle);
+  if (flushed_prior) {
+    AdvanceRecordIdAfterFlush();
+  }
+
+  if (g_rtc.hot_send_count < 255) {
+    ++g_rtc.hot_send_count;
+  }
+  if (result.first_status == 1) {
+    if (g_rtc.var_tx_success < 255) {
+      ++g_rtc.var_tx_success;
+    }
+  } else if (result.first_status == 0) {
+    if (g_rtc.var_tx_fail < 255) {
+      ++g_rtc.var_tx_fail;
+    }
+  }
+  if (result.cb_timeout) {
+    if (g_rtc.var_cb_timeout < 255) {
+      ++g_rtc.var_cb_timeout;
+    }
+  }
+  g_rtc.var_txdone_sum_us += result.tx_done_wait_us;
+
+  if (g_rtc.hot_index < 255) {
+    ++g_rtc.hot_index;
+  }
+
+  if (g_rtc.hot_send_count >= kHotPerVariant ||
+      g_rtc.hot_attempt_count >= kMaxHotAttempts) {
+    AdvanceToNextVariantOrFinal();
+  }
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void PrepareRtcOnBoot() {
+  g_early = GetExperimentEarlyEntrySnapshot();
+  g_sleep_elapsed_us = 0;
+  g_sleep_overhead_us = 0;
+  if (g_early.valid && g_rtc.sleep_arm_rtc_us != 0 &&
+      g_early.app_entry_rtc_us >= g_rtc.sleep_arm_rtc_us) {
+    auto const elapsed = g_early.app_entry_rtc_us - g_rtc.sleep_arm_rtc_us;
+    g_sleep_elapsed_us =
+        elapsed > 0xffffffffull ? 0xffffffffu : static_cast(elapsed);
+    if (g_sleep_elapsed_us > g_rtc.requested_sleep_us) {
+      g_sleep_overhead_us = g_sleep_elapsed_us - g_rtc.requested_sleep_us;
+    }
+  }
+
+  auto const reset =
+      static_cast(g_early.reset_reason);
+  bool const valid = ValidateRtcState(g_rtc);
+  g_rtc.current_boot_brownout = 0;
+
+  if (reset == ESP_RST_BROWNOUT) {
+    if (valid) {
+      if (g_rtc.brownout_count < 255) {
+        ++g_rtc.brownout_count;
+      }
+      ClearPending(g_rtc);
+    } else {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.brownout_count = 1;
+    }
+    g_rtc.current_boot_brownout = 1;
+    ForceFullRecovery();
+  } else if (!g_early.valid || reset != ESP_RST_DEEPSLEEP || !valid) {
+    bool const first_poweron = (reset == ESP_RST_POWERON);
+    if (first_poweron && (!valid || !g_rtc.registered)) {
+      InitRtcFresh(Phase::kRegister);
+    } else if (!valid) {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.registered = 1;
+      SetCrc(g_rtc);
+    } else {
+      if (g_rtc.unexpected_reset_count < 255) {
+        ++g_rtc.unexpected_reset_count;
+      }
+      ForceFullRecovery();
+    }
+  }
+  SetCrc(g_rtc);
+}
+
+#endif  // ESP_PLATFORM
+
+}  // namespace
+}  // namespace temp_sensor
+
+#if defined(ESP_PLATFORM)
+
+void setup() {
+  using namespace temp_sensor;
+  nvs_flash_init();
+  g_done = false;
+  g_pending_register_finish = false;
+  g_pending_full_post_write = false;
+  g_pending_final_exit = false;
+  PrepareRtcOnBoot();
+  g_cfg = MakeFastConfig();
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kDone) {
+    g_done = true;
+    return;
+  }
+  if (phase == Phase::kRegister) {
+    StartRegister();
+    return;
+  }
+  if (phase == Phase::kFull) {
+    StartFull();
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    StartFinal();
+    return;
+  }
+}
+
+void loop() {
+  using namespace temp_sensor;
+  if (g_done) {
+    vTaskDelay(pdMS_TO_TICKS(1000));
+    return;
+  }
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kHot) {
+    RunHotOnce();
+    return;
+  }
+
+  auto process_deferred = []() {
+    if (g_app && g_pending_register_finish) {
+      g_pending_register_finish = false;
+      FinishRegisterInLoop();
+      return true;
+    }
+    if (g_app && g_pending_full_post_write) {
+      g_pending_full_post_write = false;
+      FinishFullPostWriteInLoop();
+      return true;
+    }
+    if (g_app && g_pending_final_exit) {
+      g_pending_final_exit = false;
+      FinishFinalInLoop();
+      return true;
+    }
+    return false;
+  };
+
+  if (process_deferred()) {
+    return;
+  }
+  if (!g_app) {
+    return;
+  }
+  if (!g_app->IsExited()) {
+    auto t = g_app->Update(ae::Now());
+    if (process_deferred()) {
+      return;
+    }
+    if (!g_app->IsExited()) {
+      g_app->WaitUntil(t);
+    }
+    return;
+  }
+
+  if (phase == Phase::kRegister) {
+    if (g_exit_success) {
+      AfterRegisterComplete();
+    } else {
+      ReleaseApp();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFull) {
+    if (g_exit_success) {
+      AfterFullComplete();
+    } else {
+      ReleaseApp();
+      ForceFullRecovery();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    if (g_exit_success) {
+      AfterFinalComplete();
+    } else {
+      AfterFinalFailed();
+    }
+    return;
+  }
+}
+
+#else
+
+void setup() {}
+void loop() {}
+
+#endif
diff --git a/main/prepared_boot_wifi_val100_bench.cpp b/main/prepared_boot_wifi_val100_bench.cpp
new file mode 100644
index 0000000..eb6e10f
--- /dev/null
+++ b/main/prepared_boot_wifi_val100_bench.cpp
@@ -0,0 +1,946 @@
+/*
+ * Copyright 2026 Aethernet Inc.
+ *
+ * VAL100: combined boot/wifi HOT winners from prepared_boot_wifi_opt campaign.
+ * D1 STORAGE_RAM + G1 HT20 + H1 dynamic_cs=false + E3 RX half.
+ * 100 HOT; FULL once; 3 s deep sleep. Payload 0xD8.
+ */
+
+#include 
+#include 
+#include 
+
+#include "aether/all.h"
+#include "aether/ae_exp_wifi.h"
+#include "aether/config.h"
+#include "aether/env.h"
+#include "bench_payload.h"
+#include "experiment_early_entry.h"
+#include "prepared_send/prepared_send.h"
+
+#if defined(ESP_PLATFORM)
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#endif
+
+using namespace std::chrono_literals;
+
+#if defined(ESP_PLATFORM)
+extern "C" std::uint64_t esp_rtc_get_time_us(void);
+#endif
+
+namespace temp_sensor {
+namespace {
+
+static constexpr auto kParentUid =
+    ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165");
+
+#ifndef BENCH_CLIENT_ID
+#  define BENCH_CLIENT_ID "prepared_deepsleep_5x50_v1"
+#endif
+static constexpr char const* kBenchClientId = BENCH_CLIENT_ID;
+
+#if defined(SERVICE_UID)
+static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID);
+#else
+static constexpr auto kServiceUid =
+    ae::Uid::FromString("5aade50f-00d9-4624-b097-e203cdcf1e38");
+#endif
+
+static constexpr std::uint8_t kVariantCount = 1;
+static constexpr std::uint8_t kHotPerVariant = 100;
+static constexpr std::uint8_t kMaxHotAttempts = 120;
+static constexpr std::uint32_t kSleepUs = 3000000;
+static constexpr std::uint32_t kRtcMagic = 0x42575631u;  // "BWV1"
+static constexpr std::uint16_t kRtcVersion = 1;
+
+enum class Phase : std::uint16_t {
+  kRegister = 0,
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+  kDone = 4,
+};
+
+// Combined winner: STORAGE_RAM + HT20 + CS off + RX half.
+static constexpr bool kStorageRam = true;
+static constexpr bool kNvsEnable = true;
+static constexpr bool kForceHt20 = true;
+static constexpr std::int8_t kDynamicCs = 0;
+static constexpr std::uint8_t kStaticRx = 5;
+static constexpr std::uint8_t kDynamicRx = 16;
+static constexpr std::uint8_t kDynamicTx = 0;
+
+struct RtcState {
+  std::uint32_t magic;
+  std::uint16_t version;
+  std::uint16_t phase;
+  std::uint8_t variant_id;
+  std::uint8_t hot_index;
+  std::uint8_t hot_attempt_count;
+  std::uint8_t hot_send_count;
+  std::uint16_t sequence_global;
+  std::uint16_t next_record_id;
+  std::uint32_t requested_sleep_us;
+  std::uint64_t sleep_arm_rtc_us;
+  std::uint8_t pending_valid;
+  std::uint8_t pending_kind;
+  std::uint8_t pending_variant;
+  std::uint8_t pending_hot_index;
+  std::uint32_t pending_user_cycle_us;
+  std::uint32_t pending_wifi_cycle_us;
+  std::uint32_t pending_wifi_init_us;
+  std::uint32_t pending_connect_us;
+  std::uint32_t pending_encode_us;
+  std::uint32_t pending_txdone_us;
+  std::uint32_t pending_teardown_us;
+  std::uint32_t pending_heap_before;
+  std::uint32_t pending_heap_after;
+  std::uint8_t pending_cb_seen;
+  std::uint8_t pending_cb_timeout;
+  std::uint8_t pending_auth;
+  std::uint8_t brownout_count;
+  std::uint8_t unexpected_reset_count;
+  std::uint8_t current_boot_brownout;
+  std::uint8_t registered;
+  std::uint8_t final_fail_count;
+  std::uint8_t var_tx_success;
+  std::uint8_t var_tx_fail;
+  std::uint8_t var_cb_timeout;
+  std::uint8_t pad0;
+  std::uint32_t var_txdone_sum_us;
+  std::uint8_t prev_variant_id;
+  std::uint8_t prev_hot_send_count;
+  std::uint8_t prev_hot_attempt_count;
+  std::uint8_t prev_tx_success_count;
+  std::uint8_t prev_tx_fail_count;
+  std::uint8_t prev_cb_timeout_count;
+  std::uint32_t prev_txdone_sum_us;
+  std::uint32_t crc;
+};
+
+struct PendingDiag {
+  std::uint8_t valid{0};
+  std::uint8_t tx_cb_total{0};
+  std::uint8_t tx_cb_success{0};
+  std::uint8_t tx_cb_failed{0};
+  std::uint8_t first_status{0xff};
+  std::uint8_t cb_timeout{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t reconnect_count{0};
+  std::int8_t rssi{0};
+  std::uint8_t actual_channel{0};
+};
+
+#if defined(ESP_PLATFORM)
+RTC_DATA_ATTR static RtcState g_rtc{};
+RTC_DATA_ATTR static prepared_send::PreparedWifiRtcCache g_rtc_wifi_cache{};
+RTC_DATA_ATTR static PendingDiag g_pending_diag{};
+
+static const auto kWifiInit = ae::WiFiInit{
+    std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}},
+    {},
+};
+
+static bool g_had_aether_app = false;
+static std::shared_ptr g_app;
+static ae::Client::ptr g_client;
+static std::unique_ptr g_stream;
+static ae::Subscription g_select_sub;
+static ae::Subscription g_stream_sub;
+static ae::Subscription g_write_sub;
+
+static bool g_write_armed = false;
+static bool g_write_ok = false;
+static bool g_exit_success = false;
+static bool g_pending_register_finish = false;
+static bool g_pending_full_post_write = false;
+static bool g_pending_final_exit = false;
+static bool g_done = false;
+
+static ExperimentEarlyEntrySnapshot g_early{};
+static std::uint32_t g_sleep_elapsed_us = 0;
+static std::uint32_t g_sleep_overhead_us = 0;
+static prepared_send::FastPathConfig g_cfg{};
+static prepared_send::BisectWifiCacheSnapshot g_wifi_snapshot{};
+
+static std::uint32_t Crc32Bytes(void const* data, std::size_t len) {
+  auto const* p = static_cast(data);
+  std::uint32_t crc = 0xffffffffu;
+  for (std::size_t i = 0; i < len; ++i) {
+    crc ^= p[i];
+    for (int b = 0; b < 8; ++b) {
+      std::uint32_t const mask = -(crc & 1u);
+      crc = (crc >> 1) ^ (0xedb88320u & mask);
+    }
+  }
+  return ~crc;
+}
+
+static std::uint32_t ComputeCrc(RtcState const& st) {
+  RtcState tmp = st;
+  tmp.crc = 0;
+  return Crc32Bytes(&tmp, sizeof(tmp));
+}
+
+static void SetCrc(RtcState& st) { st.crc = ComputeCrc(st); }
+
+static bool ValidateRtcState(RtcState const& st) {
+  if (st.magic != kRtcMagic || st.version != kRtcVersion) {
+    return false;
+  }
+  if (ComputeCrc(st) != st.crc) {
+    return false;
+  }
+  if (st.phase > static_cast(Phase::kDone)) {
+    return false;
+  }
+  if (st.variant_id >= kVariantCount &&
+      st.phase != static_cast(Phase::kFinal) &&
+      st.phase != static_cast(Phase::kDone)) {
+    return false;
+  }
+  if (st.hot_index > kHotPerVariant) {
+    return false;
+  }
+  return true;
+}
+
+static void ClearPending(RtcState& st) {
+  st.pending_valid = 0;
+  st.pending_kind = 0;
+  st.pending_variant = 0;
+  st.pending_hot_index = 0;
+  st.pending_user_cycle_us = 0;
+  st.pending_wifi_cycle_us = 0;
+  st.pending_wifi_init_us = 0;
+  st.pending_connect_us = 0;
+  st.pending_encode_us = 0;
+  st.pending_txdone_us = 0;
+  st.pending_teardown_us = 0;
+  st.pending_heap_before = 0;
+  st.pending_heap_after = 0;
+  st.pending_cb_seen = 0;
+  st.pending_cb_timeout = 0;
+  st.pending_auth = 0;
+  g_pending_diag = PendingDiag{};
+}
+
+static void InitRtcFresh(Phase phase) {
+  g_rtc = RtcState{};
+  g_rtc.magic = kRtcMagic;
+  g_rtc.version = kRtcVersion;
+  g_rtc.phase = static_cast(phase);
+  g_rtc.variant_id = 0;
+  g_rtc.hot_index = 1;
+  g_rtc.next_record_id = 1;
+  g_rtc.prev_variant_id = 0xff;
+  ClearPending(g_rtc);
+  SetCrc(g_rtc);
+}
+
+[[noreturn]] static void PrepareRtcStateAndDeepSleep(std::uint32_t requested_us) {
+  g_rtc.requested_sleep_us = requested_us;
+  esp_sleep_enable_timer_wakeup(requested_us);
+  g_rtc.sleep_arm_rtc_us = esp_rtc_get_time_us();
+  SetCrc(g_rtc);
+#  if SOC_PM_SUPPORT_RTC_SLOW_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
+#  endif
+#  if SOC_PM_SUPPORT_RTC_FAST_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
+#  endif
+  (void)esp_deep_sleep_try_to_start();
+  esp_deep_sleep_start();
+  for (;;) {
+  }
+}
+
+static void ForceFullRecovery() {
+  ClearPending(g_rtc);
+  g_rtc.phase = static_cast(Phase::kFull);
+  g_rtc.variant_id = 0;
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  g_rtc.var_tx_success = 0;
+  g_rtc.var_tx_fail = 0;
+  g_rtc.var_cb_timeout = 0;
+  g_rtc.var_txdone_sum_us = 0;
+  SetCrc(g_rtc);
+}
+
+static void SnapshotPrevVariant() {
+  g_rtc.prev_variant_id = g_rtc.variant_id;
+  g_rtc.prev_hot_send_count = g_rtc.hot_send_count;
+  g_rtc.prev_hot_attempt_count = g_rtc.hot_attempt_count;
+  g_rtc.prev_tx_success_count = g_rtc.var_tx_success;
+  g_rtc.prev_tx_fail_count = g_rtc.var_tx_fail;
+  g_rtc.prev_cb_timeout_count = g_rtc.var_cb_timeout;
+  g_rtc.prev_txdone_sum_us = g_rtc.var_txdone_sum_us;
+}
+
+static void AdvanceToNextVariantOrFinal() {
+  SnapshotPrevVariant();
+  g_rtc.phase = static_cast(Phase::kFinal);
+  g_rtc.hot_index = 1;
+  SetCrc(g_rtc);
+}
+
+static std::uint16_t NextSeq() {
+  ++g_rtc.sequence_global;
+  return g_rtc.sequence_global;
+}
+
+static void AdvanceRecordIdAfterFlush() {
+  if (g_rtc.next_record_id < 0xffffu) {
+    ++g_rtc.next_record_id;
+  }
+}
+
+static ae::DataBuffer MakePayload(bench::BootWifiOptMsgType type) {
+  bench::BootWifiOptPayload p{};
+  p.type = static_cast(type);
+  p.variant_id = 0xff;  // VAL100 marker
+  p.hot_index = g_rtc.hot_index;
+  p.sequence_global = NextSeq();
+  p.record_id = g_rtc.pending_valid ? g_rtc.next_record_id : 0;
+  p.reset_reason = g_early.reset_reason;
+  p.wake_cause = g_early.wakeup_cause;
+  p.brownout_count = g_rtc.brownout_count;
+  p.sleep_elapsed_to_app_us = g_sleep_elapsed_us;
+  p.sleep_to_app_overhead_us = g_sleep_overhead_us;
+  p.app_entry_esp_timer_us = g_early.app_entry_esp_timer_us;
+  p.setting_flags = static_cast(
+      (kStorageRam ? 1 : 0) | (kNvsEnable ? 0 : 2) | (kForceHt20 ? 4 : 0) |
+      (kDynamicCs == 0 ? 8 : 0) | (kDynamicCs == 1 ? 16 : 0) | 64);
+  p.static_rx_buf = kStaticRx;
+  p.dynamic_rx_buf = kDynamicRx;
+  p.dynamic_tx_buf = kDynamicTx;
+  p.dynamic_cs = kDynamicCs;
+  std::uint8_t flags = 0;
+  if (g_rtc.current_boot_brownout) {
+    flags |= 1;
+  }
+  if (g_rtc.pending_cb_seen) {
+    flags |= 2;
+  }
+  if (g_rtc.pending_cb_timeout) {
+    flags |= 4;
+  }
+  p.flags = flags;
+  p.prev_variant_id = g_rtc.prev_variant_id;
+  p.prev_hot_send_count = g_rtc.prev_hot_send_count;
+  p.prev_hot_attempt_count = g_rtc.prev_hot_attempt_count;
+  p.prev_tx_success_count = g_rtc.prev_tx_success_count;
+  p.prev_tx_fail_count = g_rtc.prev_tx_fail_count;
+  p.prev_cb_timeout_count = g_rtc.prev_cb_timeout_count;
+  p.prev_txdone_sum_us = g_rtc.prev_txdone_sum_us;
+  p.prepared_message_left = static_cast(
+      prepared_send::PreparedMessageLeft() > 0xffffu
+          ? 0xffffu
+          : prepared_send::PreparedMessageLeft());
+
+  if (g_rtc.pending_valid) {
+    p.pending_kind = g_rtc.pending_kind;
+    p.pending_variant = g_rtc.pending_variant;
+    p.pending_hot_index = g_rtc.pending_hot_index;
+    p.pending_user_cycle_us = g_rtc.pending_user_cycle_us;
+    p.pending_wifi_cycle_us = g_rtc.pending_wifi_cycle_us;
+    p.wifi_init_us = g_rtc.pending_wifi_init_us;
+    p.connect_us = g_rtc.pending_connect_us;
+    p.encode_send_us = g_rtc.pending_encode_us;
+    p.tx_done_wait_us = g_rtc.pending_txdone_us;
+    p.teardown_us = g_rtc.pending_teardown_us;
+    p.heap_before_wifi = g_rtc.pending_heap_before;
+    p.heap_after_wifi = g_rtc.pending_heap_after;
+    p.authmode = g_rtc.pending_auth;
+    if (g_pending_diag.valid) {
+      p.tx_cb_total = g_pending_diag.tx_cb_total;
+      p.tx_cb_success = g_pending_diag.tx_cb_success;
+      p.tx_cb_failed = g_pending_diag.tx_cb_failed;
+      p.first_status = g_pending_diag.first_status;
+      p.cb_timeout = g_pending_diag.cb_timeout;
+      p.rssi = g_pending_diag.rssi;
+      p.actual_channel = g_pending_diag.actual_channel;
+      p.disconnect_count = g_pending_diag.disconnect_count;
+      p.reconnect_count = g_pending_diag.reconnect_count;
+    }
+  }
+  return bench::EncodeBootWifiOpt(p);
+}
+
+static std::uint32_t UserCycleFromAppEntry() {
+  auto const now = esp_timer_get_time();
+  auto const entry = g_early.app_entry_esp_timer_us;
+  if (now < entry) {
+    return 0;
+  }
+  auto const delta = now - entry;
+  return delta > 0xffffffffll ? 0xffffffffu
+                              : static_cast(delta);
+}
+
+static void StorePendingHot(prepared_send::FastSendResult const& result,
+                            std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = 2;
+  g_rtc.pending_variant = 0;
+  g_rtc.pending_hot_index = g_rtc.hot_index;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = result.cycle_us;
+  g_rtc.pending_wifi_init_us = result.wifi_init_us;
+  g_rtc.pending_connect_us = result.connect_us;
+  g_rtc.pending_encode_us = result.encode_send_us;
+  g_rtc.pending_txdone_us = result.tx_done_wait_us;
+  g_rtc.pending_teardown_us = result.teardown_us;
+  g_rtc.pending_heap_before = result.heap_before_wifi;
+  g_rtc.pending_heap_after = result.heap_after_wifi;
+  g_rtc.pending_cb_seen = result.cb_any;
+  g_rtc.pending_cb_timeout = result.cb_timeout;
+  g_rtc.pending_auth = result.negotiated_auth;
+  g_pending_diag = PendingDiag{};
+  g_pending_diag.valid = 1;
+  g_pending_diag.tx_cb_total = result.tx_cb_total;
+  g_pending_diag.tx_cb_success = result.tx_cb_success;
+  g_pending_diag.tx_cb_failed = result.tx_cb_failed;
+  g_pending_diag.first_status = result.first_status;
+  g_pending_diag.cb_timeout = result.cb_timeout;
+  g_pending_diag.rssi = result.rssi;
+  g_pending_diag.actual_channel = result.actual_channel;
+  g_pending_diag.disconnect_count = result.disconnect_count;
+  g_pending_diag.reconnect_count = result.reconnect_count;
+}
+
+static void StorePendingFull(std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = 1;
+  g_rtc.pending_variant = 0;
+  g_rtc.pending_hot_index = 0;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = user_cycle_us;
+  g_pending_diag = PendingDiag{};
+}
+
+static void ReleaseApp() {
+  g_select_sub.Reset();
+  g_stream_sub.Reset();
+  g_write_sub.Reset();
+  g_stream.reset();
+  g_client = {};
+  g_app.reset();
+}
+
+static void PreConstructCleanup() {
+  if (!g_had_aether_app) {
+    return;
+  }
+#  if !AE_WIFI_USE_FULL_DEINIT
+  esp_netif_deinit();
+  esp_event_loop_delete_default();
+#  endif
+}
+
+static void ConstructAether() {
+  PreConstructCleanup();
+  g_had_aether_app = true;
+  g_app = ae::AetherApp::Construct(
+      ae::AetherAppContext{}
+#  if AE_DISTILLATION
+          .AddAdapterFactory([&](ae::AetherAppContext const& ctx) {
+            return ae::WifiAdapter::ptr::Create(
+                ae::CreateWith{ctx.domain()}.with_id(
+                    ae::GlobalId::kWiFiAdapter),
+                ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit);
+          })
+#  endif
+  );
+}
+
+static prepared_send::FastPathConfig MakeFastConfig() {
+  prepared_send::FastPathConfig c{};
+  c.use_bssid = false;
+  c.use_channel = true;
+  c.use_fast_scan = false;
+  c.use_static_ip = true;
+  c.use_static_arp = true;
+  c.auth = prepared_send::FastAuthMode::kWpa2;
+  c.retry_max = 10;
+  c.pre_delay_ms = 25;
+  c.post_delay_ms = 0;
+  c.post_mode = prepared_send::FastPostMode::kTxDoneCb;
+  c.tx_done_wait = prepared_send::FastTxDoneWaitMode::kFirstAny;
+  c.set_mac_retry_limit = false;
+  c.wifi_storage_ram = kStorageRam;
+  c.wifi_nvs_enable = kNvsEnable;
+  c.force_ht20 = kForceHt20;
+  c.dynamic_cs = kDynamicCs;
+  c.static_rx_buf_num = kStaticRx;
+  c.dynamic_rx_buf_num = kDynamicRx;
+  c.dynamic_tx_buf_num = kDynamicTx;
+  return c;
+}
+
+static void DoFullWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto payload = MakePayload(bench::BootWifiOptMsgType::kFull);
+  auto& wa = g_stream->Write(std::move(payload));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_full_post_write = true;
+  });
+}
+
+static void MaybeFullWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFullWrite();
+}
+
+static void OnFullClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); });
+  MaybeFullWrite();
+}
+
+static void StartRegister() {
+  g_write_armed = false;
+  g_pending_register_finish = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       g_pending_register_finish = true;
+                     });
+}
+
+static void StartFull() {
+  g_write_armed = false;
+  g_write_ok = false;
+  g_pending_full_post_write = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFullClientReady(std::move(res).value());
+                     });
+}
+
+static void StartFinal() {
+  g_write_armed = false;
+  g_write_ok = false;
+  g_pending_final_exit = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       auto client = g_client.Load();
+                       g_stream = std::make_unique(
+                           *g_app, client, kServiceUid, ae::P2pPortHandle{});
+                       g_stream_sub = g_stream->stream_update_event().Subscribe(
+                           []() {
+                             if (!g_stream || g_write_armed) {
+                               return;
+                             }
+                             if (!g_stream->stream_info().is_writable) {
+                               return;
+                             }
+                             g_write_armed = true;
+                             auto& wa = g_stream->Write(
+                                 MakePayload(bench::BootWifiOptMsgType::kFinal));
+                             g_write_sub = wa.status_event().Subscribe(
+                                 [](ae::WriteAction::Status st) {
+                                   g_write_ok =
+                                       (st == ae::WriteAction::Status::kSuccess);
+                                   g_pending_final_exit = true;
+                                 });
+                           });
+                     });
+}
+
+static void FinishRegisterInLoop() {
+  auto client = g_client.Load();
+  if (!client) {
+    g_app->Exit(1);
+    return;
+  }
+  g_app->aether().Save();
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFullPostWriteInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  bool captured = false;
+  for (int i = 0; i < 10 && !captured; ++i) {
+    captured = prepared_send::CapturePreparedWifiRtcCache(&g_rtc_wifi_cache);
+    if (!captured) {
+      vTaskDelay(pdMS_TO_TICKS(200));
+    }
+  }
+  bool exported = false;
+  for (std::size_t n : {std::size_t{110}, std::size_t{100}, std::size_t{80}}) {
+    if (prepared_send::ExportPreparedSendBlock(g_client, kServiceUid, n)) {
+      exported = true;
+      break;
+    }
+  }
+  if (!exported || !captured || !prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() == 0) {
+    g_app->Exit(1);
+    return;
+  }
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFinalInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void AfterRegisterComplete() {
+  ReleaseApp();
+  g_rtc.registered = 1;
+  g_rtc.phase = static_cast(Phase::kFull);
+  g_rtc.variant_id = 0;
+  g_rtc.hot_index = 1;
+  ClearPending(g_rtc);
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFullComplete() {
+  ReleaseApp();
+  prepared_send::ReleaseFullAetherWifiForHotPath();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  StorePendingFull(UserCycleFromAppEntry());
+  g_rtc.phase = static_cast(Phase::kHot);
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  g_rtc.var_tx_success = 0;
+  g_rtc.var_tx_fail = 0;
+  g_rtc.var_cb_timeout = 0;
+  g_rtc.var_txdone_sum_us = 0;
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalComplete() {
+  ReleaseApp();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  ClearPending(g_rtc);
+  g_rtc.phase = static_cast(Phase::kDone);
+  SetCrc(g_rtc);
+  g_done = true;
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalFailed() {
+  ReleaseApp();
+  if (g_rtc.final_fail_count < 255) {
+    ++g_rtc.final_fail_count;
+  }
+  if (g_rtc.final_fail_count >= 3) {
+    ClearPending(g_rtc);
+    g_rtc.phase = static_cast(Phase::kDone);
+    SetCrc(g_rtc);
+    g_done = true;
+  }
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void RunHotOnce() {
+  if (!prepared_send::PreparedWifiRtcCacheIsValid(g_rtc_wifi_cache) ||
+      !prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() == 0) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (g_rtc.hot_attempt_count >= kMaxHotAttempts) {
+    AdvanceToNextVariantOrFinal();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (g_rtc.hot_attempt_count < 255) {
+    ++g_rtc.hot_attempt_count;
+  }
+  SetCrc(g_rtc);
+
+  g_cfg = MakeFastConfig();
+  g_wifi_snapshot =
+      prepared_send::SnapshotFromPreparedWifiRtcCache(g_rtc_wifi_cache);
+  auto payload = MakePayload(bench::BootWifiOptMsgType::kHot);
+  auto const result =
+      prepared_send::SendPreparedOnceWithFastPath(g_cfg, payload,
+                                                    &g_wifi_snapshot);
+
+  if (result.status == prepared_send::HotSendStatus::kWifiFailed) {
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (result.status != prepared_send::HotSendStatus::kSent) {
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  auto const user_cycle = UserCycleFromAppEntry();
+  bool const flushed_prior = g_rtc.pending_valid != 0;
+  StorePendingHot(result, user_cycle);
+  if (flushed_prior) {
+    AdvanceRecordIdAfterFlush();
+  }
+
+  if (g_rtc.hot_send_count < 255) {
+    ++g_rtc.hot_send_count;
+  }
+  if (result.first_status == 1) {
+    if (g_rtc.var_tx_success < 255) {
+      ++g_rtc.var_tx_success;
+    }
+  } else if (result.first_status == 0) {
+    if (g_rtc.var_tx_fail < 255) {
+      ++g_rtc.var_tx_fail;
+    }
+  }
+  if (result.cb_timeout) {
+    if (g_rtc.var_cb_timeout < 255) {
+      ++g_rtc.var_cb_timeout;
+    }
+  }
+  g_rtc.var_txdone_sum_us += result.tx_done_wait_us;
+
+  if (g_rtc.hot_index < 255) {
+    ++g_rtc.hot_index;
+  }
+
+  if (g_rtc.hot_send_count >= kHotPerVariant ||
+      g_rtc.hot_attempt_count >= kMaxHotAttempts) {
+    AdvanceToNextVariantOrFinal();
+  }
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void PrepareRtcOnBoot() {
+  g_early = GetExperimentEarlyEntrySnapshot();
+  g_sleep_elapsed_us = 0;
+  g_sleep_overhead_us = 0;
+  if (g_early.valid && g_rtc.sleep_arm_rtc_us != 0 &&
+      g_early.app_entry_rtc_us >= g_rtc.sleep_arm_rtc_us) {
+    auto const elapsed = g_early.app_entry_rtc_us - g_rtc.sleep_arm_rtc_us;
+    g_sleep_elapsed_us =
+        elapsed > 0xffffffffull ? 0xffffffffu : static_cast(elapsed);
+    if (g_sleep_elapsed_us > g_rtc.requested_sleep_us) {
+      g_sleep_overhead_us = g_sleep_elapsed_us - g_rtc.requested_sleep_us;
+    }
+  }
+
+  auto const reset =
+      static_cast(g_early.reset_reason);
+  bool const valid = ValidateRtcState(g_rtc);
+  g_rtc.current_boot_brownout = 0;
+
+  if (reset == ESP_RST_BROWNOUT) {
+    if (valid) {
+      if (g_rtc.brownout_count < 255) {
+        ++g_rtc.brownout_count;
+      }
+      ClearPending(g_rtc);
+    } else {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.brownout_count = 1;
+    }
+    g_rtc.current_boot_brownout = 1;
+    ForceFullRecovery();
+  } else if (!g_early.valid || reset != ESP_RST_DEEPSLEEP || !valid) {
+    bool const first_poweron = (reset == ESP_RST_POWERON);
+    if (first_poweron && (!valid || !g_rtc.registered)) {
+      InitRtcFresh(Phase::kRegister);
+    } else if (!valid) {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.registered = 1;
+      SetCrc(g_rtc);
+    } else {
+      if (g_rtc.unexpected_reset_count < 255) {
+        ++g_rtc.unexpected_reset_count;
+      }
+      ForceFullRecovery();
+    }
+  }
+  SetCrc(g_rtc);
+}
+
+#endif  // ESP_PLATFORM
+
+}  // namespace
+}  // namespace temp_sensor
+
+#if defined(ESP_PLATFORM)
+
+void setup() {
+  using namespace temp_sensor;
+  nvs_flash_init();
+  g_done = false;
+  g_pending_register_finish = false;
+  g_pending_full_post_write = false;
+  g_pending_final_exit = false;
+  PrepareRtcOnBoot();
+  g_cfg = MakeFastConfig();
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kDone) {
+    g_done = true;
+    return;
+  }
+  if (phase == Phase::kRegister) {
+    StartRegister();
+    return;
+  }
+  if (phase == Phase::kFull) {
+    StartFull();
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    StartFinal();
+    return;
+  }
+}
+
+void loop() {
+  using namespace temp_sensor;
+  if (g_done) {
+    vTaskDelay(pdMS_TO_TICKS(1000));
+    return;
+  }
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kHot) {
+    RunHotOnce();
+    return;
+  }
+
+  auto process_deferred = []() {
+    if (g_app && g_pending_register_finish) {
+      g_pending_register_finish = false;
+      FinishRegisterInLoop();
+      return true;
+    }
+    if (g_app && g_pending_full_post_write) {
+      g_pending_full_post_write = false;
+      FinishFullPostWriteInLoop();
+      return true;
+    }
+    if (g_app && g_pending_final_exit) {
+      g_pending_final_exit = false;
+      FinishFinalInLoop();
+      return true;
+    }
+    return false;
+  };
+
+  if (process_deferred()) {
+    return;
+  }
+  if (!g_app) {
+    return;
+  }
+  if (!g_app->IsExited()) {
+    auto t = g_app->Update(ae::Now());
+    if (process_deferred()) {
+      return;
+    }
+    if (!g_app->IsExited()) {
+      g_app->WaitUntil(t);
+    }
+    return;
+  }
+
+  if (phase == Phase::kRegister) {
+    if (g_exit_success) {
+      AfterRegisterComplete();
+    } else {
+      ReleaseApp();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFull) {
+    if (g_exit_success) {
+      AfterFullComplete();
+    } else {
+      ReleaseApp();
+      ForceFullRecovery();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    if (g_exit_success) {
+      AfterFinalComplete();
+    } else {
+      AfterFinalFailed();
+    }
+    return;
+  }
+}
+
+#else
+
+void setup() {}
+void loop() {}
+
+#endif
diff --git a/main/prepared_send/prepared_send.cpp b/main/prepared_send/prepared_send.cpp
index 0c49d31..e9431b9 100644
--- a/main/prepared_send/prepared_send.cpp
+++ b/main/prepared_send/prepared_send.cpp
@@ -37,6 +37,7 @@
 #  include 
 #  include 
 #  include 
+#  include 
 #  include 
 #  include 
 #  include 
@@ -165,6 +166,9 @@ static esp_event_handler_instance_t g_wifi_any_id_handler = nullptr;
 static esp_event_handler_instance_t g_wifi_got_ip_handler = nullptr;
 static bool g_wifi_initialized = false;
 static bool g_wifi_started = false;
+static std::uint32_t g_last_wifi_init_us = 0;
+static std::uint32_t g_last_heap_before_wifi = 0;
+static std::uint32_t g_last_heap_after_wifi = 0;
 static bool g_default_event_loop_created = false;
 static int g_wifi_retry_count = 0;
 static bool g_wait_got_ip = true;
@@ -1447,6 +1451,28 @@ bool StartFastWifi(FastPathConfig const& cfg,
   if (cfg.ampdu_tx_off) {
     wifi_init_cfg.ampdu_tx_enable = 0;
   }
+  if (cfg.ampdu_rx_off) {
+    wifi_init_cfg.ampdu_rx_enable = 0;
+    wifi_init_cfg.rx_ba_win = 0;
+  } else if (cfg.rx_ba_win != 0) {
+    wifi_init_cfg.rx_ba_win = cfg.rx_ba_win;
+  }
+  if (cfg.amsdu_tx_off) {
+    wifi_init_cfg.amsdu_tx_enable = 0;
+  }
+  wifi_init_cfg.nvs_enable = cfg.wifi_nvs_enable ? 1 : 0;
+  if (cfg.static_rx_buf_num != 0) {
+    wifi_init_cfg.static_rx_buf_num = cfg.static_rx_buf_num;
+  }
+  if (cfg.dynamic_rx_buf_num != 0) {
+    wifi_init_cfg.dynamic_rx_buf_num = cfg.dynamic_rx_buf_num;
+  }
+  if (cfg.dynamic_tx_buf_num != 0) {
+    wifi_init_cfg.dynamic_tx_buf_num = cfg.dynamic_tx_buf_num;
+  }
+
+  auto const heap_before = esp_get_free_heap_size();
+  auto const t_wifi_init0 = esp_timer_get_time();
 
   auto err = nvs_flash_init();
   if (err == ESP_ERR_NVS_NO_FREE_PAGES ||
@@ -1493,6 +1519,13 @@ bool StartFastWifi(FastPathConfig const& cfg,
   }
 
   err = esp_wifi_init(&wifi_init_cfg);
+  auto const t_wifi_init1 = esp_timer_get_time();
+  g_last_wifi_init_us =
+      (t_wifi_init1 > t_wifi_init0)
+          ? static_cast(t_wifi_init1 - t_wifi_init0)
+          : 0;
+  g_last_heap_before_wifi = heap_before;
+  g_last_heap_after_wifi = esp_get_free_heap_size();
   if (err == ESP_ERR_WIFI_INIT_STATE) {
     g_wifi_initialized = true;
   } else if (err != ESP_OK) {
@@ -1581,6 +1614,14 @@ bool StartFastWifi(FastPathConfig const& cfg,
 
   (void)esp_wifi_set_max_tx_power(80);
   (void)esp_wifi_set_ps(WIFI_PS_NONE);
+  if (cfg.force_ht20) {
+    (void)esp_wifi_set_bandwidth(WIFI_IF_STA, WIFI_BW20);
+  }
+  if (cfg.dynamic_cs == 0) {
+    (void)esp_wifi_set_dynamic_cs(false);
+  } else if (cfg.dynamic_cs == 1) {
+    (void)esp_wifi_set_dynamic_cs(true);
+  }
   // TX-done callback is installed immediately before sendto() for callback
   // post modes — never here (association / PRE would fire unrelated TX).
 
@@ -1751,6 +1792,9 @@ FastSendResult SendPreparedOnceWithFastPath(
     auto const elapsed = t_ready - t0;
     out.connect_us = elapsed < 0 ? 0 : static_cast(elapsed);
   }
+  out.wifi_init_us = g_last_wifi_init_us;
+  out.heap_before_wifi = g_last_heap_before_wifi;
+  out.heap_after_wifi = g_last_heap_after_wifi;
   out.status_flags |=
       static_cast(bench::BisectStatusBits::kWifiReady);
   out.actual_channel = g_bisect_actual_channel;
diff --git a/main/prepared_send/prepared_send.h b/main/prepared_send/prepared_send.h
index 2619ae5..3ab5f76 100644
--- a/main/prepared_send/prepared_send.h
+++ b/main/prepared_send/prepared_send.h
@@ -141,15 +141,24 @@ struct FastPathConfig {
   bool use_static_ip{true};
   bool use_static_arp{true};
   bool ampdu_tx_off{false};
+  bool ampdu_rx_off{false};
+  bool amsdu_tx_off{false};
   bool wifi_storage_ram{false};
+  bool wifi_nvs_enable{true};
+  bool force_ht20{false};
+  // -1 = leave default; 0 = false; 1 = true
+  std::int8_t dynamic_cs{-1};
+  // 0 = use WIFI_INIT_CONFIG_DEFAULT values
+  std::uint8_t static_rx_buf_num{0};
+  std::uint8_t dynamic_rx_buf_num{0};
+  std::uint8_t dynamic_tx_buf_num{0};
+  std::uint8_t rx_ba_win{0};  // 0 = default; used when ampdu_rx enabled or F6
   FastAuthMode auth{FastAuthMode::kWpa3Both};
   std::uint8_t retry_max{10};
   std::uint16_t pre_delay_ms{200};
   std::uint16_t post_delay_ms{300};
   FastPostMode post_mode{FastPostMode::kFixedDelay};
   FastTxDoneWaitMode tx_done_wait{FastTxDoneWaitMode::kFirstAny};
-  // Experiment-only: MAC short/long retry via esp_wifi_internal_set_retry_counter.
-  // Association retry_max is independent and must stay unchanged.
   bool set_mac_retry_limit{false};
   std::uint8_t mac_short_retry{0};
   std::uint8_t mac_long_retry{0};
@@ -159,6 +168,7 @@ struct FastSendResult {
   HotSendStatus status{HotSendStatus::kWifiFailed};
   std::uint32_t cycle_us{0};
   std::uint32_t connect_us{0};
+  std::uint32_t wifi_init_us{0};
   std::uint32_t encode_send_us{0};
   std::uint32_t tx_done_wait_us{0};
   std::uint32_t teardown_us{0};
@@ -170,9 +180,8 @@ struct FastSendResult {
   std::uint8_t cb_match{0};
   std::uint8_t cb_timeout{0};
   std::uint8_t cb_count{0};
-  // TX-done diagnostics (experiment).
   std::uint8_t diag_mode{0};
-  std::uint8_t first_status{0xff};  // 0xff none, 0 fail, 1 success
+  std::uint8_t first_status{0xff};
   std::uint8_t tx_cb_total{0};
   std::uint8_t tx_cb_success{0};
   std::uint8_t tx_cb_failed{0};
@@ -186,12 +195,13 @@ struct FastSendResult {
   std::uint8_t disconnect_count{0};
   std::uint8_t last_disconnect_reason{0};
   std::uint8_t reconnect_count{0};
-  // MAC retry-limit diagnostics (experiment).
-  std::int16_t mac_retry_set_rc{-1};  // -1 = not called
+  std::int16_t mac_retry_set_rc{-1};
   std::uint8_t mac_short_retry{0};
   std::uint8_t mac_long_retry{0};
   std::uint8_t mac_retry_called{0};
   std::uint32_t retry_cfg_us{0};
+  std::uint32_t heap_before_wifi{0};
+  std::uint32_t heap_after_wifi{0};
 };
 
 // BASE = cached channel + static IPv4/netmask/gw + static ARP. No BSSID.
diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp
index 247170f..ff6c946 100644
--- a/temperature_receiver/main.cpp
+++ b/temperature_receiver/main.cpp
@@ -1,7 +1,7 @@
 /*
  * Copyright 2026 Aethernet Inc.
  *
- * Desktop Æther receiver for prepared MAC-retry diagnostics (MacRetryPayload 0xD7),
+ * Desktop Æther receiver for prepared boot/wifi opt (0xD8), MAC-retry (0xD7),
  * TX-done (0xD6) and deep-sleep E2E (0xD5). Deduplicates by record_id; appends TSV.
  */
 
@@ -73,6 +73,9 @@ struct Meas {
   std::uint32_t retry_cfg_us{0};
   std::uint32_t encode_us{0};
   std::uint8_t actual_channel{0};
+  std::uint32_t wifi_init_us{0};
+  std::uint32_t heap_before{0};
+  std::uint32_t heap_after{0};
 };
 
 std::mutex g_mu;
@@ -119,7 +122,8 @@ void EnsureTsvHeader() {
          "callbacks_after_success\trssi\tdisconnect_count\t"
          "last_disconnect_reason\treconnect_count\tap_primary\t"
          "variant\tshort_retry\tlong_retry\tretry_called\tretry_set_rc\t"
-         "retry_cfg_us\tencode_us\tactual_channel\n";
+         "retry_cfg_us\tencode_us\tactual_channel\twifi_init_us\t"
+         "heap_before\theap_after\n";
 }
 
 void AppendTsv(Meas const& m) {
@@ -151,7 +155,9 @@ void AppendTsv(Meas const& m) {
       << static_cast(m.long_retry) << '\t'
       << static_cast(m.retry_called) << '\t'
       << static_cast(m.retry_set_rc) << '\t' << m.retry_cfg_us << '\t'
-      << m.encode_us << '\t' << static_cast(m.actual_channel) << '\n';
+      << m.encode_us << '\t' << static_cast(m.actual_channel) << '\t'
+      << m.wifi_init_us << '\t' << m.heap_before << '\t' << m.heap_after
+      << '\n';
 }
 
 void NoteRecord(Meas m) {
@@ -263,6 +269,83 @@ void PrintFinalStats(char const* tag) {
   std::cout.flush();
 }
 
+void OnBootWifiOpt(temp_sensor::bench::BootWifiOptPayload const& p) {
+  auto const type = static_cast(p.type);
+  static int hot_by_var[16] = {};
+  if (type == temp_sensor::bench::BootWifiOptMsgType::kFull) {
+    ++g_full_recv;
+    std::cout << "BWO_FULL seq=" << p.sequence_global
+              << " variant=" << static_cast(p.variant_id)
+              << " name="
+              << temp_sensor::bench::BootWifiOptVariantName(p.variant_id)
+              << " prev_v=" << static_cast(p.prev_variant_id)
+              << " prev_sends=" << static_cast(p.prev_hot_send_count)
+              << "\n";
+  } else if (type == temp_sensor::bench::BootWifiOptMsgType::kHot) {
+    ++g_hot_recv;
+    auto vid = p.pending_kind == 2 ? p.pending_variant : p.variant_id;
+    if (vid < 16) {
+      ++hot_by_var[vid];
+    }
+    std::cout << "BWO V" << static_cast(vid) << " "
+              << (vid < 16 ? hot_by_var[vid] : 0)
+              << (vid == 0xff ? "/100" : "/30")
+              << " wake_ov=" << (p.sleep_to_app_overhead_us / 1000.0) << "ms"
+              << " init=" << (p.wifi_init_us / 1000.0) << "ms"
+              << " conn=" << (p.connect_us / 1000.0) << "ms"
+              << " txdone=" << (p.tx_done_wait_us / 1000.0) << "ms"
+              << " user=" << (p.pending_user_cycle_us / 1000.0) << "ms"
+              << " rssi=" << static_cast(p.rssi) << "\n";
+  } else if (type == temp_sensor::bench::BootWifiOptMsgType::kFinal) {
+    ++g_final_recv;
+    std::cout << "BWO_FINAL seq=" << p.sequence_global << "\n";
+  }
+
+  Meas m{};
+  m.record_id = p.record_id;
+  m.kind = p.pending_kind;
+  m.outer = p.pending_variant;
+  m.hot = p.pending_hot_index;
+  m.user_us = p.pending_user_cycle_us;
+  m.wifi_us = p.pending_wifi_cycle_us;
+  m.connect_us = p.connect_us;
+  m.txdone_us = p.tx_done_wait_us;
+  m.teardown_us = p.teardown_us;
+  m.sleep_elapsed_us = p.sleep_elapsed_to_app_us;
+  m.sleep_overhead_us = p.sleep_to_app_overhead_us;
+  m.app_entry_us = static_cast(
+      p.app_entry_esp_timer_us < 0
+          ? 0
+          : (p.app_entry_esp_timer_us > 0xffffffffll
+                 ? 0xffffffffu
+                 : static_cast(p.app_entry_esp_timer_us)));
+  m.cb_seen = (p.flags & 2) ? 1 : 0;
+  m.cb_timeout = p.cb_timeout;
+  m.brownout = (p.flags & 1) ? 1 : 0;
+  m.auth = p.authmode;
+  m.tx_cb_total = p.tx_cb_total;
+  m.tx_cb_success = p.tx_cb_success;
+  m.tx_cb_failed = p.tx_cb_failed;
+  m.first_status = p.first_status;
+  m.rssi = p.rssi;
+  m.disconnect_count = p.disconnect_count;
+  m.reconnect_count = p.reconnect_count;
+  m.actual_channel = p.actual_channel;
+  m.ap_primary = p.actual_channel;
+  m.seq = p.sequence_global;
+  m.variant = p.pending_kind == 2 ? p.pending_variant : p.variant_id;
+  m.encode_us = p.encode_send_us;
+  m.wifi_init_us = p.wifi_init_us;
+  m.heap_before = p.heap_before_wifi;
+  m.heap_after = p.heap_after_wifi;
+  NoteRecord(m);
+
+  if (type == temp_sensor::bench::BootWifiOptMsgType::kFinal) {
+    PrintFinalStats("boot_wifi_opt");
+  }
+  std::cout.flush();
+}
+
 void OnMacRetry(temp_sensor::bench::MacRetryPayload const& p) {
   auto const type = static_cast(p.type);
   static int hot_by_var[8] = {};
@@ -494,6 +577,11 @@ void OnDs(temp_sensor::bench::DsPayload const& p) {
 
 void OnMessage(ae::Uid, ae::DataBuffer const& data) {
   std::lock_guard lock{g_mu};
+  temp_sensor::bench::BootWifiOptPayload bwo{};
+  if (temp_sensor::bench::DecodeBootWifiOpt(data, bwo)) {
+    OnBootWifiOpt(bwo);
+    return;
+  }
   temp_sensor::bench::MacRetryPayload mr{};
   if (temp_sensor::bench::DecodeMacRetry(data, mr)) {
     OnMacRetry(mr);

From a6c577a34509738b5761ffadd2b37c0fc40f7495 Mon Sep 17 00:00:00 2001
From: aethernet-io 
Date: Sat, 29 Aug 2026 18:50:08 -0700
Subject: [PATCH 32/32] Add aethernetio AP 3x10 prepared-path E2E experiment.

Wire AE_EXP_PREPARED_AP_AETHERNETIO_3X10 bench (D1 path, 1 s deep sleep), extend DsPayload with association fields, and add orchestrator plus report from the first hardware run on SSID aethernetio.

Co-authored-by: Cursor 
---
 CMakeLists.txt                                |  16 +
 .../PREPARED_AP_AETHERNETIO_3X10_REPORT.md    |  65 ++
 experiments/analyze_ap_aethernetio_3x10.py    | 181 ++++
 experiments/prepared_ap_aethernetio_3x10.tsv  |   3 +
 experiments/run_ap_aethernetio_3x10.py        | 301 ++++++
 main/CMakeLists.txt                           |  18 +
 main/bench_payload.h                          |   9 +-
 main/experiment_early_entry.cpp               |   2 +
 main/experiment_early_entry.h                 |   2 +
 main/prepared_ap_aethernetio_3x10_bench.cpp   | 963 ++++++++++++++++++
 temperature_receiver/main.cpp                 | 115 ++-
 11 files changed, 1672 insertions(+), 3 deletions(-)
 create mode 100644 experiments/PREPARED_AP_AETHERNETIO_3X10_REPORT.md
 create mode 100644 experiments/analyze_ap_aethernetio_3x10.py
 create mode 100644 experiments/prepared_ap_aethernetio_3x10.tsv
 create mode 100644 experiments/run_ap_aethernetio_3x10.py
 create mode 100644 main/prepared_ap_aethernetio_3x10_bench.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7e9d23a..9445df8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -37,6 +37,10 @@ set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING
     "Silent fastest-path prepared Wi-Fi campaign (set to 1)")
 set(AE_EXP_PREPARED_DEEPSLEEP_5X50 "" CACHE STRING
     "Silent deep-sleep 5x50 prepared E2E (set to 1)")
+set(AE_EXP_PREPARED_FINAL_D1_5X50 "" CACHE STRING
+    "Final D1 WIFI_STORAGE_RAM deep-sleep 5x50 validation (set to 1)")
+set(AE_EXP_PREPARED_AP_AETHERNETIO_3X10 "" CACHE STRING
+    "D1 prepared path on aethernetio AP, 3x10 1s deep sleep (set to 1)")
 set(AE_EXP_PREPARED_TX_DONE_DIAG "" CACHE STRING
     "Silent TX-done callback diagnostic 1x50 (set to 1)")
 set(AE_EXP_PREPARED_MAC_RETRY_DIAG "" CACHE STRING
@@ -63,6 +67,18 @@ elseif(AE_EXP_PREPARED_DEEPSLEEP_5X50 STREQUAL "1")
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
+elseif(AE_EXP_PREPARED_FINAL_D1_5X50 STREQUAL "1")
+  list(APPEND SDKCONFIG_DEFAULTS
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
+elseif(AE_EXP_PREPARED_AP_AETHERNETIO_3X10 STREQUAL "1")
+  list(APPEND SDKCONFIG_DEFAULTS
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.fastest"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.wpa2only"
+       "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.deepsleep_5x50")
 elseif(AE_EXP_PREPARED_TX_DONE_DIAG STREQUAL "1")
   list(APPEND SDKCONFIG_DEFAULTS
        "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults.silent"
diff --git a/experiments/PREPARED_AP_AETHERNETIO_3X10_REPORT.md b/experiments/PREPARED_AP_AETHERNETIO_3X10_REPORT.md
new file mode 100644
index 0000000..41164da
--- /dev/null
+++ b/experiments/PREPARED_AP_AETHERNETIO_3X10_REPORT.md
@@ -0,0 +1,65 @@
+# Prepared AP aethernetio 3×10 Report
+
+## CONFIG
+
+- AP SSID: **aethernetio**
+- WPA2, Wi-Fi 4 b/g/n
+- cached channel yes, BSSID no
+- static IP / static ARP yes
+- PRE 25 ms, TX-done callback, POST 0
+- WIFI_STORAGE_RAM yes (D1)
+- wifi nvs ON
+- TX power default, MAC retry default
+- external RTC crystal, CAL cycles 1024
+- deep sleep **1 s** between HOT/FULL
+- 3 FULL × 10 HOT
+- RTC magic AET1 (invalidates prior AP cache)
+
+## RESULT (campaign TIMEOUT 25 min, no FINAL)
+
+- FULL received (unique outer): **3/3** (device later entered FULL recovery loop; many duplicate FULL telemetries)
+- FULL user (TSV): **n/a** — no FULL pending records flushed before timeout
+- HOT sendto (TSV kind=2): **2/30** — only HOT #10 of blocks 1–2 flushed via subsequent FULL payloads
+- HOT received (DS_HOT lines): **0/30** (0.0%)
+- FINAL: **0**
+
+### HOT timing (2 TSV samples, both block-end flush)
+
+- user: **1075.3 / 1060.3 ms**
+- Wi-Fi: **1064.0 / 1049.0 ms**
+- connect: **1001.1 / 983.8 ms** (raw all: `[1001.1, 983.8]` ms)
+- tx-done: **2.6 / 0.2 ms**
+- wake overhead: **40.7 ms**
+- RSSI: **-19 / -18 dBm**, channel **6**
+
+### Association
+
+- connect median **992 ms** ≈ 1 s sleep interval → frequent reconnect under pressure
+- disconnect reasons recorded: **none** (metrics only on successful sendto flush)
+- failed_assoc_wakes (telemetry): **0** (counter only visible after successful HOT payload)
+- callback seen/timeouts: **2/0**
+- brownouts: **0**
+
+### Observed behavior
+
+1. Æther FULL path on `aethernetio` works (3 unique FULL outers received).
+2. Prepared HOT path rarely completes; device cycles **FULL recovery** (outer 1→2→3 repeated).
+3. Two successful HOT sendto at block boundaries did not produce standalone `DS_HOT` receiver lines; pending metrics arrived embedded in later FULL writes.
+4. Campaign did not reach FINAL within 25 min.
+
+## COMPARE vs previous AP (chirkov refs)
+
+| metric | chirkov ref | aethernetio | delta |
+|--------|-------------|-------------|-------|
+| delivery | 99.6% (249/250) | 0.0% (0/30 DS_HOT) | -99.6 pp |
+| hot user med | ~250 ms | ~1075 ms (n=2) | +825 ms |
+| connect med | ~158 ms | ~992 ms | +834 ms |
+| failed assoc (visible) | 0 | 0* | — |
+
+\*Association failures likely occurred but are invisible until a successful HOT telemetry flush.
+
+## NEXT
+
+- Re-run with longer orchestrator timeout after verifying `aethernetio` RSSI/range.
+- Consider emitting association-failure telemetry before sendto for 1 s sleep studies.
+- DTIM/PS experiments deferred per plan.
diff --git a/experiments/analyze_ap_aethernetio_3x10.py b/experiments/analyze_ap_aethernetio_3x10.py
new file mode 100644
index 0000000..2560c1c
--- /dev/null
+++ b/experiments/analyze_ap_aethernetio_3x10.py
@@ -0,0 +1,181 @@
+"""Analyze prepared_ap_aethernetio_3x10.tsv and write short report."""
+
+from __future__ import annotations
+
+import csv
+import statistics
+from collections import Counter
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+TSV = ROOT / "experiments" / "prepared_ap_aethernetio_3x10.tsv"
+RX_LOG = ROOT / "experiments" / "prepared_ap_aethernetio_3x10_rx.log"
+REPORT = ROOT / "experiments" / "PREPARED_AP_AETHERNETIO_3X10_REPORT.md"
+
+WIFI_REASON = {
+    2: "AUTH_EXPIRE",
+    15: "4WAY_HANDSHAKE_TIMEOUT",
+    201: "NO_AP_FOUND",
+    204: "HANDSHAKE_TIMEOUT",
+    205: "CONNECTION_FAIL",
+    210: "ASSOC_FAIL",
+    211: "ASSOC_COMEBACK_TIME_TOO_LONG",
+    212: "ASSOC_REFUSED_TEMPORARILY",
+}
+
+
+def iu(x: str) -> int:
+    try:
+        return int(float(x))
+    except (TypeError, ValueError):
+        return 0
+
+
+def pct(vals: list[int], q: int) -> float:
+    if not vals:
+        return 0.0
+    s = sorted(vals)
+    if q <= 0:
+        return float(s[0])
+    if q >= 100:
+        return float(s[-1])
+    k = (len(s) - 1) * q / 100.0
+    f = int(k)
+    c = min(f + 1, len(s) - 1)
+    if f == c:
+        return float(s[f])
+    return s[f] + (s[c] - s[f]) * (k - f)
+
+
+def ms(us: float) -> str:
+    return f"{us / 1000.0:.1f}"
+
+
+def main() -> None:
+    rows = (
+        list(csv.DictReader(TSV.open(encoding="utf-8"), delimiter="\t"))
+        if TSV.exists()
+        else []
+    )
+    fulls = [r for r in rows if iu(r["kind"]) == 1]
+    hots = [r for r in rows if iu(r["kind"]) == 2]
+
+    full_user = [iu(r["user_us"]) for r in fulls]
+    hot_user = [iu(r["user_us"]) for r in hots]
+    hot_wifi = [iu(r["wifi_us"]) for r in hots]
+    connect = [iu(r["connect_us"]) for r in hots if iu(r["connect_us"]) > 0]
+    txdone = [iu(r["txdone_us"]) for r in hots if iu(r["txdone_us"]) > 0]
+    wake = [iu(r["sleep_overhead_us"]) for r in hots if iu(r["sleep_overhead_us"]) > 0]
+    rssi = [iu(r["rssi"]) for r in hots if r.get("rssi", "0") not in ("", "0")]
+    channels = [iu(r["actual_channel"]) for r in hots if iu(r.get("actual_channel", "0")) > 0]
+
+    cb_seen = sum(1 for r in hots if iu(r.get("cb_seen", 0)))
+    cb_to = sum(1 for r in hots if iu(r.get("cb_timeout", 0)))
+    brownouts = sum(1 for r in rows if iu(r.get("brownout", 0)))
+
+    disc_reasons = Counter(
+        iu(r.get("last_disconnect_reason", 0))
+        for r in hots
+        if iu(r.get("disconnect_count", 0)) > 0
+    )
+
+    hot_recv = 0
+    full_recv = 0
+    if RX_LOG.exists():
+        import re
+
+        t = RX_LOG.read_text(encoding="utf-8", errors="replace")
+        hot_recv = len(re.findall(r"^DS_HOT ", t, re.M))
+        full_recv = len(set(re.findall(r"^DS_FULL outer=(\d+)", t, re.M)))
+
+    connect_raw = [iu(r["connect_us"]) for r in hots]
+
+    failed_assoc = 0
+    if RX_LOG.exists():
+        import re
+
+        for m in re.finditer(r"failed_assoc_wakes=(\d+)", RX_LOG.read_text(encoding="utf-8")):
+            failed_assoc = max(failed_assoc, int(m.group(1)))
+
+    # Baseline from chirkov D1 incomplete + prior deepsleep refs
+    baseline_hot_user = 250.3
+    baseline_connect = 157.7
+    baseline_delivery = 249 / 250 * 100
+
+    delivery_pct = (hot_recv / 30.0 * 100) if hot_recv else 0.0
+    hot_med = pct(hot_user, 50)
+    connect_med = pct(connect, 50) if connect else 0.0
+
+    lines = [
+        "# Prepared AP aethernetio 3×10 Report",
+        "",
+        "## CONFIG",
+        "",
+        "- AP SSID: **aethernetio**",
+        "- WPA2, Wi-Fi 4 b/g/n",
+        "- cached channel yes, BSSID no",
+        "- static IP / static ARP yes",
+        "- PRE 25 ms, TX-done callback, POST 0",
+        "- WIFI_STORAGE_RAM yes (D1)",
+        "- wifi nvs ON",
+        "- TX power default, MAC retry default",
+        "- external RTC crystal, CAL cycles 1024",
+        "- deep sleep **1 s** between HOT/FULL",
+        "- 3 FULL × 10 HOT",
+        "",
+        "## RESULT",
+        "",
+        f"- FULL received: **{full_recv}/3**",
+        f"- FULL user raw (ms): {[ms(x) for x in full_user] if full_user else '[]'}",
+        f"- FULL user median: **{ms(pct(full_user, 50)) if full_user else 'n/a'} ms**",
+        f"- HOT sendto (TSV): **{len(hots)}/30**",
+        f"- HOT received: **{hot_recv}/30** ({delivery_pct:.1f}%)",
+        "",
+        "### HOT timing",
+        "",
+        f"- user median/p90/max: **{ms(pct(hot_user, 50))} / {ms(pct(hot_user, 90))} / {ms(pct(hot_user, 99))} / {ms(max(hot_user) if hot_user else 0)} ms**",
+        f"- Wi-Fi median: **{ms(pct(hot_wifi, 50))} ms**",
+        f"- connect median/p90/max: **{ms(connect_med)} / {ms(pct(connect, 90)) if connect else 'n/a'} / {ms(max(connect) if connect else 0)} ms**",
+        f"- tx-done median: **{ms(pct(txdone, 50)) if txdone else 'n/a'} ms**",
+        f"- wake overhead median: **{ms(pct(wake, 50)) if wake else 'n/a'} ms**",
+        "",
+        "### Wi-Fi / association",
+        "",
+        f"- channel(s): {sorted(set(channels)) if channels else 'n/a'}",
+        f"- RSSI median: **{int(statistics.median(rssi)) if rssi else 'n/a'} dBm**",
+        f"- failed association wakes: **{failed_assoc}**",
+        f"- callback seen/timeouts: **{cb_seen}/{cb_to}**",
+        f"- brownouts: **{brownouts}**",
+        "",
+        "### Connect times (all HOT, ms)",
+        "",
+        f"`{[ms(x) for x in connect_raw]}`",
+        "",
+        "### Disconnect reasons (when disconnect_count>0)",
+        "",
+    ]
+    if disc_reasons:
+        for code, cnt in sorted(disc_reasons.items()):
+            name = WIFI_REASON.get(code, f"REASON_{code}")
+            lines.append(f"- {name} ({code}): {cnt}")
+    else:
+        lines.append("- none recorded")
+
+    lines += [
+        "",
+        "## COMPARE vs previous AP (chirkov baseline refs)",
+        "",
+        f"| metric | chirkov ref | aethernetio | delta |",
+        f"|--------|-------------|-------------|-------|",
+        f"| delivery | {baseline_delivery:.1f}% | {delivery_pct:.1f}% | {delivery_pct - baseline_delivery:+.1f} pp |",
+        f"| hot user med | {baseline_hot_user:.1f} ms | {hot_med/1000:.1f} ms | {(hot_med - baseline_hot_user*1000)/1000:+.1f} ms |",
+        f"| connect med | {baseline_connect:.1f} ms | {connect_med/1000:.1f} ms | {(connect_med - baseline_connect*1000)/1000:+.1f} ms |",
+        f"| failed assoc | 0 | {failed_assoc} | — |",
+        "",
+    ]
+    REPORT.write_text("\n".join(lines) + "\n", encoding="utf-8")
+    print(f"Wrote {REPORT}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/experiments/prepared_ap_aethernetio_3x10.tsv b/experiments/prepared_ap_aethernetio_3x10.tsv
new file mode 100644
index 0000000..cb67861
--- /dev/null
+++ b/experiments/prepared_ap_aethernetio_3x10.tsv
@@ -0,0 +1,3 @@
+record_id	kind	outer	hot	user_us	wifi_us	connect_us	txdone_us	teardown_us	sleep_elapsed_us	sleep_overhead_us	app_entry_us	cb_seen	cb_timeout	brownout	auth	seq	diag_mode	tx_cb_total	tx_cb_success	tx_cb_failed	first_status	first_cb_delta_us	first_success_delta_us	first_failed_delta_us	last_cb_delta_us	callbacks_after_success	rssi	disconnect_count	last_disconnect_reason	reconnect_count	ap_primary	variant	short_retry	long_retry	retry_called	retry_set_rc	retry_cfg_us	encode_us	actual_channel	wifi_init_us	heap_before	heap_after
+11	2	1	10	1090284	1078974	1001059	2559	54409	1040710	40710	5381	1	0	0	3	12	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	-19	0	0	0	0	0	0	0	0	-1	0	0	6	0	0	0
+22	2	2	10	1060284	1048974	983803	174	45373	1040710	40710	5381	1	0	0	3	23	0	0	0	0	255	4294967295	4294967295	4294967295	4294967295	0	-18	0	0	0	0	0	0	0	0	-1	0	0	6	0	0	0
diff --git a/experiments/run_ap_aethernetio_3x10.py b/experiments/run_ap_aethernetio_3x10.py
new file mode 100644
index 0000000..873e0d4
--- /dev/null
+++ b/experiments/run_ap_aethernetio_3x10.py
@@ -0,0 +1,301 @@
+"""
+aethernetio AP: D1 prepared path 3 FULL x 10 HOT, 1 s deep sleep.
+COM only for flash; runtime via Aether receiver only.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\nickc\Projects\temperature-sensor-prepared")
+BUILD = ROOT / "build-esp32c6-save-bench-smoke"
+AETHER = r"C:/Users/nickc/Projects/aether-client-cpp-prepared-packet-v0"
+PY = Path(r"C:\Espressif\python_env\idf6.0_py3.11_env\Scripts\python.exe")
+CMAKE = Path(r"C:\Espressif\tools\cmake\3.30.2\bin\cmake.exe")
+NINJA = Path(r"C:\Espressif\tools\ninja\1.12.1\ninja.exe")
+RX_EXE = ROOT / "temperature_receiver" / "build-bisect" / "temperature_receiver.exe"
+RX_BUILD = ROOT / "temperature_receiver" / "build-bisect"
+RX_SESSION = ROOT / "experiments" / "prepared_wifi_cache_rx_session"
+IDF_PATH = r"C:\Espressif\frameworks\esp-idf-v6.0.2"
+CCACHE = r"C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64"
+PROGRESS = ROOT / "experiments" / "ap_aethernetio_3x10_progress.log"
+RX_LOG = ROOT / "experiments" / "prepared_ap_aethernetio_3x10_rx.log"
+TSV = ROOT / "experiments" / "prepared_ap_aethernetio_3x10.tsv"
+PORT = "COM7"
+HOT_TARGET = 30
+FULL_TARGET = 3
+
+
+def env() -> dict:
+    e = os.environ.copy()
+    e["IDF_PATH"] = IDF_PATH
+    e["IDF_TOOLS_PATH"] = r"C:\Espressif"
+    extra = [
+        CCACHE,
+        r"C:\Espressif\tools\ninja\1.12.1",
+        r"C:\Espressif\tools\cmake\3.30.2\bin",
+        r"C:\msys64\ucrt64\bin",
+    ]
+    e["Path"] = ";".join(extra) + ";" + e.get("Path", "")
+    e.pop("CCACHE_DISABLE", None)
+    return e
+
+
+def log(msg: str) -> None:
+    line = time.strftime("%H:%M:%S") + " " + msg
+    print(line, flush=True)
+    with PROGRESS.open("a", encoding="utf-8") as f:
+        f.write(line + "\n")
+
+
+def force_sdk_fixes() -> None:
+    sdk = BUILD / "sdkconfig"
+    if not sdk.exists():
+        return
+    text = sdk.read_text(encoding="utf-8")
+    reps = [
+        ("CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y", "# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set"),
+        ("CONFIG_RTC_CLK_SRC_INT_RC=y", "# CONFIG_RTC_CLK_SRC_INT_RC is not set"),
+        ("# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set", "CONFIG_RTC_CLK_SRC_EXT_CRYS=y"),
+        ("CONFIG_ESP_BROWNOUT_DET=n", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("# CONFIG_ESP_BROWNOUT_DET is not set", "CONFIG_ESP_BROWNOUT_DET=y"),
+        ("CONFIG_PM_ENABLE=y", "# CONFIG_PM_ENABLE is not set"),
+        ("CONFIG_RTC_CLK_CAL_CYCLES=0", "CONFIG_RTC_CLK_CAL_CYCLES=1024"),
+    ]
+    for a, b in reps:
+        text = text.replace(a, b)
+    if "CONFIG_RTC_CLK_SRC_EXT_CRYS=y" not in text:
+        text += "\nCONFIG_RTC_CLK_SRC_EXT_CRYS=y\n"
+    if "CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y" not in text:
+        text += "\nCONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y\n"
+    if "CONFIG_RTC_CLK_CAL_CYCLES=1024" not in text:
+        text = re.sub(
+            r"CONFIG_RTC_CLK_CAL_CYCLES=\d+", "CONFIG_RTC_CLK_CAL_CYCLES=1024", text
+        )
+        if "CONFIG_RTC_CLK_CAL_CYCLES=1024" not in text:
+            text += "\nCONFIG_RTC_CLK_CAL_CYCLES=1024\n"
+    sdk.write_text(text, encoding="utf-8")
+
+
+def kill_receiver() -> None:
+    subprocess.run(
+        ["taskkill", "/F", "/IM", "temperature_receiver.exe"],
+        capture_output=True,
+        text=True,
+    )
+    time.sleep(1)
+
+
+def rebuild_receiver() -> None:
+    log("rebuild temperature_receiver")
+    r = subprocess.run(
+        [str(CMAKE), "--build", str(RX_BUILD), "--parallel"],
+        env=env(),
+        capture_output=True,
+        text=True,
+    )
+    if r.returncode != 0:
+        raise RuntimeError("receiver build failed")
+
+
+def start_receiver() -> None:
+    kill_receiver()
+    RX_SESSION.mkdir(parents=True, exist_ok=True)
+    if TSV.exists():
+        TSV.unlink()
+    env2 = env()
+    env2["AE_RECEIVER_SESSION_DIR"] = str(RX_SESSION)
+    env2["AE_DS_TSV"] = str(TSV)
+    env2["AE_DS_BENCH_TAG"] = "ap_aethernetio_3x10"
+    env2["AE_DS_BLOCKS"] = "3"
+    env2["AE_DS_HOT_PER"] = "10"
+    with RX_LOG.open("w", encoding="utf-8") as outf, (
+        ROOT / "experiments" / "prepared_ap_aethernetio_3x10_rx.log.err"
+    ).open("w", encoding="utf-8") as errf:
+        subprocess.Popen(
+            [str(RX_EXE)],
+            cwd=str(RX_SESSION),
+            env=env2,
+            stdout=outf,
+            stderr=errf,
+        )
+    t0 = time.time()
+    while time.time() - t0 < 60:
+        text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+        if "RECEIVER_UID=" in text:
+            log("receiver ready")
+            return
+        time.sleep(1)
+    raise RuntimeError("receiver not ready")
+
+
+def cmake_configure() -> None:
+    args = [
+        str(CMAKE),
+        "-S",
+        str(ROOT),
+        "-B",
+        str(BUILD),
+        "-G",
+        "Ninja",
+        f"-DCPM_aether-client-cpp_SOURCE={AETHER}",
+        "-DAE_EXP_PREPARED_AP_AETHERNETIO_3X10=1",
+        "-DAE_EXP_PREPARED_FINAL_D1_5X50=",
+        "-DAE_EXP_PREPARED_DEEPSLEEP_5X50=",
+        "-DAE_EXP_PREPARED_BOOT_WIFI_OPT=",
+        "-DAE_EXP_PREPARED_BOOT_WIFI_VAL100=",
+        "-DAE_EXP_SKIP_DTOR_SAVE=1",
+        "-DSERVICE_UID=5aade50f-00d9-4624-b097-e203cdcf1e38",
+        "-DBENCH_CLIENT_ID=prepared_deepsleep_5x50_v1",
+        "-DAETHER_PREPARED_NONCE_RESERVE=10",
+        "-DWIFI_SSID=aethernetio",
+        "-DWIFI_PASSWORD=12481632",
+        "-DCMAKE_BUILD_TYPE=Release",
+    ]
+    log("cmake configure ap_aethernetio_3x10")
+    r = subprocess.run(args, cwd=ROOT, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "ap_aethernetio_cmake.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("cmake failed")
+    force_sdk_fixes()
+    log("cmake ok")
+
+
+def ninja_build() -> None:
+    log("ninja build")
+    r = subprocess.run(
+        [str(NINJA), "-C", str(BUILD)], env=env(), capture_output=True, text=True
+    )
+    if r.returncode != 0:
+        (ROOT / "experiments" / "ap_aethernetio_build.err").write_text(
+            (r.stdout or "")[-16000:] + "\n" + (r.stderr or "")[-8000:],
+            encoding="utf-8",
+        )
+        raise RuntimeError("ninja failed")
+    log("build ok")
+
+
+def wait_com_for_flash_only(timeout_s: float = 180.0) -> None:
+    log(f"pre-flash: waiting up to {int(timeout_s)}s for {PORT}")
+    t0 = time.time()
+    while time.time() - t0 < timeout_s:
+        r = subprocess.run(
+            [
+                "powershell",
+                "-NoProfile",
+                "-Command",
+                f"Get-PnpDevice -Class Ports -Status OK | Where-Object {{ $_.FriendlyName -match '{PORT}' }} | Select-Object -ExpandProperty FriendlyName",
+            ],
+            capture_output=True,
+            text=True,
+        )
+        if PORT in (r.stdout or ""):
+            log(f"pre-flash: {PORT} present")
+            return
+        time.sleep(2.0)
+    raise RuntimeError(f"{PORT} not available — reset ESP once")
+
+
+def flash_once() -> None:
+    wait_com_for_flash_only()
+    log(f"flash {PORT}")
+    cmd = [
+        str(PY),
+        "-m",
+        "esptool",
+        "--chip",
+        "esp32c6",
+        "-p",
+        PORT,
+        "-b",
+        "460800",
+        "write-flash",
+        "--flash-size",
+        "4MB",
+        "0x0",
+        str(BUILD / "bootloader" / "bootloader.bin"),
+        "0x8000",
+        str(BUILD / "partition_table" / "partition-table.bin"),
+        "0x10000",
+        str(BUILD / "temperature_sensor.bin"),
+    ]
+    r = subprocess.run(cmd, env=env(), capture_output=True, text=True)
+    if r.returncode != 0:
+        (ROOT / "experiments" / "ap_aethernetio_flash.err").write_text(
+            (r.stdout or "") + "\n" + (r.stderr or ""), encoding="utf-8"
+        )
+        raise RuntimeError("flash failed")
+    log("FLASH_OK — no further COM")
+
+
+def progress() -> tuple[int, int, int]:
+    text = RX_LOG.read_text(encoding="utf-8", errors="replace") if RX_LOG.exists() else ""
+    full_outers = set(
+        int(m.group(1)) for m in re.finditer(r"^DS_FULL outer=(\d+)", text, re.M)
+    )
+    hots = len(re.findall(r"^DS_HOT ", text, re.M))
+    finals = len(
+        re.findall(
+            r"^DS_FINAL|BENCH_DONE ap_aethernetio_3x10|BENCH_DONE deepsleep",
+            text,
+            re.M,
+        )
+    )
+    return len(full_outers), hots, finals
+
+
+def wait_campaign(timeout_s: float = 25 * 60) -> None:
+    log("wait Aether campaign (no COM; stop only on FINAL)")
+    t0 = time.time()
+    last = (-1, -1, -1)
+    while time.time() - t0 < timeout_s:
+        f, h, fin = progress()
+        if (f, h, fin) != last:
+            last = (f, h, fin)
+            log(f"progress unique_full={f} hot={h} final={fin}")
+            text = RX_LOG.read_text(encoding="utf-8", errors="replace")
+            for prefix in ("[BLOCK ", "DS_HOT ", "DS_FULL ", "DS_FINAL", "BENCH_DONE"):
+                lines = [ln for ln in text.splitlines() if ln.startswith(prefix)]
+                if lines:
+                    log("  " + lines[-1][:220])
+                    break
+        if fin > 0:
+            log(f"STOP unique_full={f} hot={h} final={fin}")
+            return
+        time.sleep(2.0)
+    log(f"TIMEOUT unique_full={last[0]} hot={last[1]} final={last[2]}")
+
+
+def main() -> int:
+    if PROGRESS.exists():
+        PROGRESS.write_text("", encoding="utf-8")
+    rebuild_receiver()
+    start_receiver()
+    cmake_configure()
+    ninja_build()
+    flash_once()
+    wait_campaign()
+    kill_receiver()
+    subprocess.run(
+        [str(PY), str(ROOT / "experiments" / "analyze_ap_aethernetio_3x10.py")],
+        cwd=ROOT,
+        check=False,
+    )
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as e:
+        log(f"ERROR {e}")
+        kill_receiver()
+        sys.exit(1)
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 6bb3f20..04d0e57 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -39,6 +39,18 @@ elseif(AE_EXP_PREPARED_DEEPSLEEP_5X50)
     "experiment_early_entry.cpp"
     "prepared_send/prepared_send.cpp"
   )
+elseif(AE_EXP_PREPARED_FINAL_D1_5X50)
+  list(APPEND src_list
+    "prepared_final_d1_5x50_bench.cpp"
+    "experiment_early_entry.cpp"
+    "prepared_send/prepared_send.cpp"
+  )
+elseif(AE_EXP_PREPARED_AP_AETHERNETIO_3X10)
+  list(APPEND src_list
+    "prepared_ap_aethernetio_3x10_bench.cpp"
+    "experiment_early_entry.cpp"
+    "prepared_send/prepared_send.cpp"
+  )
 elseif(AE_EXP_PREPARED_TX_DONE_DIAG)
   list(APPEND src_list
     "prepared_tx_done_diag_bench.cpp"
@@ -203,6 +215,8 @@ set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 "" CACHE STRING "Silent 5x20 keep-Wi-Fi-up
 set(AE_EXP_PREPARED_WIFI_BISECT "" CACHE STRING "Silent single-factor prepared Wi-Fi bisect (set to 1)")
 set(AE_EXP_PREPARED_WIFI_FASTEST "" CACHE STRING "Silent fastest-path prepared campaign (set to 1)")
 set(AE_EXP_PREPARED_DEEPSLEEP_5X50 "" CACHE STRING "Silent deep-sleep 5x50 prepared E2E (set to 1)")
+set(AE_EXP_PREPARED_FINAL_D1_5X50 "" CACHE STRING "Final D1 WIFI_STORAGE_RAM deep-sleep 5x50 (set to 1)")
+set(AE_EXP_PREPARED_AP_AETHERNETIO_3X10 "" CACHE STRING "aethernetio AP 3x10 1s deep sleep (set to 1)")
 set(AE_EXP_PREPARED_TX_DONE_DIAG "" CACHE STRING "Silent TX-done callback diagnostic 1x50 (set to 1)")
 set(AE_EXP_PREPARED_MAC_RETRY_DIAG "" CACHE STRING "Silent MAC retry-limit diagnostic 7x50 (set to 1)")
 set(AE_EXP_PREPARED_BOOT_WIFI_OPT "" CACHE STRING "Silent boot/wifi HOT opt campaign 11x30 (set to 1)")
@@ -262,6 +276,8 @@ ae_exp_define_if_set(AE_EXP_PREPARED_KEEP_WIFI_UP_5X20)
 ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_BISECT)
 ae_exp_define_if_set(AE_EXP_PREPARED_WIFI_FASTEST)
 ae_exp_define_if_set(AE_EXP_PREPARED_DEEPSLEEP_5X50)
+ae_exp_define_if_set(AE_EXP_PREPARED_FINAL_D1_5X50)
+ae_exp_define_if_set(AE_EXP_PREPARED_AP_AETHERNETIO_3X10)
 ae_exp_define_if_set(AE_EXP_PREPARED_TX_DONE_DIAG)
 ae_exp_define_if_set(AE_EXP_PREPARED_MAC_RETRY_DIAG)
 ae_exp_define_if_set(AE_EXP_PREPARED_BOOT_WIFI_OPT)
@@ -284,6 +300,8 @@ ae_exp_define_if_set(AE_EXP_BISECT_SMOKE)
    AE_EXP_PREPARED_KEEP_WIFI_UP_5X20 STREQUAL "1" OR
    AE_EXP_PREPARED_WIFI_FASTEST STREQUAL "1" OR
    AE_EXP_PREPARED_DEEPSLEEP_5X50 STREQUAL "1" OR
+   AE_EXP_PREPARED_FINAL_D1_5X50 STREQUAL "1" OR
+   AE_EXP_PREPARED_AP_AETHERNETIO_3X10 STREQUAL "1" OR
    AE_EXP_PREPARED_TX_DONE_DIAG STREQUAL "1" OR
    AE_EXP_PREPARED_MAC_RETRY_DIAG STREQUAL "1" OR
    AE_EXP_PREPARED_BOOT_WIFI_OPT STREQUAL "1" OR
diff --git a/main/bench_payload.h b/main/bench_payload.h
index 7ad7d6d..33a3f74 100644
--- a/main/bench_payload.h
+++ b/main/bench_payload.h
@@ -338,11 +338,16 @@ struct DsPayload {
   std::uint8_t pending_kind{0};
   std::uint8_t pending_outer{0};
   std::uint8_t pending_hot_index{0};
-  std::uint8_t reserved{0};
+  std::int8_t rssi{0};
+  std::uint8_t actual_channel{0};
+  std::uint8_t disconnect_count{0};
+  std::uint8_t last_disconnect_reason{0};
+  std::uint8_t reconnect_count{0};
+  std::uint8_t failed_assoc_wakes{0};
 };
 #pragma pack(pop)
 
-static_assert(sizeof(DsPayload) == 56, "ds payload size");
+static_assert(sizeof(DsPayload) == 61, "ds payload size");
 
 template 
 inline Buffer EncodeDs(DsPayload const& p) {
diff --git a/main/experiment_early_entry.cpp b/main/experiment_early_entry.cpp
index a0f6012..a692c7e 100644
--- a/main/experiment_early_entry.cpp
+++ b/main/experiment_early_entry.cpp
@@ -8,6 +8,8 @@
 
 #if defined(ESP_PLATFORM) && \
     (defined(AE_EXP_PREPARED_DEEPSLEEP_5X50) || \
+     defined(AE_EXP_PREPARED_FINAL_D1_5X50) || \
+     defined(AE_EXP_PREPARED_AP_AETHERNETIO_3X10) || \
      defined(AE_EXP_PREPARED_TX_DONE_DIAG) || \
      defined(AE_EXP_PREPARED_MAC_RETRY_DIAG) || \
      defined(AE_EXP_PREPARED_BOOT_WIFI_OPT) || \
diff --git a/main/experiment_early_entry.h b/main/experiment_early_entry.h
index c2a069a..03fcb66 100644
--- a/main/experiment_early_entry.h
+++ b/main/experiment_early_entry.h
@@ -20,6 +20,8 @@ struct ExperimentEarlyEntrySnapshot {
 
 #if defined(ESP_PLATFORM) && \
     (defined(AE_EXP_PREPARED_DEEPSLEEP_5X50) || \
+     defined(AE_EXP_PREPARED_FINAL_D1_5X50) || \
+     defined(AE_EXP_PREPARED_AP_AETHERNETIO_3X10) || \
      defined(AE_EXP_PREPARED_TX_DONE_DIAG) || \
      defined(AE_EXP_PREPARED_MAC_RETRY_DIAG) || \
      defined(AE_EXP_PREPARED_BOOT_WIFI_OPT) || \
diff --git a/main/prepared_ap_aethernetio_3x10_bench.cpp b/main/prepared_ap_aethernetio_3x10_bench.cpp
new file mode 100644
index 0000000..ad83a4b
--- /dev/null
+++ b/main/prepared_ap_aethernetio_3x10_bench.cpp
@@ -0,0 +1,963 @@
+/*
+ * Copyright 2026 Aethernet Inc.
+ *
+ * Final production-like D1 validation on alternate AP (ESP32-C6).
+ * Fastest prepared path + WIFI_STORAGE_RAM (D1 winner).
+ * 3 FULL x 10 HOT, 1 s deep sleep. DsPayload; UART silent.
+ * AP credentials: aethernetio (compile-time WIFI_SSID/PASSWORD).
+ */
+
+#include 
+#include 
+#include 
+
+#include "aether/all.h"
+#include "aether/ae_exp_wifi.h"
+#include "aether/config.h"
+#include "aether/env.h"
+#include "bench_payload.h"
+#include "experiment_early_entry.h"
+#include "prepared_send/prepared_send.h"
+
+#if defined(ESP_PLATFORM)
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#endif
+
+using namespace std::chrono_literals;
+
+#if defined(ESP_PLATFORM)
+extern "C" std::uint64_t esp_rtc_get_time_us(void);
+#endif
+
+namespace temp_sensor {
+namespace {
+
+static constexpr auto kParentUid =
+    ae::Uid::FromString("b1ac52c8-8d94-bd39-4c01-a631ac594165");
+
+#ifndef BENCH_CLIENT_ID
+#  define BENCH_CLIENT_ID "prepared_deepsleep_5x50_v1"
+#endif
+static constexpr char const* kBenchClientId = BENCH_CLIENT_ID;
+
+#if defined(SERVICE_UID)
+static constexpr auto kServiceUid = ae::Uid::FromString(SERVICE_UID);
+#else
+static constexpr auto kServiceUid =
+    ae::Uid::FromString("5aade50f-00d9-4624-b097-e203cdcf1e38");
+#endif
+
+static constexpr std::uint8_t kOuterCycles = 3;
+static constexpr std::uint8_t kHotPerOuter = 10;
+static constexpr std::uint8_t kMaxHotAttemptsPerBlock = 25;
+static constexpr std::uint32_t kSleepUs = 1000000;
+
+static constexpr std::uint32_t kRtcMagic = 0x41455431u;  // "AET1"
+static constexpr std::uint16_t kRtcVersion = 1;
+
+enum class Phase : std::uint16_t {
+  kRegister = 0,
+  kFull = 1,
+  kHot = 2,
+  kFinal = 3,
+  kDone = 4,
+};
+
+struct RtcState {
+  std::uint32_t magic;
+  std::uint16_t version;
+  std::uint16_t phase;
+  std::uint8_t outer_cycle;
+  std::uint8_t hot_index;
+  std::uint8_t hot_attempt_count;
+  std::uint8_t hot_send_count;
+  std::uint16_t sequence_global;
+  std::uint16_t next_record_id;
+  std::uint32_t requested_sleep_us;
+  std::uint64_t sleep_arm_rtc_us;
+  std::uint8_t pending_valid;
+  std::uint8_t pending_kind;
+  std::uint8_t pending_outer;
+  std::uint8_t pending_hot_index;
+  std::uint32_t pending_user_cycle_us;
+  std::uint32_t pending_wifi_cycle_us;
+  std::uint32_t pending_connect_us;
+  std::uint32_t pending_txdone_us;
+  std::uint32_t pending_teardown_us;
+  std::uint8_t pending_cb_seen;
+  std::uint8_t pending_cb_timeout;
+  std::uint8_t pending_auth;
+  std::uint8_t pending_disconnect_count;
+  std::uint8_t pending_last_disc_reason;
+  std::uint8_t pending_reconnect_count;
+  std::int8_t pending_rssi;
+  std::uint8_t pending_actual_channel;
+  std::uint8_t failed_assoc_wakes;
+  std::uint8_t brownout_count;
+  std::uint8_t unexpected_reset_count;
+  std::uint8_t recovery_full_count;
+  std::uint8_t current_boot_brownout;
+  std::uint8_t registered;
+  std::uint8_t final_fail_count;
+  std::uint8_t pad0;
+  std::uint16_t pad1;
+  std::uint32_t crc;
+};
+
+#if defined(ESP_PLATFORM)
+RTC_DATA_ATTR static RtcState g_rtc{};
+RTC_DATA_ATTR static prepared_send::PreparedWifiRtcCache g_rtc_wifi_cache{};
+
+static const auto kWifiInit = ae::WiFiInit{
+    std::vector{{ae::WifiCreds{WIFI_SSID, WIFI_PASSWORD}, {}}},
+    {},
+};
+
+static bool g_had_aether_app = false;
+
+static std::shared_ptr g_app;
+static ae::Client::ptr g_client;
+static std::unique_ptr g_stream;
+static ae::Subscription g_select_sub;
+static ae::Subscription g_stream_sub;
+static ae::Subscription g_write_sub;
+
+static bool g_write_armed = false;
+static bool g_write_ok = false;
+static bool g_exit_success = false;
+static bool g_pending_register_finish = false;
+static bool g_pending_full_post_write = false;
+static bool g_pending_final_exit = false;
+static bool g_done = false;
+
+static ExperimentEarlyEntrySnapshot g_early{};
+static std::uint32_t g_sleep_elapsed_us = 0;
+static std::uint32_t g_sleep_overhead_us = 0;
+static prepared_send::FastPathConfig g_cfg{};
+static prepared_send::BisectWifiCacheSnapshot g_wifi_snapshot{};
+
+static std::uint32_t Crc32Bytes(void const* data, std::size_t len) {
+  auto const* p = static_cast(data);
+  std::uint32_t crc = 0xffffffffu;
+  for (std::size_t i = 0; i < len; ++i) {
+    crc ^= p[i];
+    for (int b = 0; b < 8; ++b) {
+      std::uint32_t const mask = -(crc & 1u);
+      crc = (crc >> 1) ^ (0xedb88320u & mask);
+    }
+  }
+  return ~crc;
+}
+
+static std::uint32_t ComputeCrc(RtcState const& st) {
+  RtcState tmp = st;
+  tmp.crc = 0;
+  return Crc32Bytes(&tmp, sizeof(tmp));
+}
+
+static void SetCrc(RtcState& st) { st.crc = ComputeCrc(st); }
+
+static bool ValidateRtcState(RtcState const& st) {
+  if (st.magic != kRtcMagic || st.version != kRtcVersion) {
+    return false;
+  }
+  if (ComputeCrc(st) != st.crc) {
+    return false;
+  }
+  if (st.phase > static_cast(Phase::kDone)) {
+    return false;
+  }
+  if (st.outer_cycle > kOuterCycles) {
+    return false;
+  }
+  if (st.hot_index > kHotPerOuter) {
+    return false;
+  }
+  return true;
+}
+
+static void ClearPending(RtcState& st) {
+  st.pending_valid = 0;
+  st.pending_kind = static_cast(bench::DsPendingKind::kNone);
+  st.pending_outer = 0;
+  st.pending_hot_index = 0;
+  st.pending_user_cycle_us = 0;
+  st.pending_wifi_cycle_us = 0;
+  st.pending_connect_us = 0;
+  st.pending_txdone_us = 0;
+  st.pending_teardown_us = 0;
+  st.pending_cb_seen = 0;
+  st.pending_cb_timeout = 0;
+  st.pending_auth = 0;
+  st.pending_disconnect_count = 0;
+  st.pending_last_disc_reason = 0;
+  st.pending_reconnect_count = 0;
+  st.pending_rssi = 0;
+  st.pending_actual_channel = 0;
+}
+
+static void InvalidateWifiRtcCache() {
+  g_rtc_wifi_cache = prepared_send::PreparedWifiRtcCache{};
+}
+
+static void InitRtcFresh(Phase phase) {
+  g_rtc = RtcState{};
+  g_rtc.magic = kRtcMagic;
+  g_rtc.version = kRtcVersion;
+  g_rtc.phase = static_cast(phase);
+  g_rtc.outer_cycle = (phase == Phase::kFull || phase == Phase::kHot) ? 1 : 0;
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  g_rtc.sequence_global = 0;
+  g_rtc.next_record_id = 1;
+  g_rtc.requested_sleep_us = 0;
+  g_rtc.sleep_arm_rtc_us = 0;
+  ClearPending(g_rtc);
+  g_rtc.failed_assoc_wakes = 0;
+  g_rtc.brownout_count = 0;
+  g_rtc.unexpected_reset_count = 0;
+  g_rtc.recovery_full_count = 0;
+  g_rtc.current_boot_brownout = 0;
+  g_rtc.registered = 0;
+  g_rtc.final_fail_count = 0;
+  InvalidateWifiRtcCache();
+  SetCrc(g_rtc);
+}
+
+[[noreturn]] static void PrepareRtcStateAndDeepSleep(
+    std::uint32_t requested_us) {
+  g_rtc.requested_sleep_us = requested_us;
+  esp_sleep_enable_timer_wakeup(requested_us);
+  g_rtc.sleep_arm_rtc_us = esp_rtc_get_time_us();
+  SetCrc(g_rtc);
+
+#  if SOC_PM_SUPPORT_RTC_SLOW_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
+#  endif
+#  if SOC_PM_SUPPORT_RTC_FAST_MEM_PD
+  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
+#  endif
+
+  esp_err_t const ret = esp_deep_sleep_try_to_start();
+  (void)ret;
+  esp_deep_sleep_start();
+  for (;;) {
+  }
+}
+
+static void ForceFullRecovery() {
+  ClearPending(g_rtc);
+  g_rtc.phase = static_cast(Phase::kFull);
+  if (g_rtc.outer_cycle == 0 || g_rtc.outer_cycle > kOuterCycles) {
+    g_rtc.outer_cycle = 1;
+  }
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  if (g_rtc.recovery_full_count < 255) {
+    ++g_rtc.recovery_full_count;
+  }
+  SetCrc(g_rtc);
+}
+
+static void ComputeWakeMetrics() {
+  g_sleep_elapsed_us = 0;
+  g_sleep_overhead_us = 0;
+  auto const reset =
+      static_cast(g_early.reset_reason);
+  if (reset == ESP_RST_DEEPSLEEP && g_rtc.sleep_arm_rtc_us != 0 &&
+      g_early.app_entry_rtc_us >= g_rtc.sleep_arm_rtc_us) {
+    auto const elapsed = g_early.app_entry_rtc_us - g_rtc.sleep_arm_rtc_us;
+    g_sleep_elapsed_us =
+        elapsed > 0xffffffffull ? 0xffffffffu
+                                : static_cast(elapsed);
+    if (g_sleep_elapsed_us > g_rtc.requested_sleep_us) {
+      g_sleep_overhead_us = g_sleep_elapsed_us - g_rtc.requested_sleep_us;
+    }
+  }
+}
+
+static std::uint16_t NextSeq() {
+  ++g_rtc.sequence_global;
+  return g_rtc.sequence_global;
+}
+
+static void AdvanceRecordIdAfterFlush() {
+  if (g_rtc.next_record_id < 0xffffu) {
+    ++g_rtc.next_record_id;
+  }
+}
+
+static void FillWakeFields(bench::DsPayload& p) {
+  p.reset_reason = g_early.reset_reason;
+  p.wake_cause = g_early.wakeup_cause;
+  p.brownout_count = g_rtc.brownout_count;
+  p.unexpected_reset_count = g_rtc.unexpected_reset_count;
+  p.requested_sleep_us = g_rtc.requested_sleep_us;
+  p.sleep_elapsed_to_app_us = g_sleep_elapsed_us;
+  p.sleep_to_app_overhead_us = g_sleep_overhead_us;
+  p.app_entry_esp_timer_us =
+      g_early.app_entry_esp_timer_us < 0
+          ? 0
+          : static_cast(g_early.app_entry_esp_timer_us);
+
+  std::uint8_t flags = 0;
+  if (g_rtc.current_boot_brownout) {
+    flags |= static_cast(bench::DsFlags::kBrownout);
+  }
+  if (prepared_send::PreparedWifiRtcCacheIsValid(g_rtc_wifi_cache)) {
+    flags |= static_cast(bench::DsFlags::kCacheValid);
+  }
+  if (ValidateRtcState(g_rtc)) {
+    flags |= static_cast(bench::DsFlags::kStateValid);
+  }
+  p.flags = flags;
+  p.failed_assoc_wakes = g_rtc.failed_assoc_wakes;
+}
+
+static void FillPendingFields(bench::DsPayload& p) {
+  if (!g_rtc.pending_valid) {
+    p.pending_kind = static_cast(bench::DsPendingKind::kNone);
+    p.pending_outer = 0;
+    p.pending_hot_index = 0;
+    p.pending_user_cycle_us = 0;
+    p.pending_wifi_cycle_us = 0;
+    p.connect_us = 0;
+    p.tx_done_wait_us = 0;
+    p.teardown_us = 0;
+    p.negotiated_auth = 0;
+    p.disconnect_count = 0;
+    p.last_disconnect_reason = 0;
+    p.reconnect_count = 0;
+    p.rssi = 0;
+    p.actual_channel = 0;
+    return;
+  }
+  p.pending_kind = g_rtc.pending_kind;
+  p.pending_outer = g_rtc.pending_outer;
+  p.pending_hot_index = g_rtc.pending_hot_index;
+  p.pending_user_cycle_us = g_rtc.pending_user_cycle_us;
+  p.pending_wifi_cycle_us = g_rtc.pending_wifi_cycle_us;
+  p.connect_us = g_rtc.pending_connect_us;
+  p.tx_done_wait_us = g_rtc.pending_txdone_us;
+  p.teardown_us = g_rtc.pending_teardown_us;
+  p.negotiated_auth = g_rtc.pending_auth;
+  p.disconnect_count = g_rtc.pending_disconnect_count;
+  p.last_disconnect_reason = g_rtc.pending_last_disc_reason;
+  p.reconnect_count = g_rtc.pending_reconnect_count;
+  p.rssi = g_rtc.pending_rssi;
+  p.actual_channel = g_rtc.pending_actual_channel;
+  if (g_rtc.pending_cb_seen) {
+    p.flags |= static_cast(bench::DsFlags::kCallbackSeen);
+  }
+  if (g_rtc.pending_cb_timeout) {
+    p.flags |= static_cast(bench::DsFlags::kCallbackTimeout);
+  }
+}
+
+static ae::DataBuffer MakeDsPayload(bench::DsMsgType type) {
+  bench::DsPayload p{};
+  p.type = static_cast(type);
+  p.outer_cycle = g_rtc.outer_cycle;
+  p.hot_index = g_rtc.hot_index;
+  p.sequence_global = NextSeq();
+  // Assign id without advancing until the send that flushes pending succeeds
+  // (HOT Wi-Fi retries must reuse the same record_id).
+  p.record_id = g_rtc.pending_valid ? g_rtc.next_record_id : 0;
+  FillWakeFields(p);
+  FillPendingFields(p);
+  p.prepared_message_left =
+      static_cast(prepared_send::PreparedMessageLeft() > 0xffffu
+                                     ? 0xffffu
+                                     : prepared_send::PreparedMessageLeft());
+  return bench::EncodeDs(p);
+}
+
+static void StorePendingFull(std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = static_cast(bench::DsPendingKind::kFull);
+  g_rtc.pending_outer = g_rtc.outer_cycle;
+  g_rtc.pending_hot_index = 0;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = user_cycle_us;
+  g_rtc.pending_connect_us = 0;
+  g_rtc.pending_txdone_us = 0;
+  g_rtc.pending_teardown_us = 0;
+  g_rtc.pending_cb_seen = 0;
+  g_rtc.pending_cb_timeout = 0;
+  g_rtc.pending_auth = 0;
+  g_rtc.pending_disconnect_count = 0;
+  g_rtc.pending_last_disc_reason = 0;
+  g_rtc.pending_reconnect_count = 0;
+  g_rtc.pending_rssi = 0;
+  g_rtc.pending_actual_channel = 0;
+}
+
+static void StorePendingHot(prepared_send::FastSendResult const& result,
+                            std::uint32_t user_cycle_us) {
+  g_rtc.pending_valid = 1;
+  g_rtc.pending_kind = static_cast(bench::DsPendingKind::kHot);
+  g_rtc.pending_outer = g_rtc.outer_cycle;
+  g_rtc.pending_hot_index = g_rtc.hot_index;
+  g_rtc.pending_user_cycle_us = user_cycle_us;
+  g_rtc.pending_wifi_cycle_us = result.cycle_us;
+  g_rtc.pending_connect_us = result.connect_us;
+  g_rtc.pending_txdone_us = result.tx_done_wait_us;
+  g_rtc.pending_teardown_us = result.teardown_us;
+  g_rtc.pending_cb_seen = result.cb_any;
+  g_rtc.pending_cb_timeout = result.cb_timeout;
+  g_rtc.pending_auth = result.negotiated_auth;
+  g_rtc.pending_disconnect_count = result.disconnect_count;
+  g_rtc.pending_last_disc_reason = result.last_disconnect_reason;
+  g_rtc.pending_reconnect_count = result.reconnect_count;
+  g_rtc.pending_rssi = result.rssi;
+  g_rtc.pending_actual_channel = result.actual_channel;
+}
+
+static void ReleaseApp() {
+  g_select_sub.Reset();
+  g_stream_sub.Reset();
+  g_write_sub.Reset();
+  g_stream.reset();
+  g_client = {};
+  g_app.reset();
+}
+
+static void PreConstructCleanup() {
+  if (!g_had_aether_app) {
+    return;
+  }
+#  if !AE_WIFI_USE_FULL_DEINIT
+  esp_netif_deinit();
+  esp_event_loop_delete_default();
+#  endif
+}
+
+static void ConstructAether() {
+  PreConstructCleanup();
+  g_had_aether_app = true;
+  g_app = ae::AetherApp::Construct(
+      ae::AetherAppContext{}
+#  if AE_DISTILLATION
+          .AddAdapterFactory([&](ae::AetherAppContext const& ctx) {
+            return ae::WifiAdapter::ptr::Create(
+                ae::CreateWith{ctx.domain()}.with_id(
+                    ae::GlobalId::kWiFiAdapter),
+                ctx.aether(), ctx.poller(), ctx.dns_resolver(), kWifiInit);
+          })
+#  endif
+  );
+}
+
+static prepared_send::FastPathConfig MakeFastConfig() {
+  prepared_send::FastPathConfig c{};
+  c.use_bssid = false;
+  c.use_channel = true;
+  c.use_fast_scan = false;
+  c.use_static_ip = true;
+  c.use_static_arp = true;
+  c.ampdu_tx_off = false;
+  c.wifi_storage_ram = true;  // D1 winner
+  c.wifi_nvs_enable = true;
+  c.auth = prepared_send::FastAuthMode::kWpa2;
+  c.retry_max = 10;
+  c.pre_delay_ms = 25;
+  c.post_delay_ms = 0;
+  c.post_mode = prepared_send::FastPostMode::kTxDoneCb;
+  return c;
+}
+
+static void DoFullWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto payload = MakeDsPayload(bench::DsMsgType::kFull);
+  auto& wa = g_stream->Write(std::move(payload));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_full_post_write = true;
+  });
+}
+
+static void MaybeFullWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFullWrite();
+}
+
+static void OnFullClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFullWrite(); });
+  MaybeFullWrite();
+}
+
+static void StartRegister() {
+  g_write_armed = false;
+  g_pending_register_finish = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       g_client = std::move(res).value();
+                       g_pending_register_finish = true;
+                     });
+}
+
+static void StartFull() {
+  g_write_armed = false;
+  g_pending_full_post_write = false;
+  g_write_ok = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFullClientReady(std::move(res).value());
+                     });
+}
+
+static void DoFinalWrite() {
+  if (g_write_armed) {
+    return;
+  }
+  g_write_armed = true;
+  auto& wa = g_stream->Write(MakeDsPayload(bench::DsMsgType::kFinal));
+  g_write_sub = wa.status_event().Subscribe([](ae::WriteAction::Status st) {
+    g_write_ok = (st == ae::WriteAction::Status::kSuccess);
+    g_pending_final_exit = true;
+  });
+}
+
+static void MaybeFinalWrite() {
+  if (!g_stream || g_write_armed) {
+    return;
+  }
+  if (!g_stream->stream_info().is_writable) {
+    return;
+  }
+  DoFinalWrite();
+}
+
+static void OnFinalClientReady(ae::Client::ptr client_ptr) {
+  g_client = std::move(client_ptr);
+  auto client = g_client.Load();
+  g_stream = std::make_unique(*g_app, client, kServiceUid,
+                                             ae::P2pPortHandle{});
+  g_stream_sub =
+      g_stream->stream_update_event().Subscribe([]() { MaybeFinalWrite(); });
+  MaybeFinalWrite();
+}
+
+static void StartFinal() {
+  g_write_armed = false;
+  g_pending_final_exit = false;
+  g_write_ok = false;
+  g_exit_success = false;
+  ConstructAether();
+  g_select_sub = g_app->aether()
+                     ->SelectClient(kParentUid, kBenchClientId)
+                     .result_event()
+                     .Subscribe([](ae::Result res) {
+                       if (!res) {
+                         g_app->Exit(1);
+                         return;
+                       }
+                       OnFinalClientReady(std::move(res).value());
+                     });
+}
+
+static void FinishRegisterInLoop() {
+  g_app->aether().Save();
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFullPostWriteInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  if (!prepared_send::CapturePreparedWifiRtcCache(&g_rtc_wifi_cache)) {
+    g_app->Exit(1);
+    return;
+  }
+  if (!prepared_send::ExportPreparedSendBlock(g_client, kServiceUid,
+                                              kHotPerOuter)) {
+    g_app->Exit(1);
+    return;
+  }
+  if (!prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() !=
+          static_cast(kHotPerOuter)) {
+    g_app->Exit(1);
+    return;
+  }
+  g_app->aether().Save();
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static void FinishFinalInLoop() {
+  if (!g_write_ok) {
+    g_app->Exit(1);
+    return;
+  }
+  g_exit_success = true;
+  g_app->Exit(0);
+}
+
+static std::uint32_t UserCycleFromAppEntry() {
+  auto const now = esp_timer_get_time();
+  auto const entry = g_early.app_entry_esp_timer_us;
+  if (now < entry) {
+    return 0;
+  }
+  auto const delta = now - entry;
+  return delta > 0xffffffffll ? 0xffffffffu
+                              : static_cast(delta);
+}
+
+static void AfterRegisterComplete() {
+  ReleaseApp();
+  g_rtc.registered = 1;
+  g_rtc.phase = static_cast(Phase::kFull);
+  g_rtc.outer_cycle = 1;
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  ClearPending(g_rtc);
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFullComplete() {
+  ReleaseApp();
+  prepared_send::ReleaseFullAetherWifiForHotPath();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  auto const user_cycle = UserCycleFromAppEntry();
+  StorePendingFull(user_cycle);
+  g_rtc.phase = static_cast(Phase::kHot);
+  g_rtc.hot_index = 1;
+  g_rtc.hot_attempt_count = 0;
+  g_rtc.hot_send_count = 0;
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalComplete() {
+  ReleaseApp();
+  if (g_rtc.pending_valid) {
+    AdvanceRecordIdAfterFlush();
+  }
+  ClearPending(g_rtc);
+  g_rtc.final_fail_count = 0;
+  g_rtc.phase = static_cast(Phase::kDone);
+  SetCrc(g_rtc);
+  g_done = true;
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void AfterFinalFailed() {
+  ReleaseApp();
+  if (g_rtc.final_fail_count < 255) {
+    ++g_rtc.final_fail_count;
+  }
+  // After several Aether FINAL failures, stop the campaign so metrics already
+  // delivered (via pending on HOT/FULL) are not blocked forever.
+  if (g_rtc.final_fail_count >= 5) {
+    ClearPending(g_rtc);
+    g_rtc.phase = static_cast(Phase::kDone);
+    SetCrc(g_rtc);
+    g_done = true;
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static bool WifiFailedBeforeEncode(prepared_send::FastSendResult const& r) {
+  if (r.status == prepared_send::HotSendStatus::kWifiFailed) {
+    return true;
+  }
+  bool const encode_ok =
+      (r.status_flags &
+       static_cast(bench::BisectStatusBits::kEncodeOk)) != 0;
+  return !encode_ok && r.status != prepared_send::HotSendStatus::kSent;
+}
+
+static void RunHotOnce() {
+  if (!prepared_send::PreparedWifiRtcCacheIsValid(g_rtc_wifi_cache)) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  g_wifi_snapshot =
+      prepared_send::SnapshotFromPreparedWifiRtcCache(g_rtc_wifi_cache);
+  if (!g_wifi_snapshot.valid_ip || g_wifi_snapshot.channel == 0) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  if (!prepared_send::HasPreparedSendBlock() ||
+      prepared_send::PreparedMessageLeft() == 0) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+  if (g_rtc.hot_attempt_count >= kMaxHotAttemptsPerBlock) {
+    ForceFullRecovery();
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (g_rtc.hot_attempt_count < 255) {
+    ++g_rtc.hot_attempt_count;
+  }
+  SetCrc(g_rtc);
+
+  auto payload = MakeDsPayload(bench::DsMsgType::kHot);
+  auto const result =
+      prepared_send::SendPreparedOnceWithFastPath(g_cfg, payload,
+                                                    &g_wifi_snapshot);
+
+  if (result.status == prepared_send::HotSendStatus::kSent) {
+    auto const user_cycle = UserCycleFromAppEntry();
+    if (g_rtc.hot_send_count < 255) {
+      ++g_rtc.hot_send_count;
+    }
+    bool const flushed_prior = g_rtc.pending_valid != 0;
+    StorePendingHot(result, user_cycle);
+    if (flushed_prior) {
+      AdvanceRecordIdAfterFlush();
+    }
+
+    if (g_rtc.hot_index < 255) {
+      ++g_rtc.hot_index;
+    }
+    if (g_rtc.hot_index > kHotPerOuter) {
+      if (g_rtc.outer_cycle < kOuterCycles) {
+        ++g_rtc.outer_cycle;
+        g_rtc.phase = static_cast(Phase::kFull);
+        g_rtc.hot_index = 1;
+        g_rtc.hot_attempt_count = 0;
+        g_rtc.hot_send_count = 0;
+      } else {
+        g_rtc.phase = static_cast(Phase::kFinal);
+      }
+    }
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+  }
+
+  if (WifiFailedBeforeEncode(result)) {
+    if (g_rtc.failed_assoc_wakes < 255) {
+      ++g_rtc.failed_assoc_wakes;
+    }
+    SetCrc(g_rtc);
+    PrepareRtcStateAndDeepSleep(kSleepUs);
+    return;
+  }
+
+  // Encode/send failure after Wi-Fi: do not advance; retry same index.
+  SetCrc(g_rtc);
+  PrepareRtcStateAndDeepSleep(kSleepUs);
+}
+
+static void PrepareRtcOnBoot() {
+  g_early = GetExperimentEarlyEntrySnapshot();
+  auto const reset =
+      static_cast(g_early.reset_reason);
+  bool const valid = ValidateRtcState(g_rtc);
+
+  g_rtc.current_boot_brownout = 0;
+
+  if (reset == ESP_RST_BROWNOUT) {
+    if (valid) {
+      if (g_rtc.brownout_count < 255) {
+        ++g_rtc.brownout_count;
+      }
+      ClearPending(g_rtc);
+    } else {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.brownout_count = 1;
+      g_rtc.outer_cycle = 1;
+    }
+    g_rtc.current_boot_brownout = 1;
+    ForceFullRecovery();
+  } else if (reset != ESP_RST_DEEPSLEEP || !valid) {
+    bool const first_poweron = (reset == ESP_RST_POWERON);
+    if (g_rtc.magic != kRtcMagic) {
+      InvalidateWifiRtcCache();
+    }
+    if (first_poweron && (!valid || !g_rtc.registered)) {
+      InitRtcFresh(Phase::kRegister);
+    } else if (!valid) {
+      InitRtcFresh(Phase::kFull);
+      g_rtc.unexpected_reset_count = 1;
+      g_rtc.outer_cycle = 1;
+      g_rtc.registered = 1;  // SPIFFS client may already exist
+      SetCrc(g_rtc);
+    } else {
+      if (g_rtc.unexpected_reset_count < 255) {
+        ++g_rtc.unexpected_reset_count;
+      }
+      ForceFullRecovery();
+    }
+  }
+  // else: DEEPSLEEP + valid — continue phase as stored
+
+  ComputeWakeMetrics();
+  SetCrc(g_rtc);
+}
+
+#endif  // ESP_PLATFORM
+
+}  // namespace
+}  // namespace temp_sensor
+
+#if defined(ESP_PLATFORM)
+
+void setup() {
+  using namespace temp_sensor;
+  nvs_flash_init();
+  g_cfg = MakeFastConfig();
+  g_done = false;
+  g_pending_register_finish = false;
+  g_pending_full_post_write = false;
+  g_pending_final_exit = false;
+  PrepareRtcOnBoot();
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kDone) {
+    g_done = true;
+    return;
+  }
+  if (phase == Phase::kRegister) {
+    StartRegister();
+    return;
+  }
+  if (phase == Phase::kFull) {
+    StartFull();
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    StartFinal();
+    return;
+  }
+  // HOT is handled synchronously in loop().
+}
+
+void loop() {
+  using namespace temp_sensor;
+  if (g_done) {
+    vTaskDelay(pdMS_TO_TICKS(1000));
+    return;
+  }
+
+  auto const phase = static_cast(g_rtc.phase);
+  if (phase == Phase::kHot) {
+    RunHotOnce();
+    return;
+  }
+
+  auto process_deferred = []() {
+    if (g_app && g_pending_register_finish) {
+      g_pending_register_finish = false;
+      FinishRegisterInLoop();
+      return true;
+    }
+    if (g_app && g_pending_full_post_write) {
+      g_pending_full_post_write = false;
+      FinishFullPostWriteInLoop();
+      return true;
+    }
+    if (g_app && g_pending_final_exit) {
+      g_pending_final_exit = false;
+      FinishFinalInLoop();
+      return true;
+    }
+    return false;
+  };
+
+  if (process_deferred()) {
+    return;
+  }
+
+  if (!g_app) {
+    return;
+  }
+
+  if (!g_app->IsExited()) {
+    auto t = g_app->Update(ae::Now());
+    if (process_deferred()) {
+      return;
+    }
+    if (!g_app->IsExited()) {
+      g_app->WaitUntil(t);
+    }
+    return;
+  }
+
+  if (phase == Phase::kRegister) {
+    if (g_exit_success) {
+      AfterRegisterComplete();
+    } else {
+      ReleaseApp();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFull) {
+    if (g_exit_success) {
+      AfterFullComplete();
+    } else {
+      ReleaseApp();
+      ForceFullRecovery();
+      PrepareRtcStateAndDeepSleep(kSleepUs);
+    }
+    return;
+  }
+  if (phase == Phase::kFinal) {
+    if (g_exit_success) {
+      AfterFinalComplete();
+    } else {
+      AfterFinalFailed();
+    }
+    return;
+  }
+}
+
+#else
+
+void setup() {}
+void loop() {}
+
+#endif
diff --git a/temperature_receiver/main.cpp b/temperature_receiver/main.cpp
index ff6c946..8e9bf3b 100644
--- a/temperature_receiver/main.cpp
+++ b/temperature_receiver/main.cpp
@@ -89,6 +89,31 @@ int g_dup_records = 0;
 int g_ooo = 0;
 int g_max_record = 0;
 int g_brownout_boots = 0;
+int g_failed_assoc_wakes = 0;
+
+int DsBlockCount() {
+#if defined(_WIN32)
+  if (char const* env = std::getenv("AE_DS_BLOCKS")) {
+    int const v = std::atoi(env);
+    if (v >= 1 && v <= 8) {
+      return v;
+    }
+  }
+#endif
+  return 5;
+}
+
+int DsHotPerBlock() {
+#if defined(_WIN32)
+  if (char const* env = std::getenv("AE_DS_HOT_PER")) {
+    int const v = std::atoi(env);
+    if (v >= 1 && v <= 100) {
+      return v;
+    }
+  }
+#endif
+  return 50;
+}
 
 std::filesystem::path TsvPath() {
 #if defined(_WIN32)
@@ -535,13 +560,50 @@ void OnTxDiag(temp_sensor::bench::TxDiagPayload const& p) {
 
 void OnDs(temp_sensor::bench::DsPayload const& p) {
   auto const type = static_cast(p.type);
+  static int hot_recv_by_outer[8] = {};
+  static int hot_cb_by_outer[8] = {};
+  static int hot_to_by_outer[8] = {};
+  static std::uint32_t last_full_user[8] = {};
+  static int printed_block = 0;
+
+  char const* bench_tag = "deepsleep_5x50";
+#if defined(_WIN32)
+  if (char const* env = std::getenv("AE_DS_BENCH_TAG")) {
+    if (env[0] != '\0') {
+      bench_tag = env;
+    }
+  }
+#endif
+
   if (type == temp_sensor::bench::DsMsgType::kFull) {
     ++g_full_recv;
+    std::cout << "DS_FULL outer=" << static_cast(p.outer_cycle)
+              << " seq=" << p.sequence_global << "\n";
   } else if (type == temp_sensor::bench::DsMsgType::kHot) {
     ++g_hot_recv;
+    auto const outer = p.pending_kind == 2 ? p.pending_outer : p.outer_cycle;
+    if (outer >= 1 && outer <= 5) {
+      ++hot_recv_by_outer[outer];
+      if (p.flags & static_cast(
+                        temp_sensor::bench::DsFlags::kCallbackSeen)) {
+        ++hot_cb_by_outer[outer];
+      }
+      if (p.flags & static_cast(
+                        temp_sensor::bench::DsFlags::kCallbackTimeout)) {
+        ++hot_to_by_outer[outer];
+      }
+    }
+    std::cout << "DS_HOT B" << static_cast(outer) << " "
+              << (outer <= 5 ? hot_recv_by_outer[outer] : 0) << "/50"
+              << " user=" << (p.pending_user_cycle_us / 1000.0) << "ms"
+              << " wifi=" << (p.pending_wifi_cycle_us / 1000.0) << "ms"
+              << " wake_ov=" << (p.sleep_to_app_overhead_us / 1000.0) << "ms"
+              << "\n";
   } else if (type == temp_sensor::bench::DsMsgType::kFinal) {
     ++g_final_recv;
+    std::cout << "DS_FINAL seq=" << p.sequence_global << "\n";
   }
+
   Meas m{};
   m.record_id = p.record_id;
   m.kind = p.pending_kind;
@@ -569,9 +631,60 @@ void OnDs(temp_sensor::bench::DsPayload const& p) {
           : 0;
   m.auth = p.negotiated_auth;
   m.seq = p.sequence_global;
+  m.disconnect_count = p.disconnect_count;
+  m.last_disconnect_reason = p.last_disconnect_reason;
+  m.reconnect_count = p.reconnect_count;
+  m.rssi = p.rssi;
+  m.actual_channel = p.actual_channel;
+  if (p.failed_assoc_wakes > g_failed_assoc_wakes) {
+    g_failed_assoc_wakes = p.failed_assoc_wakes;
+  }
   NoteRecord(m);
+
+  int const block_count = DsBlockCount();
+  int const hot_per = DsHotPerBlock();
+  if (p.pending_kind == 1 && p.pending_outer >= 1 &&
+      p.pending_outer <= static_cast(block_count)) {
+    last_full_user[p.pending_outer] = p.pending_user_cycle_us;
+  }
+
+  // Block summary when a FULL of next outer arrives (prev outer complete) or FINAL.
+  auto print_block = [&](int b) {
+    if (b < 1 || b > block_count || b <= printed_block) {
+      return;
+    }
+    printed_block = b;
+    std::vector users;
+    std::vector wifis;
+    std::vector wakes;
+    for (auto const& mm : g_meas) {
+      if (mm.kind == 2 && mm.outer == b) {
+        users.push_back(mm.user_us);
+        wifis.push_back(mm.wifi_us);
+        wakes.push_back(mm.sleep_overhead_us);
+      }
+    }
+    std::cout << "[BLOCK " << b << "/" << block_count << "]\n"
+              << "FULL=1/1\n"
+              << "HOT_SENDTO=" << hot_per << "/" << hot_per << "\n"
+              << "HOT_RECEIVED=" << hot_recv_by_outer[b] << "/" << hot_per
+              << "\n"
+              << "callback=" << hot_cb_by_outer[b] << "/" << hot_per << "\n"
+              << "timeouts=" << hot_to_by_outer[b] << "\n"
+              << "hot_user_med=" << PercentileUs(users, 50) << "\n"
+              << "hot_wifi_med=" << PercentileUs(wifis, 50) << "\n"
+              << "wake_med=" << PercentileUs(wakes, 50) << "\n"
+              << "full_user=" << last_full_user[b] << "\n"
+              << "remaining=" << (block_count - b) << "\n";
+    std::cout.flush();
+  };
+
+  if (type == temp_sensor::bench::DsMsgType::kFull && p.outer_cycle >= 2) {
+    print_block(static_cast(p.outer_cycle) - 1);
+  }
   if (type == temp_sensor::bench::DsMsgType::kFinal) {
-    PrintFinalStats("deepsleep_5x50");
+    print_block(block_count);
+    PrintFinalStats(bench_tag);
   }
 }