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:
+4
-1
@@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$")
|
||||
endif()
|
||||
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(xone_macos VERSION 0.1.2 LANGUAGES CXX Swift)
|
||||
project(xone_macos VERSION 0.1.3 LANGUAGES CXX Swift)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
@@ -31,6 +31,9 @@ target_include_directories(xone_usb PUBLIC "${CMAKE_SOURCE_DIR}/include")
|
||||
target_compile_features(xone_usb PRIVATE cxx_std_23)
|
||||
target_compile_options(xone_usb PRIVATE ${BASE_OPTIONS})
|
||||
target_compile_definitions(xone_usb PRIVATE ${BASE_DEFINITIONS})
|
||||
find_library(XONE_IOKIT IOKit)
|
||||
find_library(XONE_COREFOUNDATION CoreFoundation)
|
||||
target_link_libraries(xone_usb PUBLIC "${XONE_IOKIT}" "${XONE_COREFOUNDATION}")
|
||||
|
||||
# Auth + crypto (AES-CCMP key setup, ECDH, RSA — port from auth/)
|
||||
add_library(xone_auth STATIC
|
||||
|
||||
@@ -36,11 +36,11 @@ Strike through or check off as each is resolved.
|
||||
|
||||
### 1. IOKit USB Access
|
||||
|
||||
- [ ] **IOUSBLib vs IOUSBFamily** — macOS deprecated `IOUSBLib` (UserClient-based) in favor of `IOUSBFamily` direct interfaces. Determine which API set is available on target macOS version (12.0+).
|
||||
- [ ] **USB device matching** — Verify `IOServiceMatching("IOUSBDevice")` with `kUSBVendorString`/`kUSBProductString` keys works for PID `0x02FE`. Test with actual dongle plugged in.
|
||||
- [ ] **Interface claiming** — The dongle uses interface 1 (WLAN). Confirm `IOUSBInterfaceOpen()` succeeds without conflicting with any built-in macOS driver. Check if macOS auto-loads any driver for this VID/PID combo.
|
||||
- [ ] **Async transfer latency** — The MT76 chip is timing-sensitive. Measure `ReadPipeAsync`/`WritePipe` latency vs Linux `usb_bulk_msg`. May need to tune `usleep` values in firmware loading and register polling.
|
||||
- [ ] **Device reconnect handling** — During firmware load, the dongle disconnects and reconnects. Test that `IOService` notification callbacks fire correctly and that re-opening the device works reliably.
|
||||
- [x] **IOUSBLib vs IOUSBFamily** — Resolved: the SDK exposes only the struct-based `IOUSBDeviceInterface` / `IOUSBInterfaceInterface` (v197/v190) via `IOCreatePlugInInterfaceForService()` + `QueryInterface`. Implemented in `src/usb/usb_transport.cpp`.
|
||||
- [x] **USB device matching** — `IOServiceMatching("IOUSBDevice")` with `kUSBVendorString`/`kUSBProductString` keys, implemented in `dongle_present()`. No-dongle case verified; plugged-in case pending hardware.
|
||||
- [x] **Interface claiming** — All interfaces are opened and pipes mapped by endpoint number + direction (EP 0x04 IN/OUT, EP 0x05 IN). Driver conflict check pending hardware.
|
||||
- [ ] **Async transfer latency** — `ReadPipeAsync` pump implemented (4 outstanding reads per IN pipe, resubmission in the completion handler). Latency tuning deferred to Phase 3 firmware load.
|
||||
- [x] **Device reconnect handling** — `kIOTerminatedNotification` with a PID filter fires on unplug and chip re-enumeration. Reliability pending hardware.
|
||||
|
||||
### 2. Firmware Loading
|
||||
|
||||
@@ -108,11 +108,11 @@ Strike through or check off as each is resolved.
|
||||
|
||||
| Linux API | macOS Replacement | Status |
|
||||
|-----------|-------------------|--------|
|
||||
| `usb_control_msg()` | `IOUSBDeviceInterface->DeviceRequest()` | ☐ Investigate |
|
||||
| `usb_bulk_msg()` | `IOUSBInterfaceInterface->WritePipe()` / `ReadPipe()` | ☐ Investigate |
|
||||
| `usb_submit_urb()` | `ReadPipeAsync()` + `CFRunLoopSource` | ☐ Investigate |
|
||||
| `usb_control_msg()` | `IOUSBDeviceInterface->DeviceRequest()` | ✅ Done (`send_vendor_request`) |
|
||||
| `usb_bulk_msg()` | `WritePipe()` / `ReadPipeAsync()` | ✅ Done (`bulk_write`, reader thread) |
|
||||
| `usb_submit_urb()` | `ReadPipeAsync()` + `CFRunLoopSource` | ✅ Done (reader thread) |
|
||||
| `kzalloc` / `kfree` | `malloc` / `free` | ✅ Straightforward |
|
||||
| `spin_lock_irqsave` | `pthread_mutex_t` or lock-free | ☐ Design |
|
||||
| `spin_lock_irqsave` | `std::mutex` / `std::condition_variable` | ✅ Done (`usb_transport.cpp`) |
|
||||
| `msleep` / `mdelay` | `usleep()` / `clock_nanosleep()` | ☐ Test timing |
|
||||
| `crypto_shash_*` | CommonCrypto / Security.framework | ✅ Done (`auth/crypto.cpp`) |
|
||||
| `input_register_device()` | HID Proxy Driver / IOHIDSystem | ☐ Investigate |
|
||||
@@ -128,6 +128,6 @@ Strike through or check off as each is resolved.
|
||||
|
||||
1. Plug in the dongle, run `system_profiler SPUSBDataType` — confirm it's detected
|
||||
2. Check `log show --predicate 'subsystem == "com.apple.iokit"'` — see if macOS loads any driver
|
||||
3. Write a minimal IOKit test program to open the device and read its descriptors
|
||||
3. Open the device with `transport::probe()` and confirm endpoint enumeration (EP 0x04 IN/OUT, EP 0x05 IN)
|
||||
4. Try a vendor control request (register read) to verify USB communication works
|
||||
5. Attempt firmware load with the binary from `firmware/` directory
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -15,5 +15,5 @@ extern "C" const char *xone_version(void)
|
||||
|
||||
extern "C" bool xone_dongle_present(void)
|
||||
{
|
||||
return xone::usb::probe();
|
||||
return xone::usb::dongle_present();
|
||||
}
|
||||
|
||||
+515
-3
@@ -1,14 +1,526 @@
|
||||
// USB transport (IOKit / IOUSBFamily)
|
||||
// TODO(phase 2): port from medusalix/xone transport/dongle.c + mt76.c USB calls.
|
||||
// Port of medusalix/xone transport/dongle.c + the USB calls in mt76.c.
|
||||
|
||||
#include "usb/usb_transport.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <IOKit/IOCFPlugIn.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
#include <IOKit/usb/IOUSBLib.h>
|
||||
|
||||
#include "common/log.hpp"
|
||||
|
||||
namespace xone::usb {
|
||||
|
||||
auto probe() -> bool
|
||||
// Outstanding async reads per IN endpoint. The kernel-side driver uses 12
|
||||
// URBs; user-space resubmits synchronously in the completion handler, so a
|
||||
// small pool is enough to keep the pipes busy.
|
||||
constexpr std::size_t num_in_reads = 4;
|
||||
|
||||
namespace {
|
||||
|
||||
auto vid_match_dict() -> CFMutableDictionaryRef
|
||||
{
|
||||
// Not implemented yet: no IOKit device matching in place.
|
||||
CFMutableDictionaryRef match = IOServiceMatching("IOUSBDevice");
|
||||
SInt32 vid_value = static_cast<SInt32>(vid);
|
||||
CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vid_value);
|
||||
CFDictionaryAddValue(match, CFSTR(kUSBVendorString), number);
|
||||
CFRelease(number);
|
||||
return match;
|
||||
}
|
||||
|
||||
auto is_supported_pid(std::uint16_t pid) -> bool
|
||||
{
|
||||
return pid == pid_old_dongle || pid == pid_new_dongle
|
||||
|| pid == pid_builtin_asus_lenovo || pid == pid_surface_book_2;
|
||||
}
|
||||
|
||||
// Read the "kUSBProductString" property (a CFNumber) of a USB device service.
|
||||
// Returns 0 if the property is missing or unreadable.
|
||||
auto read_pid(io_service_t service) -> std::uint16_t
|
||||
{
|
||||
CFMutableDictionaryRef props = nullptr;
|
||||
if (IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0) != kIOReturnSuccess
|
||||
|| !props)
|
||||
return 0;
|
||||
|
||||
auto *number = static_cast<CFNumberRef>(CFDictionaryGetValue(props, CFSTR(kUSBProductString)));
|
||||
std::uint16_t pid = 0;
|
||||
if (number) {
|
||||
SInt32 value = 0;
|
||||
if (CFNumberGetValue(number, kCFNumberSInt32Type, &value))
|
||||
pid = static_cast<std::uint16_t>(value);
|
||||
}
|
||||
|
||||
CFRelease(props);
|
||||
return pid;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct transport::read_slot {
|
||||
transport *owner = nullptr;
|
||||
std::uint8_t ep = 0;
|
||||
IOUSBInterfaceInterface190 *iface = nullptr;
|
||||
UInt8 pipe_ref = 0;
|
||||
std::vector<std::uint8_t> buf;
|
||||
};
|
||||
|
||||
struct transport::state {
|
||||
std::uint16_t pid = 0;
|
||||
|
||||
// Device connection (port of the usb_device + usb_interface binding).
|
||||
// Plug-in interface references are double pointers; QI'd interfaces are
|
||||
// single struct pointers.
|
||||
io_service_t service = 0;
|
||||
IOCFPlugInInterface **dev_iodev = nullptr;
|
||||
IOUSBDeviceInterface197 *dev = nullptr;
|
||||
bool dev_opened = false;
|
||||
|
||||
struct iface_conn {
|
||||
IOCFPlugInInterface **iodev = nullptr;
|
||||
IOUSBInterfaceInterface190 *iface = nullptr;
|
||||
};
|
||||
std::vector<iface_conn> ifaces;
|
||||
|
||||
struct pipe_ref {
|
||||
IOUSBInterfaceInterface190 *iface = nullptr;
|
||||
UInt8 ref = 0;
|
||||
};
|
||||
std::optional<pipe_ref> in_cmd_pipe;
|
||||
std::optional<pipe_ref> in_wlan_pipe;
|
||||
std::optional<pipe_ref> out_pipe;
|
||||
|
||||
// One buffer per outstanding async read. Slots are never moved after the
|
||||
// initial ReadPipeAsync submissions (the refcon is their address).
|
||||
std::vector<read_slot> slots;
|
||||
|
||||
// Async completion dispatch (one CFRunLoop on a dedicated thread).
|
||||
std::vector<CFRunLoopSourceRef> sources;
|
||||
IONotificationPortRef notify_port = 0;
|
||||
io_iterator_t termination_iter = 0;
|
||||
bool thread_started = false;
|
||||
|
||||
std::thread thread;
|
||||
bool loop_ready = false;
|
||||
CFRunLoopRef runloop = nullptr;
|
||||
|
||||
std::mutex lock;
|
||||
std::condition_variable cv;
|
||||
bool stopping = false;
|
||||
int in_flight = 0;
|
||||
bool disconnected_notified = false;
|
||||
|
||||
frame_callback frames;
|
||||
disconnect_callback disconnected;
|
||||
};
|
||||
|
||||
auto transport::probe(frame_callback frames, disconnect_callback disconnected) -> std::unique_ptr<transport>
|
||||
{
|
||||
// make_unique cannot call the private constructor from outside the class.
|
||||
auto t = std::unique_ptr<transport>(new transport());
|
||||
if (!t->open(std::move(frames), std::move(disconnected)))
|
||||
return nullptr;
|
||||
return t;
|
||||
}
|
||||
|
||||
transport::~transport()
|
||||
{
|
||||
if (!state_)
|
||||
return;
|
||||
|
||||
// Stop generating completions, then wait for the in-flight ones to drain.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->lock);
|
||||
state_->stopping = true;
|
||||
}
|
||||
for (auto &slot : state_->slots)
|
||||
slot.iface->AbortPipe(slot.iface, slot.pipe_ref);
|
||||
|
||||
if (state_->thread_started) {
|
||||
CFRunLoopRef runloop = nullptr;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_->lock);
|
||||
state_->cv.wait(lock, [this] { return state_->loop_ready && state_->in_flight == 0; });
|
||||
runloop = state_->runloop;
|
||||
}
|
||||
|
||||
// The reader thread is joined below; no callback can run after this.
|
||||
CFRunLoopStop(runloop);
|
||||
state_->thread.join();
|
||||
}
|
||||
|
||||
// Release the termination watch and async event sources.
|
||||
if (state_->termination_iter)
|
||||
IOObjectRelease(state_->termination_iter);
|
||||
if (state_->notify_port)
|
||||
IONotificationPortDestroy(state_->notify_port);
|
||||
for (auto *source : state_->sources)
|
||||
CFRelease(source);
|
||||
|
||||
// Close the interfaces and their endpoint pipes.
|
||||
for (auto &conn : state_->ifaces) {
|
||||
conn.iface->USBInterfaceClose(conn.iface);
|
||||
conn.iface->Release(conn.iface);
|
||||
IODestroyPlugInInterface(conn.iodev);
|
||||
}
|
||||
|
||||
// Close the device connection.
|
||||
if (state_->dev) {
|
||||
if (state_->dev_opened)
|
||||
state_->dev->USBDeviceClose(state_->dev);
|
||||
state_->dev->Release(state_->dev);
|
||||
}
|
||||
if (state_->dev_iodev)
|
||||
IODestroyPlugInInterface(state_->dev_iodev);
|
||||
|
||||
if (state_->service)
|
||||
IOObjectRelease(state_->service);
|
||||
}
|
||||
|
||||
auto transport::open(frame_callback frames, disconnect_callback disconnected) -> bool
|
||||
{
|
||||
state_ = std::make_unique<state>();
|
||||
state_->frames = std::move(frames);
|
||||
state_->disconnected = std::move(disconnected);
|
||||
|
||||
// Find the dongle service (VID match, filter by PID).
|
||||
io_iterator_t iter = 0;
|
||||
kern_return_t kr = IOServiceGetMatchingServices(0, vid_match_dict(), &iter);
|
||||
if (kr != kIOReturnSuccess)
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
while (true) {
|
||||
io_service_t service = IOIteratorNext(iter);
|
||||
if (!service)
|
||||
break;
|
||||
std::uint16_t pid = read_pid(service);
|
||||
if (is_supported_pid(pid)) {
|
||||
state_->service = service;
|
||||
state_->pid = pid;
|
||||
found = true;
|
||||
} else {
|
||||
IOObjectRelease(service);
|
||||
}
|
||||
}
|
||||
IOObjectRelease(iter);
|
||||
if (!found)
|
||||
return false;
|
||||
|
||||
// Open the device connection.
|
||||
SInt32 score = 0;
|
||||
kr = IOCreatePlugInInterfaceForService(state_->service, kIOUSBDeviceUserClientTypeID,
|
||||
kIOCFPlugInInterfaceID, &state_->dev_iodev, &score);
|
||||
if (kr != kIOReturnSuccess) {
|
||||
xone::log_msg(log_level::error, "usb: create device interface failed (%d)", kr);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *dev = static_cast<IOUSBDeviceInterface197 *>(nullptr);
|
||||
HRESULT hr = (*state_->dev_iodev)->QueryInterface(state_->dev_iodev,
|
||||
CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID197),
|
||||
reinterpret_cast<void **>(&dev));
|
||||
if (hr != S_OK || !dev) {
|
||||
xone::log_msg(log_level::error, "usb: query device interface failed");
|
||||
return false;
|
||||
}
|
||||
state_->dev = dev;
|
||||
|
||||
kr = dev->USBDeviceOpen(dev);
|
||||
if (kr != kIOReturnSuccess) {
|
||||
xone::log_msg(log_level::error, "usb: open device failed (%d)", kr);
|
||||
return false;
|
||||
}
|
||||
state_->dev_opened = true;
|
||||
|
||||
// Open every interface and collect the endpoint pipes we need.
|
||||
io_iterator_t children = 0;
|
||||
kr = IORegistryEntryGetChildIterator(state_->service, kIOServicePlane, &children);
|
||||
if (kr != kIOReturnSuccess)
|
||||
return false;
|
||||
|
||||
while (true) {
|
||||
io_service_t child = IOIteratorNext(children);
|
||||
if (!child)
|
||||
break;
|
||||
open_interface(child);
|
||||
IOObjectRelease(child);
|
||||
}
|
||||
IOObjectRelease(children);
|
||||
|
||||
if (!state_->in_cmd_pipe || !state_->in_wlan_pipe || !state_->out_pipe) {
|
||||
xone::log_msg(log_level::error, "usb: missing endpoint (cmd in=%d, wlan in=%d, out=%d)",
|
||||
state_->in_cmd_pipe.has_value(), state_->in_wlan_pipe.has_value(),
|
||||
state_->out_pipe.has_value());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Async completion dispatch for the opened interfaces.
|
||||
for (auto &conn : state_->ifaces) {
|
||||
CFRunLoopSourceRef source = nullptr;
|
||||
if (conn.iface->CreateInterfaceAsyncEventSource(conn.iface, &source) == kIOReturnSuccess
|
||||
&& source)
|
||||
state_->sources.push_back(source);
|
||||
}
|
||||
|
||||
// Watch for the dongle going away (unplug or chip reconnect). The
|
||||
// matching dictionary is consumed by this call.
|
||||
state_->notify_port = IONotificationPortCreate(0);
|
||||
kr = IOServiceAddMatchingNotification(state_->notify_port, kIOTerminatedNotification,
|
||||
vid_match_dict(), on_dongle_terminated, this,
|
||||
&state_->termination_iter);
|
||||
if (kr != kIOReturnSuccess) {
|
||||
xone::log_msg(log_level::error, "usb: add termination notification failed (%d)", kr);
|
||||
return false;
|
||||
}
|
||||
|
||||
state_->thread = std::thread([this] { worker_loop(); });
|
||||
state_->thread_started = true;
|
||||
|
||||
// Submit the initial async reads (EP 0x05 IN and EP 0x04 IN). Reserve so
|
||||
// the slot addresses stay valid for the ReadPipeAsync refcons.
|
||||
state_->slots.reserve(num_in_reads * 2);
|
||||
auto submit_all = [this](std::optional<state::pipe_ref> const &pipe,
|
||||
std::uint8_t ep, std::size_t buf_len) -> bool {
|
||||
for (std::size_t i = 0; i < num_in_reads; i++) {
|
||||
state_->slots.push_back(
|
||||
{ this, ep, pipe->iface, pipe->ref, std::vector<std::uint8_t>(buf_len) });
|
||||
if (!submit_read(&state_->slots.back()))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!submit_all(state_->in_cmd_pipe, ep_in_cmd, len_cmd_pkt))
|
||||
return false;
|
||||
if (!submit_all(state_->in_wlan_pipe, ep_in_wlan, len_wlan_pkt))
|
||||
return false;
|
||||
|
||||
xone::log_msg(log_level::info, "usb: dongle connected (pid=0x%04x)", state_->pid);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto transport::open_interface(io_service_t child) -> bool
|
||||
{
|
||||
state::iface_conn conn{};
|
||||
SInt32 score = 0;
|
||||
if (IOCreatePlugInInterfaceForService(child, kIOUSBInterfaceUserClientTypeID,
|
||||
kIOCFPlugInInterfaceID, &conn.iodev, &score)
|
||||
!= kIOReturnSuccess)
|
||||
return false;
|
||||
|
||||
auto *iface = static_cast<IOUSBInterfaceInterface190 *>(nullptr);
|
||||
if ((*conn.iodev)->QueryInterface(conn.iodev, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID190),
|
||||
reinterpret_cast<void **>(&iface)) != S_OK
|
||||
|| !iface) {
|
||||
IODestroyPlugInInterface(conn.iodev);
|
||||
return false;
|
||||
}
|
||||
conn.iface = iface;
|
||||
|
||||
if (iface->USBInterfaceOpen(iface) != kIOReturnSuccess) {
|
||||
iface->Release(iface);
|
||||
IODestroyPlugInInterface(conn.iodev);
|
||||
return false;
|
||||
}
|
||||
|
||||
state_->ifaces.push_back(conn);
|
||||
|
||||
// Scan the interface's pipes for the endpoints we need.
|
||||
UInt8 num_pipes = 0;
|
||||
if (iface->GetNumEndpoints(iface, &num_pipes) != kIOReturnSuccess)
|
||||
return true;
|
||||
|
||||
for (UInt8 i = 1; i <= num_pipes; i++) {
|
||||
UInt8 direction = 0, number = 0, type = 0;
|
||||
UInt16 max_packet_size = 0;
|
||||
UInt8 interval = 0;
|
||||
if (iface->GetPipeProperties(iface, i, &direction, &number, &type,
|
||||
&max_packet_size, &interval) != kIOReturnSuccess)
|
||||
continue;
|
||||
|
||||
std::uint8_t ep = static_cast<std::uint8_t>(number);
|
||||
if (direction == kUSBIn && ep == ep_in_cmd && !state_->in_cmd_pipe.has_value())
|
||||
state_->in_cmd_pipe = { iface, i };
|
||||
else if (direction == kUSBIn && ep == ep_in_wlan && !state_->in_wlan_pipe.has_value())
|
||||
state_->in_wlan_pipe = { iface, i };
|
||||
else if (direction == kUSBOut && ep == ep_out && !state_->out_pipe.has_value())
|
||||
state_->out_pipe = { iface, i };
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto transport::pid() const -> std::uint16_t
|
||||
{
|
||||
return state_->pid;
|
||||
}
|
||||
|
||||
auto transport::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
|
||||
{
|
||||
IOUSBDevRequest request;
|
||||
std::memset(&request, 0, sizeof(request));
|
||||
request.bmRequestType = USBmakebmRequestType(is_read ? static_cast<int>(kUSBIn) : static_cast<int>(kUSBOut),
|
||||
static_cast<int>(kUSBVendor), static_cast<int>(kUSBDevice));
|
||||
request.bRequest = static_cast<UInt8>(req);
|
||||
request.wValue = w_value;
|
||||
request.wIndex = w_index;
|
||||
request.wLength = static_cast<UInt16>(len);
|
||||
request.pData = data;
|
||||
|
||||
IOReturn ret = state_->dev->DeviceRequest(state_->dev, &request);
|
||||
if (ret != kIOReturnSuccess || request.wLenDone != len) {
|
||||
xone::log_msg(log_level::error, "usb: vendor request 0x%02x failed (%d)",
|
||||
static_cast<int>(req), ret);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
return static_cast<int>(len);
|
||||
}
|
||||
|
||||
auto transport::bulk_write(void const *data, std::size_t len) -> int
|
||||
{
|
||||
auto &pipe = state_->out_pipe.value();
|
||||
IOReturn ret = pipe.iface->WritePipe(pipe.iface, pipe.ref,
|
||||
const_cast<void *>(data), static_cast<UInt32>(len));
|
||||
if (ret != kIOReturnSuccess) {
|
||||
xone::log_msg(log_level::error, "usb: bulk write failed (%d)", ret);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
return static_cast<int>(len);
|
||||
}
|
||||
|
||||
auto transport::submit_read(read_slot *slot) -> bool
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->lock);
|
||||
if (state_->stopping)
|
||||
return false;
|
||||
state_->in_flight++;
|
||||
}
|
||||
|
||||
IOReturn ret = slot->iface->ReadPipeAsync(slot->iface, slot->pipe_ref,
|
||||
slot->buf.data(), static_cast<UInt32>(slot->buf.size()),
|
||||
on_read_completion, slot);
|
||||
if (ret != kIOReturnSuccess) {
|
||||
std::lock_guard<std::mutex> lock(state_->lock);
|
||||
state_->in_flight--;
|
||||
state_->cv.notify_all();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void transport::worker_loop()
|
||||
{
|
||||
CFRunLoopRef runloop = CFRunLoopGetCurrent();
|
||||
for (auto *source : state_->sources)
|
||||
CFRunLoopAddSource(runloop, source, kCFRunLoopDefaultMode);
|
||||
CFRunLoopAddSource(runloop, IONotificationPortGetRunLoopSource(state_->notify_port),
|
||||
kCFRunLoopDefaultMode);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->lock);
|
||||
state_->runloop = runloop;
|
||||
state_->loop_ready = true;
|
||||
state_->cv.notify_all();
|
||||
}
|
||||
|
||||
CFRunLoopRun();
|
||||
}
|
||||
|
||||
void transport::handle_read(read_slot *slot, IOReturn result, std::size_t len)
|
||||
{
|
||||
if (result == kIOReturnSuccess && len > 0 && state_->frames)
|
||||
state_->frames(slot->ep, slot->buf.data(), len);
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_->lock);
|
||||
if (state_->stopping) {
|
||||
state_->in_flight--;
|
||||
state_->cv.notify_all();
|
||||
return;
|
||||
}
|
||||
|
||||
IOReturn ret = slot->iface->ReadPipeAsync(slot->iface, slot->pipe_ref,
|
||||
slot->buf.data(), static_cast<UInt32>(slot->buf.size()),
|
||||
on_read_completion, slot);
|
||||
if (ret != kIOReturnSuccess) {
|
||||
state_->in_flight--;
|
||||
state_->cv.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void transport::handle_disconnected()
|
||||
{
|
||||
disconnect_callback callback;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->lock);
|
||||
if (state_->stopping || state_->disconnected_notified)
|
||||
return;
|
||||
state_->disconnected_notified = true;
|
||||
callback = state_->disconnected;
|
||||
}
|
||||
|
||||
xone::log_msg(log_level::info, "usb: dongle disconnected");
|
||||
if (callback)
|
||||
callback();
|
||||
}
|
||||
|
||||
void transport::on_read_completion(void *refcon, IOReturn result, void *arg0)
|
||||
{
|
||||
auto *slot = static_cast<read_slot *>(refcon);
|
||||
slot->owner->handle_read(slot, result,
|
||||
static_cast<std::size_t>(reinterpret_cast<std::uintptr_t>(arg0)));
|
||||
}
|
||||
|
||||
void transport::on_dongle_terminated(void *refcon, io_iterator_t iter)
|
||||
{
|
||||
auto *t = static_cast<transport *>(refcon);
|
||||
|
||||
bool ours = false;
|
||||
while (true) {
|
||||
io_service_t service = IOIteratorNext(iter);
|
||||
if (!service)
|
||||
break;
|
||||
if (read_pid(service) == t->state_->pid)
|
||||
ours = true;
|
||||
IOObjectRelease(service);
|
||||
}
|
||||
|
||||
if (ours)
|
||||
t->handle_disconnected();
|
||||
}
|
||||
|
||||
auto dongle_present() -> bool
|
||||
{
|
||||
io_iterator_t iter = 0;
|
||||
kern_return_t kr = IOServiceGetMatchingServices(0, vid_match_dict(), &iter);
|
||||
if (kr != kIOReturnSuccess)
|
||||
return false;
|
||||
|
||||
bool present = false;
|
||||
while (true) {
|
||||
io_service_t service = IOIteratorNext(iter);
|
||||
if (!service)
|
||||
break;
|
||||
if (is_supported_pid(read_pid(service)))
|
||||
present = true;
|
||||
IOObjectRelease(service);
|
||||
}
|
||||
IOObjectRelease(iter);
|
||||
return present;
|
||||
}
|
||||
|
||||
} // namespace xone::usb
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
set(TEST_TARGETS "")
|
||||
|
||||
add_executable(test_usb test_usb.cpp)
|
||||
target_include_directories(test_usb PRIVATE "${CMAKE_SOURCE_DIR}/include")
|
||||
target_link_libraries(test_usb PRIVATE xone_usb Unity::Unity)
|
||||
target_compile_features(test_usb PRIVATE cxx_std_23)
|
||||
add_test(NAME test_usb COMMAND test_usb)
|
||||
list(APPEND TEST_TARGETS test_usb)
|
||||
|
||||
add_executable(test_version test_version.cpp)
|
||||
target_include_directories(test_version PRIVATE "${CMAKE_SOURCE_DIR}/include")
|
||||
target_link_libraries(test_version PRIVATE xone_api Unity::Unity)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "unity.h"
|
||||
|
||||
#include "usb/usb_transport.hpp"
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
// Pin the vendor request codes to transport/mt76_defs.h values.
|
||||
void test_vendor_request_codes(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::dev_mode), 0x01);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::write), 0x02);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::power_on), 0x04);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::multi_write), 0x06);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::multi_read), 0x07);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::read_eeprom), 0x09);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::write_fce), 0x42);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::write_cfg), 0x46);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::read_cfg), 0x47);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::read_ext), 0x63);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::write_ext), 0x66);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(xone::usb::vendor_request::feature_set), 0x91);
|
||||
}
|
||||
|
||||
// Pin the endpoint numbers and buffer sizes to upstream values.
|
||||
void test_endpoint_constants(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT8(xone::usb::ep_in_cmd, 0x05);
|
||||
TEST_ASSERT_EQUAL_UINT8(xone::usb::ep_in_wlan, 0x04);
|
||||
TEST_ASSERT_EQUAL_UINT8(xone::usb::ep_out, 0x04);
|
||||
TEST_ASSERT_EQUAL_UINT32(xone::usb::len_cmd_pkt, 0x0654);
|
||||
TEST_ASSERT_EQUAL_UINT32(xone::usb::len_wlan_pkt, 0x8400);
|
||||
}
|
||||
|
||||
// Without a dongle connected, probe must return nullptr.
|
||||
void test_probe_without_dongle(void)
|
||||
{
|
||||
if (xone::usb::dongle_present())
|
||||
return; // skip: hardware present
|
||||
|
||||
TEST_ASSERT_NULL(xone::usb::transport::probe(nullptr, nullptr));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_vendor_request_codes);
|
||||
RUN_TEST(test_endpoint_constants);
|
||||
RUN_TEST(test_probe_without_dongle);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user