diff --git a/docs/WITNESS-LOG-110.md b/docs/WITNESS-LOG-110.md
index 742344e2..c79b54f4 100644
--- a/docs/WITNESS-LOG-110.md
+++ b/docs/WITNESS-LOG-110.md
@@ -25,6 +25,7 @@ This witness separates what was **empirically observed on real silicon today** f
| **A0.3** | Soft-AP HE/TWT helper compiles | `c6_softap_he.{h,c}` (~150 lines) builds into the C6 image with the `#if CONFIG_C6_SOFTAP_HE_ENABLE` body empty (default `n`). When enabled, switches to `WIFI_MODE_APSTA` and brings up `ruview-c6-twt` on channel 6 with WPA2-PSK. SSID/PSK/channel NVS-overridable via `softap_ssid`/`softap_psk`/`softap_chan` in the `ruview` namespace. |
| **A0.4** | **v0.6.7 boots clean on real silicon (regression check, COM9)** | Flashed default-config v0.6.7 to ESP32-C6 on COM9 (`20:6e:f1:17:05:3c`). Boot log captured in `dist/firmware-v0.6.7/COM9-v0.6.7-regression.log`. Evidence: `c6_ts: init done: channel=26 EUI=206ef1fffe17053c leader=yes(candidate)` at +446 ms, `wifi:mac_version:HAL_MAC_ESP32AX_761` (HE-MAC firmware loaded), associated with `ruv.net` at +5206 ms (DHCP `192.168.1.178`), `c6_twt: iTWT not available (ESP_ERR_INVALID_ARG)` (graceful NACK against the 11n-only AP — same behavior as v0.6.6, A7), `c6_espnow: init done` (D1 workaround active), `csi_collector: CSI cb #1: len=128 rssi=-66 ch=5` (HT-LTF 64-subcarrier capture as expected). Zero regression vs v0.6.6 — new code paths default off, observed behavior is byte-for-byte the v0.6.6 path. |
| **A0.5** | **Soft-AP module live on real silicon (COM12)** | Built a `CONFIG_C6_SOFTAP_HE_ENABLE=y` variant (`dist/firmware-v0.6.7/esp32-csi-node-c6-4mb-softap.bin`, 1023 KB / 45% slack), flashed to ESP32-C6 on COM12 (`20:6e:f1:17:00:84`). Boot log: `dist/firmware-v0.6.7/COM12-v0.6.7-softap.log`. **Evidence the new module fires**:
`I (556) c6_softap: soft-AP starting: ssid="ruview-c6-twt" channel=6 auth=wpa2-psk`
`I (556) main: C6 soft-AP HE armed on channel 6 (ADR-110 B1/B2)`
`I (636) wifi:mode : sta (20:6e:f1:17:00:84) + softAP (20:6e:f1:17:00:85)`
`I (666) c6_softap: AP started on channel 6`
The IDF assigns the soft-AP MAC at the STA-MAC+1 offset (`...00:85`), standard behavior. **Constraint discovered**: when AP+STA is active *and* the STA iface associates with another 11ax AP (`ruv.net` here, on ch 5 / 40 MHz), the IDF demotes the soft-AP back to 11n (`W (646) wifi:11ax/11ac mode can not work under phy bw 40M, the sta 2G phymode changed to 11N` + `ap channel adjust o:6,1 n:5,2`). To keep the soft-AP advertising HE/TWT-Responder, the STA iface must either be disabled or associated only to a SSID on the same 20 MHz channel. Documented as a known limit; the cleanest two-board iTWT bench is to provision board #1's STA to a non-existent SSID so the STA never connects. |
+| **A0.6** | **Two-C6 iTWT bench attempted live — surfaces an IDF v5.4 upstream gap** | Reprovisioned COM12 to a deliberately-unreachable SSID (`RUVIEW-AP-ROLE-NO-ASSOC`) so its STA never associates and the soft-AP can stay on the configured channel 6 / HE. Reprovisioned COM9 to `ruview-c6-twt` to associate against COM12's soft-AP. Parallel boot logs in `dist/firmware-v0.6.7/iter1-{COM9,COM12}-*-role.log`.
**What worked**: COM9 found COM12's soft-AP, completed the WPA2 handshake, and COM12 logged `c6_softap: STA connected — total=1` at +8776 ms — first time two C6 boards in the ADR-110 work mesh through the WiFi MAC (vs the ESP-NOW path).
**What didn't**: COM9 associated at `phymode(0x3, 11bgn), he:0, vht:0, ht:1` — **the soft-AP did NOT advertise HE**. Source of the gap: a full grep of `components/esp_wifi/include/esp_wifi*.h` in IDF v5.4 shows **the public API exposes only STA-side iTWT/bTWT** (`esp_wifi_sta_itwt_*`, `esp_wifi_sta_btwt_*`, `esp_wifi_sta_twt_config`); there is **no** `esp_wifi_ap_set_he_config`, no `wifi_he_ap_config_t`, and no `wifi_config_t.ap.he_*` field. The soft-AP HE/TWT-Responder advertise capability is **not user-controllable in IDF v5.4** for the ESP32-C6.
Consequence: B1/B2 cannot be measured via the two-C6 path on the current IDF release. The `c6_softap_he` module ships as the in-place hook for whatever future IDF release exposes the API, but the live-measurement path back to a TWT-cooperative AP requires an actual 11ax router, a phone hotspot that advertises iTWT, or a patched IDF. **Sharpens the open question from "do we need an 11ax AP?" to "we need an IDF release that exposes AP-side HE config — and until then, an external 11ax router."** |
## A. Empirically verified (real silicon, today)
diff --git a/firmware/esp32-csi-node/main/c6_softap_he.c b/firmware/esp32-csi-node/main/c6_softap_he.c
index 6b28348e..7cde16f9 100644
--- a/firmware/esp32-csi-node/main/c6_softap_he.c
+++ b/firmware/esp32-csi-node/main/c6_softap_he.c
@@ -143,19 +143,25 @@ esp_err_t c6_softap_he_start(uint8_t *out_channel)
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.
+ /* 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.
*
- * 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. */
+ * 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
diff --git a/firmware/esp32-csi-node/main/swarm_bridge.c b/firmware/esp32-csi-node/main/swarm_bridge.c
index b6b485b2..3c5a19d9 100644
--- a/firmware/esp32-csi-node/main/swarm_bridge.c
+++ b/firmware/esp32-csi-node/main/swarm_bridge.c
@@ -230,9 +230,13 @@ static void swarm_task(void *arg)
ESP_LOGI(TAG, "Bearer token configured for Seed auth");
}
- /* Get firmware version string. */
+ /* Firmware version + IP captured locally so logs name the build; both
+ * intentionally unused in the JSON payloads — the seed extracts them
+ * from the register/heartbeat IDs. Keep as side-effect probes. */
const esp_app_desc_t *app = esp_app_get_description();
- const char *fw_ver = app ? app->version : "unknown";
+ if (app) {
+ ESP_LOGI(TAG, "swarm bridge fw=%s", app->version);
+ }
/* Get local IP. */
char ip_str[16];
@@ -278,15 +282,12 @@ static void swarm_task(void *arg)
xSemaphoreGive(s_mutex);
uint32_t uptime_s = (uint32_t)(esp_timer_get_time() / 1000000ULL);
- uint32_t free_heap = esp_get_free_heap_size();
uint32_t ts = (uint32_t)(esp_timer_get_time() / 1000ULL);
/* ---- Heartbeat ---- */
if ((now - last_heartbeat) >= pdMS_TO_TICKS(s_cfg.heartbeat_sec * 1000U)) {
last_heartbeat = now;
- bool presence = vit_valid && (vit.flags & 0x01);
-
/* Heartbeat ID: node_id * 1000000 + 100000 + ts_sec */
uint32_t hb_id = (uint32_t)s_node_id * 1000000U + 100000U + (uptime_s % 100000U);
char json[SWARM_JSON_BUF];