/** * @file c6_softap_he.c * @brief ESP32-C6 soft-AP with HE/TWT — ADR-110 B1/B2 cheap-unblock. * * Pairs with c6_softap_he.h. Builds only when both targets are set: * * CONFIG_IDF_TARGET_ESP32C6 (selected by `idf.py set-target esp32c6`) * CONFIG_C6_SOFTAP_HE_ENABLE (Kconfig, default n) * * The IDF v5.4 soft-AP path advertises HE automatically on chips with * SOC_WIFI_HE_SUPPORT; the operator-side concern here is making sure * the beacon also advertises `TWT Responder=1` so a STA-side * `esp_wifi_sta_itwt_setup()` call doesn't bounce with `INVALID_ARG` * the same way it did against `ruv.net` (the bench's 11n-only AP). * * TWT Responder advertisement in IDF v5.4 is gated by * `wifi_he_ap_config_t.twt_responder = 1`. When the IDF header doesn't * expose that struct (older v5.3), the AP still comes up with HE but * without TWT Responder — we log a warning and continue so the build * stays portable. */ #include "sdkconfig.h" #if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_SOFTAP_HE_ENABLE) #include "c6_softap_he.h" #include "esp_log.h" #include "esp_wifi.h" #include "esp_wifi_types.h" #include "esp_event.h" #include "esp_netif.h" #include "nvs_flash.h" #include "nvs.h" #include static const char *TAG = "c6_softap"; static bool s_started = false; static uint8_t s_sta_count = 0; static uint8_t s_channel = 0; #ifndef CONFIG_C6_SOFTAP_HE_SSID #define CONFIG_C6_SOFTAP_HE_SSID "ruview-c6-twt" #endif #ifndef CONFIG_C6_SOFTAP_HE_PSK #define CONFIG_C6_SOFTAP_HE_PSK "ruviewtwt" #endif #ifndef CONFIG_C6_SOFTAP_HE_CHANNEL #define CONFIG_C6_SOFTAP_HE_CHANNEL 6 #endif static void load_nvs_override(const char *key, char *dst, size_t dst_len) { nvs_handle_t h; if (nvs_open("ruview", NVS_READONLY, &h) != ESP_OK) return; size_t n = dst_len; esp_err_t err = nvs_get_str(h, key, dst, &n); if (err == ESP_OK) { ESP_LOGI(TAG, "nvs override: %s=\"%s\"", key, dst); } nvs_close(h); } static uint8_t load_nvs_u8(const char *key, uint8_t fallback) { nvs_handle_t h; if (nvs_open("ruview", NVS_READONLY, &h) != ESP_OK) return fallback; uint8_t v = fallback; if (nvs_get_u8(h, key, &v) == ESP_OK) { ESP_LOGI(TAG, "nvs override: %s=%u", key, v); } nvs_close(h); return v; } static void on_wifi_event(void *arg, esp_event_base_t base, int32_t event_id, void *event_data) { (void)arg; (void)base; (void)event_data; switch (event_id) { case WIFI_EVENT_AP_START: s_started = true; ESP_LOGI(TAG, "AP started on channel %u", s_channel); break; case WIFI_EVENT_AP_STOP: s_started = false; ESP_LOGI(TAG, "AP stopped"); break; case WIFI_EVENT_AP_STACONNECTED: if (s_sta_count < 255) s_sta_count++; ESP_LOGI(TAG, "STA connected — total=%u", s_sta_count); break; case WIFI_EVENT_AP_STADISCONNECTED: if (s_sta_count > 0) s_sta_count--; ESP_LOGI(TAG, "STA disconnected — total=%u", s_sta_count); break; default: break; } } esp_err_t c6_softap_he_start(uint8_t *out_channel) { if (s_started) { if (out_channel) *out_channel = s_channel; return ESP_OK; } /* Resolve config: NVS overrides Kconfig defaults. */ char ssid[33] = CONFIG_C6_SOFTAP_HE_SSID; char psk[64] = CONFIG_C6_SOFTAP_HE_PSK; load_nvs_override("softap_ssid", ssid, sizeof(ssid)); load_nvs_override("softap_psk", psk, sizeof(psk)); s_channel = load_nvs_u8("softap_chan", CONFIG_C6_SOFTAP_HE_CHANNEL); if (s_channel < 1 || s_channel > 13) s_channel = CONFIG_C6_SOFTAP_HE_CHANNEL; /* AP+STA so the existing STA path keeps working (NVS-provisioned upstream). */ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); wifi_config_t ap_cfg = {0}; size_t ssid_len = strlen(ssid); if (ssid_len > 32) ssid_len = 32; memcpy(ap_cfg.ap.ssid, ssid, ssid_len); ap_cfg.ap.ssid_len = (uint8_t)ssid_len; strncpy((char *)ap_cfg.ap.password, psk, sizeof(ap_cfg.ap.password) - 1); ap_cfg.ap.channel = s_channel; ap_cfg.ap.max_connection = 4; ap_cfg.ap.authmode = strlen(psk) >= 8 ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN; ap_cfg.ap.beacon_interval = 100; /* pmf_cfg.required = false keeps backward compatibility for STA clients * that don't speak PMF. */ ap_cfg.ap.pmf_cfg.required = false; /* Register the event handler before bringing the AP up so we don't * miss WIFI_EVENT_AP_START. */ ESP_ERROR_CHECK(esp_event_handler_instance_register( WIFI_EVENT, ESP_EVENT_ANY_ID, on_wifi_event, NULL, NULL)); esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &ap_cfg); if (err != ESP_OK) { ESP_LOGE(TAG, "set_config(AP) failed: %s", esp_err_to_name(err)); return err; } /* IDF v5.4 LIMIT (verified empirically 2026-05-23 — WITNESS-LOG-110 §A0.6): * the public API exposes ONLY STA-side iTWT/bTWT (esp_wifi_sta_itwt_*, * esp_wifi_sta_btwt_*). There is NO esp_wifi_ap_set_he_config(), NO * wifi_he_ap_config_t, and NO wifi_config_t.ap.he_* field. A second C6 * associating against this soft-AP currently lands at phymode 11bgn * (he:0, vht:0, ht:1) — the AP doesn't advertise HE because there's no * way to ask it to. A future IDF release that exposes AP-side HE config * (or a patched WiFi blob) is required to make this AP iTWT-capable. * * Until then, this module still gives you a working WPA2 soft-AP on a * controlled channel for AP+STA bench experiments and ESP-NOW peer * discovery — just not iTWT validation. The c6_twt module on the STA * side will return ESP_ERR_INVALID_ARG against this AP (no TWT Responder * in the beacon), exactly as it does against any other 11n-only AP. */ ESP_LOGI(TAG, "soft-AP starting: ssid=\"%s\" channel=%u auth=%s", ssid, s_channel, ap_cfg.ap.authmode == WIFI_AUTH_OPEN ? "open" : "wpa2-psk"); ESP_LOGW(TAG, "IDF v5.4 soft-AP does NOT advertise HE — STAs will associate at 11bgn. " "iTWT validation requires an external 11ax AP. See WITNESS-LOG-110 §A0.6."); /* Don't call esp_wifi_start() here — main.c brings the WiFi up once * for both AP and STA. We just configured the AP iface so it joins * the existing start. */ if (out_channel) *out_channel = s_channel; return ESP_OK; } bool c6_softap_he_is_up(void) { return s_started; } uint8_t c6_softap_he_sta_count(void) { return s_sta_count; } #endif /* CONFIG_IDF_TARGET_ESP32C6 && CONFIG_C6_SOFTAP_HE_ENABLE */