feat: add IOKit-based USB transport

Replace the stub probe with a real IOKit/IOUSBFamily transport:
discovery by VID/PID, device and interface connections, vendor
control requests on EP0, bulk writes to EP 0x04 OUT, and an async
read pump (EP 0x05 IN / EP 0x04 IN) on a dedicated CFRunLoop
thread with disconnect notification.

Add the test_usb suite, link IOKit/CoreFoundation into xone_usb,
and update the README investigation checklist.

Co-Authored-By: qwen3.8-27b@q2_k_xl: implemented USB transport layer
This commit is contained in:
portersky
2026-08-17 15:52:22 +02:00
parent 5a102bb511
commit 4f0f1ff75d
7 changed files with 690 additions and 23 deletions
+101 -7
View File
@@ -7,19 +7,113 @@
// VID 0x045E, PIDs 0x02E6 (old), 0x02FE (new), 0x02F9 (ASUS/Lenovo built-in),
// 0x091E (Surface Book 2).
//
// Port target: medusalix/xone transport/dongle.c + transport/mt76.c USB calls.
// Port target: medusalix/xone transport/dongle.c + the USB calls in mt76.c.
// Linux usb_control_msg()/usb_bulk_msg() map to IOUSBDeviceInterface
// DeviceRequest / IOUSBInterfaceInterface WritePipe+ReadPipe.
// DeviceRequest and IOUSBInterfaceInterface WritePipe/ReadPipeAsync.
// ==============================================================================
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
namespace xone::usb {
// Probe for a connected dongle and claim its WLAN interface
// (bInterfaceNumber 1).
// TODO(phase 2): IOKit implementation (IOServiceMatching, interface open,
// EP 0x04 bulk in/out + EP 0x05 interrupt in).
auto probe() -> bool;
// Vendor ID of the Xbox Wireless Dongle.
constexpr std::uint16_t vid = 0x045E;
// Supported dongle PIDs (port of xone_dongle_id_table in transport/dongle.c).
constexpr std::uint16_t pid_old_dongle = 0x02E6;
constexpr std::uint16_t pid_new_dongle = 0x02FE;
constexpr std::uint16_t pid_builtin_asus_lenovo = 0x02F9;
constexpr std::uint16_t pid_surface_book_2 = 0x091E;
// Vendor control request codes (port of MT_VEND_* in transport/mt76_defs.h).
enum class vendor_request : std::uint8_t {
dev_mode = 0x01, // enter firmware load mode
write = 0x02, // register write
power_on = 0x04,
multi_write = 0x06, // register write (multi)
multi_read = 0x07, // register read (multi)
read_eeprom = 0x09,
write_fce = 0x42,
write_cfg = 0x46, // register write (config space)
read_cfg = 0x47, // register read (config space)
read_ext = 0x63,
write_ext = 0x66,
feature_set = 0x91,
};
// Bulk endpoint numbers (port of XONE_MT_EP_* in transport/mt76.h).
constexpr std::uint8_t ep_in_cmd = 0x05; // MCU command responses
constexpr std::uint8_t ep_in_wlan = 0x04; // WLAN data frames
constexpr std::uint8_t ep_out = 0x04; // commands to the chip
// Receive buffer sizes (port of XONE_DONGLE_LEN_* in transport/dongle.c).
constexpr std::size_t len_cmd_pkt = 0x0654;
constexpr std::size_t len_wlan_pkt = 0x8400;
// Delivered from the transport's reader thread for every message received
// on a bulk IN endpoint. `ep` is ep_in_cmd or ep_in_wlan. Do not block and
// do not destroy the transport from within the callback.
using frame_callback = std::function<void(std::uint8_t ep, void const *data, std::size_t len)>;
// Delivered once from the transport's reader thread when the dongle is
// disconnected (including the chip reconnect during firmware load). Do not
// block and do not destroy the transport from within the callback.
using disconnect_callback = std::function<void()>;
// Owns an open Xbox Wireless Dongle: its device and interface connections,
// endpoint pipes, and the reader thread that pumps async bulk IN reads.
class transport {
public:
// Probe for a connected dongle (any supported PID), open it, and start
// the async read pump on EP 0x05 IN and EP 0x04 IN. `frames` receives
// each received message and `disconnected` fires when the dongle goes
// away; either may be empty. Returns nullptr if no dongle is present or
// opening fails.
static auto probe(frame_callback frames, disconnect_callback disconnected) -> std::unique_ptr<transport>;
~transport();
transport(transport const&) = delete;
transport& operator=(transport const&) = delete;
// Product ID of the connected dongle.
auto pid() const -> std::uint16_t;
// Vendor control request on EP0 (register R/W, firmware mode). `is_read`
// selects IN (`data` holds len bytes to receive) or OUT (`data` holds
// len bytes to send). Returns bytes transferred, or -EIO.
auto send_vendor_request(vendor_request req, bool is_read, std::uint16_t w_value,
std::uint16_t w_index, void *data, std::size_t len) -> int;
// Bulk write to EP 0x04 OUT. Returns bytes written, or -EIO.
auto bulk_write(void const *data, std::size_t len) -> int;
private:
struct read_slot;
struct state;
transport() = default;
auto open(frame_callback frames, disconnect_callback disconnected) -> bool;
// IOKit services are uint32_t registry handles (io_service_t).
auto open_interface(std::uint32_t child) -> bool;
auto submit_read(read_slot *slot) -> bool;
void worker_loop();
// IOKit callbacks use IOReturn (int32_t) and io_iterator_t (uint32_t).
void handle_read(read_slot *slot, std::int32_t result, std::size_t len);
void handle_disconnected();
static void on_read_completion(void *refcon, std::int32_t result, void *arg0);
static void on_dongle_terminated(void *refcon, std::uint32_t iter);
std::unique_ptr<state> state_;
};
// TRUE if an Xbox Wireless Dongle is connected (any supported PID).
auto dongle_present() -> bool;
} // namespace xone::usb