/** * @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; } /* On IDF v5.4 with SOC_WIFI_HE_SUPPORT, HE advertisement is automatic * once the AP is started in HE-capable mode. TWT Responder advertise * is automatic when the AP is on an HE-capable channel and the IDF * SOC config has SOC_WIFI_HE_SUPPORT — verified by sniffing the beacon * and confirming `TWT Responder=1`. If a future IDF exposes * `esp_wifi_ap_set_he_config()` or similar, hook it here. * * Empirically against IDF v5.4 / C6 silicon: the beacon advertises * HE capability when the band is 2.4 GHz and the AP is on an * 11ax-capable channel, and TWT Responder follows. */ 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"); /* 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 */