diff --git a/CLAUDE.md b/CLAUDE.md index b1b237e7..14c38528 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -316,7 +316,11 @@ are parsed in each demo's own code. The ones needed daily: radiotap overrides the mode per-packet (ER SU = radiotap-HE FORMAT=EXT_SU). Programmatic: `SetTxMode` / `ClearTxMode`. - `DEVOURER_SKIP_RESET=1` — skip `libusb_reset_device` before claim (only - helps when firmware state is intact). + helps when firmware state is intact). Kestrel adapters skip the reset + unconditionally (`claim_interface_then_reset` guard): their `power_on` + forces the MAC off from any retained state, while a USB reset on running + firmware drops the chip to ROM — a stale-handle re-enumeration that can + land in the dead ZeroCD DISK id (`0bda:1a2b`). - `DEVOURER_TX_GAP_US=N` — txdemo inter-frame gap (default 2000, ~500 fps; `0` = max duty for heating experiments). - `DEVOURER_USB_DEBUG=1` — libusb DEBUG log level (~7 MB / 15 s, has filled diff --git a/examples/rx/main.cpp b/examples/rx/main.cpp index 807df275..63b1fd80 100644 --- a/examples/rx/main.cpp +++ b/examples/rx/main.cpp @@ -955,27 +955,45 @@ int main() { if (const char *bus_env = std::getenv("DEVOURER_USB_BUS")) { const auto want_bus = static_cast(std::strtoul(bus_env, nullptr, 0)); const char *port_env = std::getenv("DEVOURER_USB_PORT"); - libusb_device **list = nullptr; - ssize_t n = libusb_get_device_list(ctx, &list); - for (ssize_t i = 0; i < n && dev_handle == NULL; ++i) { - libusb_device_descriptor dd{}; - if (libusb_get_device_descriptor(list[i], &dd) != 0) continue; - if (dd.idVendor != target_vid) continue; - if (target_pid != 0 && dd.idProduct != target_pid) continue; - if (libusb_get_bus_number(list[i]) != want_bus) continue; - if (port_env != nullptr) { - uint8_t ports[8]; - int pc = libusb_get_port_numbers(list[i], ports, sizeof(ports)); - std::string path; - for (int p = 0; p < pc; ++p) - path += (path.empty() ? "" : ".") + std::to_string(ports[p]); - if (path != port_env) continue; + /* A named socket is EXPECTED to hold the device — but the previous + * session's close/kill leaves the chip re-enumerating (firmware reload + * through ROM, sometimes via the ZeroCD id) for several seconds. Poll + * bounded instead of failing on the first empty scan. */ + const auto deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(15); + bool waited = false; + do { + libusb_device **list = nullptr; + ssize_t n = libusb_get_device_list(ctx, &list); + for (ssize_t i = 0; i < n && dev_handle == NULL; ++i) { + libusb_device_descriptor dd{}; + if (libusb_get_device_descriptor(list[i], &dd) != 0) continue; + if (dd.idVendor != target_vid) continue; + if (target_pid != 0 && dd.idProduct != target_pid) continue; + if (libusb_get_bus_number(list[i]) != want_bus) continue; + if (port_env != nullptr) { + uint8_t ports[8]; + int pc = libusb_get_port_numbers(list[i], ports, sizeof(ports)); + std::string path; + for (int p = 0; p < pc; ++p) + path += (path.empty() ? "" : ".") + std::to_string(ports[p]); + if (path != port_env) continue; + } + if (libusb_open(list[i], &dev_handle) == 0) + logger->info("Opened device {:04x}:{:04x} on bus {} port {}", + dd.idVendor, dd.idProduct, want_bus, + port_env ? port_env : "(any)"); } - if (libusb_open(list[i], &dev_handle) == 0) - logger->info("Opened device {:04x}:{:04x} on bus {} port {}", dd.idVendor, - dd.idProduct, want_bus, port_env ? port_env : "(any)"); - } - if (list != nullptr) libusb_free_device_list(list, 1); + if (list != nullptr) libusb_free_device_list(list, 1); + if (dev_handle != NULL) break; + if (!waited) { + logger->warn("DEVOURER_USB_BUS={} PORT={} matched no device — waiting " + "for it to (re-)enumerate", + want_bus, port_env ? port_env : "(any)"); + waited = true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } while (std::chrono::steady_clock::now() < deadline); /* Topology selection is strict: falling through to the VID:PID loop here * would silently open a DIFFERENT adapter sharing the id (the exact * ambiguity DEVOURER_USB_BUS exists to resolve) — fail instead. */ @@ -1022,15 +1040,19 @@ int main() { * still suppresses the reset for a warm pickup (firmware already running). * See src/UsbOpen.h. */ std::shared_ptr usb_lock; - rc = devourer::claim_interface_then_reset(dev_handle, devourer::find_wifi_interface(dev_handle), logger, std::getenv("DEVOURER_SKIP_RESET") == nullptr, - usb_lock); + /* Reopen variant: recovers in place when the reset re-enumerates the device + * (a warm Kestrel drops its firmware back to ROM on reset — the handle goes + * stale and the dongle may pass through its ZeroCD id before returning). */ + rc = devourer::claim_interface_reset_reopen(ctx, dev_handle, logger, + std::getenv("DEVOURER_SKIP_RESET") == nullptr, usb_lock); devourer::Ev(*g_ev, "init.timing") .f("stage", "demo.usb_reset") .f("ms", ms_since_start()); if (rc != 0) { /* BUSY => another process owns the adapter; any other error => open failed. * Either way, exit cleanly rather than asserting. */ - libusb_close(dev_handle); + if (dev_handle != nullptr) + libusb_close(dev_handle); libusb_exit(ctx); return 1; } diff --git a/examples/streamtx/main.cpp b/examples/streamtx/main.cpp index b6b13655..81d2e2be 100644 --- a/examples/streamtx/main.cpp +++ b/examples/streamtx/main.cpp @@ -195,10 +195,13 @@ int main(int argc, char **argv) { * guard — a second devourer on this adapter gets BUSY here and bails before * the reset, so it can't re-enumerate the adapter out from under the owner. */ std::shared_ptr usb_lock; - rc = devourer::claim_interface_then_reset(handle, devourer::find_wifi_interface(handle), logger, + /* Reopen variant: recovers in place when the reset re-enumerates the + * device (warm Kestrel firmware-drop through ROM / ZeroCD). */ + rc = devourer::claim_interface_reset_reopen(context, handle, logger, termux_fd == 0 && std::getenv("DEVOURER_SKIP_RESET") == nullptr, usb_lock); if (rc != 0) { - libusb_close(handle); + if (handle != nullptr) + libusb_close(handle); libusb_exit(context); return 1; } diff --git a/examples/tx/main.cpp b/examples/tx/main.cpp index c9804b8f..7fa09d72 100644 --- a/examples/tx/main.cpp +++ b/examples/tx/main.cpp @@ -248,28 +248,45 @@ int main(int argc, char **argv) { const auto want_bus = static_cast(std::strtoul(bus_env, nullptr, 0)); const char *port_env = std::getenv("DEVOURER_USB_PORT"); - libusb_device **list = nullptr; - ssize_t n = libusb_get_device_list(context, &list); - for (ssize_t i = 0; i < n && handle == NULL; ++i) { - libusb_device_descriptor dd{}; - if (libusb_get_device_descriptor(list[i], &dd) != 0) continue; - if (dd.idVendor != target_vid) continue; - if (target_pid != 0 && dd.idProduct != target_pid) continue; - if (libusb_get_bus_number(list[i]) != want_bus) continue; - if (port_env != nullptr) { - uint8_t ports[8]; - int pc = libusb_get_port_numbers(list[i], ports, sizeof(ports)); - std::string path; - for (int p = 0; p < pc; ++p) - path += (path.empty() ? "" : ".") + std::to_string(ports[p]); - if (path != port_env) continue; + /* A named socket is EXPECTED to hold the device — but the previous + * session's close/kill leaves the chip re-enumerating (firmware reload + * through ROM, sometimes via the ZeroCD id) for several seconds. Poll + * bounded instead of failing on the first empty scan. */ + const auto deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(15); + bool waited = false; + do { + libusb_device **list = nullptr; + ssize_t n = libusb_get_device_list(context, &list); + for (ssize_t i = 0; i < n && handle == NULL; ++i) { + libusb_device_descriptor dd{}; + if (libusb_get_device_descriptor(list[i], &dd) != 0) continue; + if (dd.idVendor != target_vid) continue; + if (target_pid != 0 && dd.idProduct != target_pid) continue; + if (libusb_get_bus_number(list[i]) != want_bus) continue; + if (port_env != nullptr) { + uint8_t ports[8]; + int pc = libusb_get_port_numbers(list[i], ports, sizeof(ports)); + std::string path; + for (int p = 0; p < pc; ++p) + path += (path.empty() ? "" : ".") + std::to_string(ports[p]); + if (path != port_env) continue; + } + if (libusb_open(list[i], &handle) == 0) + logger->info("Opened device {:04x}:{:04x} on bus {} port {}", + dd.idVendor, dd.idProduct, want_bus, + port_env ? port_env : "(any)"); } - if (libusb_open(list[i], &handle) == 0) - logger->info("Opened device {:04x}:{:04x} on bus {} port {}", - dd.idVendor, dd.idProduct, want_bus, - port_env ? port_env : "(any)"); - } - if (list != nullptr) libusb_free_device_list(list, 1); + if (list != nullptr) libusb_free_device_list(list, 1); + if (handle != NULL) break; + if (!waited) { + logger->warn("DEVOURER_USB_BUS={} PORT={} matched no device — " + "waiting for it to (re-)enumerate", + want_bus, port_env ? port_env : "(any)"); + waited = true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } while (std::chrono::steady_clock::now() < deadline); /* Topology selection is strict: falling through to the VID:PID loop * here would silently open a DIFFERENT adapter sharing the id (the * exact ambiguity DEVOURER_USB_BUS exists to resolve) — fail instead. */ @@ -322,13 +339,16 @@ int main(int argc, char **argv) { * bail here before the reset, so it can't re-enumerate the adapter out from * under the owner. Reset skipped in termux_mode (forked child shares the * fd) and for DEVOURER_SKIP_RESET (warm pickup). */ - rc = devourer::claim_interface_then_reset(handle, devourer::find_wifi_interface(handle), logger, + /* Reopen variant: recovers in place when the reset re-enumerates the + * device (warm Kestrel firmware-drop through ROM / ZeroCD). */ + rc = devourer::claim_interface_reset_reopen(context, handle, logger, !termux_mode && std::getenv("DEVOURER_SKIP_RESET") == nullptr, usb_lock); devourer::Ev(*g_ev, "init.timing") .f("stage", "txdemo.usb_reset") .f("ms", ms_since_start()); if (rc != 0) { - libusb_close(handle); + if (handle != nullptr) + libusb_close(handle); libusb_exit(context); return 1; } diff --git a/src/UsbOpen.cpp b/src/UsbOpen.cpp index 2eab14d6..e8a13d33 100644 --- a/src/UsbOpen.cpp +++ b/src/UsbOpen.cpp @@ -1,6 +1,7 @@ #include "UsbOpen.h" #include "UsbDeviceLock.h" +#include "kestrel/KestrelUsbIds.h" #if defined(__ANDROID__) || defined(_MSC_VER) || defined(__APPLE__) #include @@ -8,7 +9,10 @@ #include #endif +#include +#include #include +#include namespace devourer { @@ -122,6 +126,22 @@ int claim_interface_then_reset(libusb_device_handle *handle, int iface, return rc; } + /* Kestrel (11ax) adapters never want the USB reset: their power-on sequence + * forces the MAC off from any retained state (the real cleaner), while a + * USB reset on running/half-torn firmware drops the chip back to its ROM — + * a stale-handle re-enumeration that can land in the dead ZeroCD DISK id + * (0bda:1a2b). Bench-measured: KILLed-session churn is 6/6 clean without + * the reset vs ZeroCD roulette with it (issue #294). */ + if (do_reset) { + libusb_device_descriptor dd{}; + if (libusb_get_device_descriptor(dev, &dd) == 0 && + kestrel::variant_for_usb_id(dd.idVendor, dd.idProduct).has_value()) { + logger->info("Kestrel adapter: skipping USB reset (power-on owns state " + "cleanup; a reset drops running firmware into ROM/ZeroCD)"); + do_reset = false; + } + } + if (do_reset) { /* We hold the lock and the interface, so this re-enumeration can't disturb * anyone else. If the reset invalidates the handle, report it; otherwise @@ -144,4 +164,80 @@ int claim_interface_then_reset(libusb_device_handle *handle, int iface, return 0; } +int claim_interface_reset_reopen(libusb_context *ctx, + libusb_device_handle *&handle, + const Logger_t &logger, bool do_reset, + std::shared_ptr &out_lock, + const std::string &lock_dir, + unsigned reopen_timeout_ms) { + if (handle == nullptr) + return LIBUSB_ERROR_NO_DEVICE; + + /* Record the physical identity BEFORE the reset can invalidate the handle: + * bus number + port path name the socket; VID:PID names the device we wait + * for (a mid-re-enumeration ZeroCD id at the same port is NOT a match). */ + libusb_device *dev = libusb_get_device(handle); + const uint8_t bus = libusb_get_bus_number(dev); + uint8_t ports[8] = {}; + const int nports = libusb_get_port_numbers(dev, ports, sizeof(ports)); + libusb_device_descriptor dd{}; + libusb_get_device_descriptor(dev, &dd); + + int rc = claim_interface_then_reset(handle, find_wifi_interface(handle), + logger, do_reset, out_lock, lock_dir); + /* Recovery is only meaningful when we could have reset: with do_reset=false + * a NOT_FOUND is a claim failure (interface absent), not a re-enumeration — + * closing the handle and polling for a comeback would just burn the timeout + * (and on Termux would orphan a wrapped fd device-list scans can't find). */ + if (rc != LIBUSB_ERROR_NOT_FOUND || !do_reset) + return rc; + + /* The reset re-enumerated the device (firmware reload through ROM). Close + * the stale handle and wait for the same bus+port to come back under the + * original VID:PID, then claim WITHOUT another reset — resetting again + * would just drop the freshly loaded firmware once more. */ + libusb_close(handle); + handle = nullptr; + logger->warn("USB reset re-enumerated the device — waiting for it to " + "return on bus {} (up to {} ms)", + bus, reopen_timeout_ms); + + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(reopen_timeout_ms); + while (std::chrono::steady_clock::now() < deadline) { + libusb_device **list = nullptr; + const ssize_t n = libusb_get_device_list(ctx, &list); + for (ssize_t i = 0; i < n && handle == nullptr; ++i) { + if (libusb_get_bus_number(list[i]) != bus) + continue; + uint8_t p[8] = {}; + const int np = libusb_get_port_numbers(list[i], p, sizeof(p)); + if (np != nports || memcmp(p, ports, static_cast(np)) != 0) + continue; + libusb_device_descriptor d{}; + if (libusb_get_device_descriptor(list[i], &d) != 0) + continue; + if (d.idVendor != dd.idVendor || d.idProduct != dd.idProduct) + continue; /* ZeroCD / transient ROM id — keep waiting */ + if (libusb_open(list[i], &handle) != 0) + handle = nullptr; + } + if (list != nullptr) + libusb_free_device_list(list, 1); + if (handle != nullptr) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } + if (handle == nullptr) { + logger->error("device did not re-enumerate as {:04x}:{:04x} within {} ms", + dd.idVendor, dd.idProduct, reopen_timeout_ms); + return LIBUSB_ERROR_NOT_FOUND; + } + logger->info("re-opened {:04x}:{:04x} after reset re-enumeration", + dd.idVendor, dd.idProduct); + return claim_interface_then_reset(handle, find_wifi_interface(handle), + logger, /*do_reset=*/false, out_lock, + lock_dir); +} + } // namespace devourer diff --git a/src/UsbOpen.h b/src/UsbOpen.h index be0c81af..cb5f47b6 100644 --- a/src/UsbOpen.h +++ b/src/UsbOpen.h @@ -7,6 +7,7 @@ #include "logger.h" struct libusb_device_handle; +struct libusb_context; namespace devourer { @@ -50,6 +51,27 @@ int claim_interface_then_reset(libusb_device_handle *handle, int iface, std::shared_ptr &out_lock, const std::string &lock_dir = {}); +/* claim_interface_then_reset plus transparent recovery from the reset + * re-enumeration: on some chips (Kestrel 35bc:xxxx) libusb_reset_device on a + * warm adapter drops the firmware back to ROM and the device re-enumerates — + * the handle goes stale (LIBUSB_ERROR_NOT_FOUND) and, on the way through ROM, + * the dongle can transiently surface as its ZeroCD id (0bda:1a2b DISK). This + * wrapper closes the stale handle, waits (bounded) for the SAME physical + * device — bus + port path — to come back under its original VID:PID, then + * re-opens and re-claims WITHOUT a second reset: the re-enumeration itself + * reloaded the firmware, so its state is fresh. `handle` is replaced in place + * on that path (the old handle is closed); `iface` is re-resolved on the new + * handle via find_wifi_interface(). All other outcomes are exactly + * claim_interface_then_reset() — including NOT_FOUND when `do_reset` is + * false, which then can only be a claim failure, not a re-enumeration. `ctx` + * must be the context `handle` was opened on. */ +int claim_interface_reset_reopen(libusb_context *ctx, + libusb_device_handle *&handle, + const Logger_t &logger, bool do_reset, + std::shared_ptr &out_lock, + const std::string &lock_dir = {}, + unsigned reopen_timeout_ms = 30000); + } // namespace devourer #endif /* DEVOURER_USB_OPEN_H */ diff --git a/tests/he_er_su_cross_rx.sh b/tests/he_er_su_cross_rx.sh index 064c0bac..5aa82978 100755 --- a/tests/he_er_su_cross_rx.sh +++ b/tests/he_er_su_cross_rx.sh @@ -18,11 +18,12 @@ # sudo tests/he_er_su_cross_rx.sh \ # 35bc:0101@3:2.3.2 35bc:0101@3:2.3.1 # 8852C -> 8852C # -# Kestrel RX bring-up is bimodal on this bench: a repeatedly soft-re-inited -# DUT comes up deaf (0 frames incl. ambient) until a real VBUS cold — a -# pre-existing bring-up trait, not an ER SU property. Zero-hit cells are -# therefore retried once after a VBUS cold-cycle of the RX DUT when -# REGRESS_VBUS_MAP maps it (format "VID:PID=hub,port;...", uhubctl hubs). +# Belt-and-braces: a zero-hit cell is retried once after a VBUS cold-cycle of +# the RX DUT when REGRESS_VBUS_MAP maps it (format "VID:PID=hub,port;...", +# uhubctl hubs; full-spec "VID:PID@BUS:PORT=hub,port" keys win for same-id +# pairs). Kept as a guard against DUT-level flakiness even though the +# historical deaf-bring-up causes (RX-descriptor walk break, USB-reset +# firmware drop) are fixed in the library. # # Usage: sudo tests/he_er_su_cross_rx.sh [TX_VID:PID] [RX_VID:PID] [DUR] [CH] set -u diff --git a/tests/kestrel_rxparse_selftest.cpp b/tests/kestrel_rxparse_selftest.cpp index c0bd0078..ea95a946 100644 --- a/tests/kestrel_rxparse_selftest.cpp +++ b/tests/kestrel_rxparse_selftest.cpp @@ -116,6 +116,35 @@ int main() { CHECK(f.payload_len == 24); } + /* --- issue #294 regression: an aggregate led by a PPDU-status sub-packet + * carrying drv_info, walked with the 8852C's 16-byte unit. With the + * 8852B unit (8) the PPDU packet's next_offset lands mid-drv_info, the + * next parse reads garbage, and the trailing WIFI frame is lost — a + * green bring-up that delivers no frames (bimodal by whether the fw + * attaches phy status that session). --- */ + { + std::vector b; + const uint32_t ppdu_len = 32, ppdu_off = 16 + 2 * 16; /* drv3=2, unit 16 */ + const uint32_t frame0 = ((ppdu_off + ppdu_len + 7) & ~7u); + b.resize(frame0 + 16 + 60, 0); + put_desc(b, 0, ppdu_len, 0, /*macvld=*/true, RPKT_TYPE_PPDU, + /*drv3=*/2, false, 0, 0, false, false); + put_desc(b, frame0, 60, 0, false, RPKT_TYPE_WIFI, 0, false, 0x180, 0, + false, false, /*ppdu_type=*/7); + uint32_t off = 0; + int wifi = 0; + while (off + 16 <= b.size()) { + KestrelRxFrame f; + if (!parse_rx_8852b(b.data() + off, b.size() - off, f, + /*drv_info_unit=*/16)) + break; + if (f.rpkt_type == RPKT_TYPE_WIFI) + ++wifi; + off += f.next_offset; + } + CHECK(wifi == 1); /* the WIFI frame behind the PPDU status is reached */ + } + /* --- walk an aggregate of two WIFI frames via next_offset --- */ { std::vector b;