From 9c1b6493d8549e98dff4b4982b98e65d6952331b Mon Sep 17 00:00:00 2001 From: portersky Date: Sat, 29 Aug 2026 13:33:32 +0200 Subject: [PATCH] feat: show live controller input Route paired controller QoS data through the existing GIP protocol layer and expose the latest gamepad report through the C API. Add a SwiftUI monitor for buttons, d-pad, triggers, and stick positions without registering a native HID device. Co-Authored-By: openai/gpt-5.6-luna: GIP bridge and live input monitor --- include/app/xone_api.h | 26 +++- include/mt76/mt76.hpp | 6 + include/mt76/mt76_defs.hpp | 4 +- src/app/api.cpp | 258 ++++++++++++++++++++++++++++++++++--- src/app/main.swift | 163 ++++++++++++++++++++--- src/mt76/mt76.cpp | 43 +++++++ tests/test_mt76.cpp | 2 +- 7 files changed, 462 insertions(+), 40 deletions(-) diff --git a/include/app/xone_api.h b/include/app/xone_api.h index 4e43736..6b34a68 100644 --- a/include/app/xone_api.h +++ b/include/app/xone_api.h @@ -39,8 +39,8 @@ xone_dongle *xone_open(void); // Kick off firmware load + radio init on a background thread. Non-blocking: // returns 0 once started, -1 if the session is busy or already ready. If -// firmware_path is NULL, the image is selected per product ID -// (firmware/xone_dongle_.bin). +// firmware_path is NULL, the common upstream image +// (firmware/xow_dongle.bin) is used. int xone_start(xone_dongle *d, const char *firmware_path); // Close the session and release the dongle. Waits for any in-progress start. @@ -72,6 +72,28 @@ int xone_controller_count(xone_dongle const *d); // success, -1 if index is out of range. int xone_controller_mac(xone_dongle const *d, int index, char *buf, int len); +// Snapshot of the latest standard Xbox controller input report. Stick values +// are signed 16-bit values; trigger values are unsigned 10-bit values. The +// buttons field uses the standard GIP gamepad bit assignments. +struct xone_controller_state { + uint16_t buttons; + bool guide_down; + bool gip_ready; + bool input_active; + uint16_t trigger_left; + uint16_t trigger_right; + int16_t stick_left_x; + int16_t stick_left_y; + int16_t stick_right_x; + int16_t stick_right_y; + uint32_t sequence; +}; + +// Copy the latest input state for the controller at index. Returns 0 on +// success, -1 if index is out of range or state is NULL. +int xone_controller_get_state(xone_dongle const *d, int index, + struct xone_controller_state *state); + // Enter or leave pairing mode (beacon pairing flag + LED blink). void xone_set_pairing(xone_dongle *d, bool enable); diff --git a/include/mt76/mt76.hpp b/include/mt76/mt76.hpp index 10e6726..d6976e0 100644 --- a/include/mt76/mt76.hpp +++ b/include/mt76/mt76.hpp @@ -77,6 +77,12 @@ public: // then bulk-writes it to EP OUT. auto send_wlan(std::span frame) -> int; + // Send a GIP payload in an upstream QoS-data frame for one client. + auto send_client_frame(std::uint8_t wcid, + std::span addr, + std::span frame, + bool encrypted) -> int; + // Associate a controller: program its WCID MAC, ADD_CLIENT ms_command, // and transmit an ASSOC_RESP mgmt frame. auto associate_client(std::uint8_t wcid, std::span addr) diff --git a/include/mt76/mt76_defs.hpp b/include/mt76/mt76_defs.hpp index 1d5d69b..38fcf2e 100644 --- a/include/mt76/mt76_defs.hpp +++ b/include/mt76/mt76_defs.hpp @@ -458,10 +458,12 @@ constexpr std::uint16_t ieee80211_fctl_ftype = 0x0c; constexpr std::uint16_t ieee80211_fctl_stype = 0xf0; constexpr std::uint16_t ieee80211_ftype_mgmt = 0x00; constexpr std::uint16_t ieee80211_ftype_data = 0x08; +constexpr std::uint16_t ieee80211_fctl_from_ds = 0x0200; +constexpr std::uint16_t ieee80211_fctl_protected = 0x4000; constexpr std::uint16_t ieee80211_stype_assoc_req = 0x00; constexpr std::uint16_t ieee80211_stype_assoc_resp = 0x10; constexpr std::uint16_t ieee80211_stype_disassoc = 0xa0; -constexpr std::uint16_t ieee80211_stype_qos_data = 0x70; +constexpr std::uint16_t ieee80211_stype_qos_data = 0x80; // MGMT subtype for reserved (pairing) frames. constexpr std::uint16_t ieee80211_stype_wlan_reserved = 0x70; diff --git a/src/app/api.cpp b/src/app/api.cpp index eb55c17..a470a21 100644 --- a/src/app/api.cpp +++ b/src/app/api.cpp @@ -14,6 +14,7 @@ #include #include "common/log.hpp" +#include "gip/protocol.hpp" #include "mt76/mt76.hpp" #include "usb/usb_transport.hpp" @@ -21,11 +22,72 @@ #define XONE_VERSION "0.1.0" #endif +struct controller; +struct xone_dongle; + +// QoS-data transport used by the GIP adapter for one wireless client. +class controller_transport : public xone::gip::transport { +public: + controller_transport(xone::mt76::chip& chip, std::uint8_t wcid, + std::array mac) + : chip_(chip) + , wcid_(wcid) + , mac_(mac) + { + } + + auto send_frame(std::span frame) -> int override; + auto set_encryption_key(std::uint8_t client_id, + std::span key) -> int override; + auto enable_encryption() -> void { encrypted_ = true; } + +private: + xone::mt76::chip& chip_; + std::uint8_t wcid_; + std::array mac_; + bool encrypted_ = false; +}; + +class controller_listener : public xone::gip::client_listener { +public: + controller_listener(xone_dongle& dongle, controller& controller) + : dongle_(dongle) + , controller_(controller) + { + } + + auto on_client_added(xone::gip::client&) -> void override; + auto on_client_removed(std::uint8_t) -> void override; + auto on_guide_button(xone::gip::client&, bool down) -> void override; + auto on_input(xone::gip::client&, std::span data) + -> void override; + +private: + xone_dongle& dongle_; + controller& controller_; +}; + // One connected controller (tracked for the GUI). Guarded by xone_dongle's // lock; written by the transport reader thread, read by the UI thread. struct controller { std::uint8_t wcid; std::array mac; + std::uint16_t buttons = 0; + std::uint16_t trigger_left = 0; + std::uint16_t trigger_right = 0; + std::int16_t stick_left_x = 0; + std::int16_t stick_left_y = 0; + std::int16_t stick_right_x = 0; + std::int16_t stick_right_y = 0; + std::uint32_t input_sequence = 0; + bool input_active = false; + bool guide_down = false; + bool gip_ready = false; + + // Declared in dependency order so adapter is destroyed before its bridge. + std::unique_ptr gip_transport; + std::unique_ptr gip_listener; + std::unique_ptr gip_adapter; }; // Definition of the opaque struct xone_dongle from app/xone_api.h. @@ -44,8 +106,9 @@ struct xone_dongle { // Firmware load + radio init runs here so the UI thread never blocks. std::thread worker; - // Connected controllers (wcid 1..16). Guarded by lock. - std::vector controllers; + // Connected controllers (wcid 1..16). The pointed-to objects stay stable + // while the vector changes, as GIP bridge callbacks retain references. + std::vector> controllers; // Pairing mode state (port of dongle->pairing). Shared between the UI // thread and the transport reader thread. @@ -53,19 +116,20 @@ struct xone_dongle { ~xone_dongle() { + // Finish firmware/radio work before tearing down the transport it uses. + if (worker.joinable()) + worker.join(); // Stop the reader thread before the chip is torn down so a late IN // completion cannot touch freed state. transport.reset(); - // The session is destroyed at process exit even without an explicit - // xone_close(); joining here avoids terminating on a joinable thread. - if (worker.joinable()) - worker.join(); } // RX dispatch, driven from the transport reader thread. Port of the // xone_dongle_process_* functions in transport/dongle.c. auto process_message(void const *data, std::size_t len) -> void; auto process_wlan(std::span buf) -> void; + auto handle_qos_data(std::uint8_t wcid, + std::span payload) -> void; auto handle_association(std::span addr) -> void; auto handle_disassociation(std::uint8_t wcid) -> void; auto handle_client_command(std::span payload, @@ -74,6 +138,7 @@ struct xone_dongle { // Enter or leave pairing mode (port of xone_dongle_toggle_pairing). auto set_pairing(bool enable) -> void; + auto setup_controller(controller& controller) -> void; }; namespace { @@ -93,6 +158,75 @@ auto firmware_path_for(std::uint16_t) -> std::string } // namespace +// --- GIP/MT76 bridge ------------------------------------------------------- + +auto controller_transport::send_frame(std::span frame) + -> int +{ + return chip_.send_client_frame(wcid_, mac_, frame, encrypted_); +} + +auto controller_transport::set_encryption_key( + std::uint8_t client_id, std::span key) -> int +{ + (void)client_id; + return chip_.set_client_key(wcid_, key); +} + +auto controller_listener::on_client_added(xone::gip::client&) -> void +{ + std::lock_guard guard(dongle_.lock); + controller_.gip_ready = true; + xone::log_msg(xone::log_level::info, + "api: GIP ready (wcid=%d)", controller_.wcid); +} + +auto controller_listener::on_client_removed(std::uint8_t) -> void +{ + std::lock_guard guard(dongle_.lock); + controller_.gip_ready = false; + controller_.input_active = false; +} + +auto controller_listener::on_guide_button(xone::gip::client&, bool down) + -> void +{ + std::lock_guard guard(dongle_.lock); + controller_.guide_down = down; +} + +auto controller_listener::on_input( + xone::gip::client&, std::span data) -> void +{ + // Standard gamepad input: seven little-endian 16-bit values. + if (data.size() < 14) + return; + + std::lock_guard guard(dongle_.lock); + controller_.buttons = xone::load_le16(data.data()); + controller_.trigger_left = xone::load_le16(data.data() + 2); + controller_.trigger_right = xone::load_le16(data.data() + 4); + controller_.stick_left_x = static_cast( + xone::load_le16(data.data() + 6)); + controller_.stick_left_y = static_cast( + ~xone::load_le16(data.data() + 8)); + controller_.stick_right_x = static_cast( + xone::load_le16(data.data() + 10)); + controller_.stick_right_y = static_cast( + ~xone::load_le16(data.data() + 12)); + ++controller_.input_sequence; + controller_.input_active = true; +} + +auto xone_dongle::setup_controller(controller& c) -> void +{ + c.gip_transport = std::make_unique(*chip, c.wcid, + c.mac); + c.gip_listener = std::make_unique(*this, c); + c.gip_adapter = std::make_unique( + *c.gip_transport, *c.gip_listener, 1); +} + // --- RX dispatch (port of xone_dongle_process_*) --------------------------- auto xone_dongle::process_message(void const *data, std::size_t len) -> void @@ -159,6 +293,8 @@ auto xone_dongle::process_wlan(std::span buf) -> void auto rxinfo = xone::load_le32(buf.data()); auto ctl = xone::load_le32(buf.data() + 4); auto wcid = static_cast(field_get(mt_rxwi_ctl_wcid, ctl)); + auto mpdu_len = static_cast( + field_get(mt_rxwi_ctl_mpdu_len, ctl)); // The frame starts after the 32-byte rxwi. std::span frame = buf.subspan(sizeof(mt76_rxwi)); @@ -167,19 +303,49 @@ auto xone_dongle::process_wlan(std::span buf) -> void auto fc = xone::load_le16(frame.data()); auto match = fc & (ieee80211_fctl_ftype | ieee80211_fctl_stype); + auto header_len = k_mgmt_hdr_len; + if (match == (ieee80211_ftype_data | ieee80211_stype_qos_data)) + header_len += 2; // sequence control plus QoS control - // L2PAD: 2 bytes of padding after the 802.11 header. - std::size_t pad = (rxinfo & mt_rxinfo_l2pad) ? 2 : 0; + // L2PAD: 2 bytes of padding after the 802.11 header. MPDU length is + // measured after this padding is removed, matching the upstream path. + auto pad = (rxinfo & mt_rxinfo_l2pad) ? std::size_t{2} : std::size_t{0}; + if (mpdu_len < header_len || frame.size() < mpdu_len + pad) + return; + + auto payload = frame.subspan(header_len + pad, mpdu_len - header_len); if (match == (ieee80211_ftype_mgmt | ieee80211_stype_assoc_req)) { handle_association(frame.subspan(10, 6)); // addr2 } else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_disassoc)) { handle_disassociation(wcid); } else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_wlan_reserved)) { - handle_client_command(frame.subspan(k_mgmt_hdr_len + pad), wcid, - frame.subspan(10, 6)); // addr2 + handle_client_command(payload, wcid, frame.subspan(10, 6)); + } else if (match == (ieee80211_ftype_data | ieee80211_stype_qos_data)) { + handle_qos_data(wcid, payload); } - // DATA|QOS_DATA (GIP) is handled in a later increment. +} + +auto xone_dongle::handle_qos_data( + std::uint8_t wcid, std::span payload) -> void +{ + controller* found = nullptr; + { + std::lock_guard guard(lock); + for (auto const& c : controllers) { + if (c->wcid == wcid) { + found = c.get(); + break; + } + } + } + + if (!found || !found->gip_adapter) + return; + + if (auto err = found->gip_adapter->process_buffer(payload); err != 0) + xone::log_msg(xone::log_level::debug, + "api: GIP packet processing failed (%d)", err); } auto xone_dongle::set_pairing(bool enable) -> void @@ -213,8 +379,8 @@ auto xone_dongle::handle_association(std::span addr) -> void { std::lock_guard guard(lock); for (auto const& c : controllers) { - if (c.mac == mac) { - wcid = c.wcid; + if (c->mac == mac) { + wcid = c->wcid; existing = true; break; } @@ -223,7 +389,7 @@ auto xone_dongle::handle_association(std::span addr) -> void for (std::uint8_t i = 1; i <= 16; ++i) { bool used = false; for (auto const& c : controllers) - if (c.wcid == i) { used = true; break; } + if (c->wcid == i) { used = true; break; } if (!used) { wcid = i; break; } } } @@ -239,8 +405,12 @@ auto xone_dongle::handle_association(std::span addr) -> void } if (!existing) { + auto new_controller = std::make_unique(); + new_controller->wcid = wcid; + new_controller->mac = mac; + setup_controller(*new_controller); std::lock_guard guard(lock); - controllers.push_back(controller{wcid, mac}); + controllers.push_back(std::move(new_controller)); } if (!pairing) chip->set_led_mode(xone::mt76::led_mode::led_on); @@ -260,7 +430,7 @@ auto xone_dongle::handle_disassociation(std::uint8_t wcid) -> void { std::lock_guard guard(lock); for (auto it = controllers.begin(); it != controllers.end(); ++it) { - if (it->wcid == wcid) { + if ((*it)->wcid == wcid) { controllers.erase(it); break; } @@ -296,9 +466,28 @@ auto xone_dongle::handle_client_command(std::span payload, "api: controller paired (wcid=%d)", wcid); break; } - case client_cmd::client_enable_encryption: - // Encryption is enabled in a later increment (needs the GIP key). + case client_cmd::client_enable_encryption: { + controller* client = nullptr; + { + std::lock_guard guard(lock); + for (auto const& c : controllers) { + if (c->wcid == wcid) { + client = c.get(); + break; + } + } + } + if (!client || !client->gip_transport) + break; + + std::uint8_t data[2] = {0, 0}; + if (chip->send_client_command( + wcid, client->mac, client_cmd::client_enable_encryption, + {data, sizeof(data)}) == 0) { + client->gip_transport->enable_encryption(); + } break; + } default: break; } @@ -353,8 +542,9 @@ extern "C" int xone_start(xone_dongle *d, const char *firmware_path) if (!d) return -1; - // Select the image per product ID when no explicit path is given. - std::string path = firmware_path ? std::string(firmware_path) : firmware_path_for(d->pid); + // Use the upstream common image when no explicit path is given. + std::string path = firmware_path ? std::string(firmware_path) + : firmware_path_for(d->pid); { std::lock_guard guard(d->lock); @@ -457,12 +647,38 @@ extern "C" int xone_controller_mac(xone_dongle const *d, int index, char *buf, std::lock_guard guard(d->lock); if (index < 0 || index >= static_cast(d->controllers.size())) return -1; - auto const& c = d->controllers[index]; + auto const& c = *d->controllers[index]; std::snprintf(buf, len, "%02x:%02x:%02x:%02x:%02x:%02x", c.mac[0], c.mac[1], c.mac[2], c.mac[3], c.mac[4], c.mac[5]); return 0; } +// Copy the latest input state for the controller at index. +extern "C" int xone_controller_get_state( + xone_dongle const *d, int index, struct xone_controller_state *state) +{ + if (!d || !state) + return -1; + + std::lock_guard guard(d->lock); + if (index < 0 || index >= static_cast(d->controllers.size())) + return -1; + + auto const& c = *d->controllers[index]; + state->buttons = c.buttons; + state->guide_down = c.guide_down; + state->gip_ready = c.gip_ready; + state->input_active = c.input_active; + state->trigger_left = c.trigger_left; + state->trigger_right = c.trigger_right; + state->stick_left_x = c.stick_left_x; + state->stick_left_y = c.stick_left_y; + state->stick_right_x = c.stick_right_x; + state->stick_right_y = c.stick_right_y; + state->sequence = c.input_sequence; + return 0; +} + // Enter or leave pairing mode: sets the beacon pairing flag and blinks the // LED while enabled (port of xone_dongle_toggle_pairing). extern "C" void xone_set_pairing(xone_dongle *d, bool enable) diff --git a/src/app/main.swift b/src/app/main.swift index 25c5956..f982f9f 100644 --- a/src/app/main.swift +++ b/src/app/main.swift @@ -24,16 +24,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } +private struct ControllerSnapshot: Identifiable { + let id: String + let buttons: UInt16 + let guideDown: Bool + let gipReady: Bool + let inputActive: Bool + let triggerLeft: UInt16 + let triggerRight: UInt16 + let stickLeftX: Int16 + let stickLeftY: Int16 + let stickRightX: Int16 + let stickRightY: Int16 + let sequence: UInt32 +} + struct ContentView: View { @State private var donglePresent = false @State private var session: OpaquePointer? = nil // Radio state polled from the background worker each tick; a change here // is what re-renders the debug section. @State private var radioState = XONE_STATE_IDLE - // Connected controllers (MAC strings), refreshed each tick. - @State private var controllers: [String] = [] + // Connected controllers and their latest input snapshot. + @State private var controllers: [ControllerSnapshot] = [] - private let timer = Timer.publish(every: 1.0, on: .main, in: .common) + private let timer = Timer.publish(every: 0.1, on: .main, in: .common) .autoconnect() var body: some View { @@ -138,12 +153,32 @@ struct ContentView: View { // Query the C API for connected controllers and store their MACs. private func refreshControllers(_ session: OpaquePointer) { let count = Int(xone_controller_count(session)) - var list: [String] = [] + var list: [ControllerSnapshot] = [] for i in 0.. some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "gamecontroller") + .foregroundStyle(.secondary) + Text(controller.id) + .font(.system(.body, design: .monospaced)) + Spacer() + Text(controller.gipReady ? "ready" : "connecting") + .foregroundStyle(controller.gipReady ? .green : .orange) + } + + HStack(spacing: 4) { + buttonIndicator("Guide", pressed: controller.guideDown) + buttonIndicator("A", pressed: isPressed(controller, 0x0010)) + buttonIndicator("B", pressed: isPressed(controller, 0x0020)) + buttonIndicator("X", pressed: isPressed(controller, 0x0040)) + buttonIndicator("Y", pressed: isPressed(controller, 0x0080)) + } + + HStack(spacing: 4) { + buttonIndicator("LB", pressed: isPressed(controller, 0x1000)) + buttonIndicator("RB", pressed: isPressed(controller, 0x2000)) + buttonIndicator("LS", pressed: isPressed(controller, 0x4000)) + buttonIndicator("RS", pressed: isPressed(controller, 0x8000)) + buttonIndicator("↑", pressed: isPressed(controller, 0x0100)) + buttonIndicator("↓", pressed: isPressed(controller, 0x0200)) + buttonIndicator("←", pressed: isPressed(controller, 0x0400)) + buttonIndicator("→", pressed: isPressed(controller, 0x0800)) + } + + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text("Left stick") + .foregroundStyle(.secondary) + signedAxis("X", controller.stickLeftX) + signedAxis("Y", controller.stickLeftY) + } + VStack(alignment: .leading, spacing: 4) { + Text("Right stick") + .foregroundStyle(.secondary) + signedAxis("X", controller.stickRightX) + signedAxis("Y", controller.stickRightY) + } + } + + HStack(spacing: 12) { + triggerAxis("Left trigger", controller.triggerLeft) + triggerAxis("Right trigger", controller.triggerRight) + } + + Text(controller.inputActive + ? "Input #\(controller.sequence)" + : "Waiting for input") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(10) + .background(Color.secondary.opacity(0.08)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + + private func isPressed(_ controller: ControllerSnapshot, _ mask: UInt16) -> Bool { + (controller.buttons & mask) != 0 + } + + private func buttonIndicator(_ title: String, pressed: Bool) -> some View { + Text(title) + .font(.caption2) + .frame(minWidth: 28, minHeight: 22) + .padding(.horizontal, 2) + .background(pressed ? Color.green : Color.secondary.opacity(0.18)) + .foregroundStyle(pressed ? .white : .primary) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + + private func signedAxis(_ label: String, _ value: Int16) -> some View { + let normalized = Double(Int(value) + 32768) / 65535.0 + return HStack(spacing: 4) { + Text(label) + .font(.caption2) + .frame(width: 12, alignment: .leading) + ProgressView(value: normalized) + .frame(width: 82) + Text("\(value)") + .font(.caption2.monospacedDigit()) + .frame(width: 42, alignment: .trailing) + } + } + + private func triggerAxis(_ label: String, _ value: UInt16) -> some View { + HStack(spacing: 4) { + Text(label) + .font(.caption2) + .frame(width: 76, alignment: .leading) + ProgressView(value: Double(value), total: 1023) + .frame(width: 70) + Text("\(value)") + .font(.caption2.monospacedDigit()) + .frame(width: 32, alignment: .trailing) + } + } } // Entry point: SwiftUI App provides a static main() (top-level code lives in diff --git a/src/mt76/mt76.cpp b/src/mt76/mt76.cpp index 02e9dec..bf058a8 100644 --- a/src/mt76/mt76.cpp +++ b/src/mt76/mt76.cpp @@ -808,6 +808,49 @@ auto chip::send_wlan(std::span frame) -> int return ret < 0 ? ret : 0; } +auto chip::send_client_frame(std::uint8_t wcid, + std::span addr, + std::span frame, + bool encrypted) -> int +{ + auto address = mac_address(); + std::uint8_t data[8] = { + 0x00, 0x00, 0x00, static_cast(wcid - 1), + 0x00, 0x00, 0x00, 0x00, + }; + + std::uint16_t fc = ieee80211_ftype_data | ieee80211_stype_qos_data + | ieee80211_fctl_from_ds; + if (encrypted) + fc |= ieee80211_fctl_protected; + + std::uint8_t hdr[26] = {}; + xone::store_le16(hdr + 0, fc); + xone::store_le16(hdr + 2, 144); // duration + std::memcpy(hdr + 4, addr.data(), 6); + std::memcpy(hdr + 10, address.data(), 6); + std::memcpy(hdr + 16, address.data(), 6); + // bytes 22-23 are sequence control and stay zero + // bytes 24-25 are QoS control and stay zero + + auto txwi = build_txwi(static_cast(wcid - 1), + sizeof(hdr) + frame.size()); + std::vector payload(8 + txwi.size() + sizeof(hdr) + 2 + + frame.size()); + std::memcpy(payload.data(), data, sizeof(data)); + std::memcpy(payload.data() + 8, txwi.data(), txwi.size()); + std::memcpy(payload.data() + 8 + txwi.size(), hdr, sizeof(hdr)); + std::memcpy(payload.data() + 8 + txwi.size() + sizeof(hdr) + 2, + frame.data(), frame.size()); + + auto info = mt_mcu_msg_type_cmd + | field_prep(mt_mcu_msg_port, dma_msg_port::cpu_tx_port) + | field_prep(mt_mcu_msg_cmd_type, 0); + auto buf = build_message(info, payload.data(), payload.size()); + auto ret = transport_.bulk_write(buf.data(), buf.size()); + return ret < 0 ? ret : 0; +} + auto chip::associate_client(std::uint8_t wcid, std::span addr) -> int { diff --git a/tests/test_mt76.cpp b/tests/test_mt76.cpp index 0953ceb..dbeb644 100644 --- a/tests/test_mt76.cpp +++ b/tests/test_mt76.cpp @@ -135,7 +135,7 @@ void test_client_association_layout(void) // Client command and 802.11 frame control values. TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_assoc_resp, 0x0010); TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_disassoc, 0x00a0); - TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_qos_data, 0x0070); + TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_qos_data, 0x0080); TEST_ASSERT_EQUAL_UINT16( xone::mt76::ieee80211_stype_wlan_reserved, 0x0070); }