diff --git a/CMakeLists.txt b/CMakeLists.txt index a593039..8ddd5d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md index 1514583..7b7e6b6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/include/usb/usb_transport.hpp b/include/usb/usb_transport.hpp index 1c38bb0..a5c9fce 100644 --- a/include/usb/usb_transport.hpp +++ b/include/usb/usb_transport.hpp @@ -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 #include +#include +#include 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; + +// 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; + +// 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 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_; +}; + +// TRUE if an Xbox Wireless Dongle is connected (any supported PID). +auto dongle_present() -> bool; } // namespace xone::usb diff --git a/src/app/api.cpp b/src/app/api.cpp index 95acad1..bb436fa 100644 --- a/src/app/api.cpp +++ b/src/app/api.cpp @@ -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(); } diff --git a/src/usb/usb_transport.cpp b/src/usb/usb_transport.cpp index 0a34104..fa7a7f2 100644 --- a/src/usb/usb_transport.cpp +++ b/src/usb/usb_transport.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#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. - return false; + CFMutableDictionaryRef match = IOServiceMatching("IOUSBDevice"); + SInt32 vid_value = static_cast(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(CFDictionaryGetValue(props, CFSTR(kUSBProductString))); + std::uint16_t pid = 0; + if (number) { + SInt32 value = 0; + if (CFNumberGetValue(number, kCFNumberSInt32Type, &value)) + pid = static_cast(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 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 ifaces; + + struct pipe_ref { + IOUSBInterfaceInterface190 *iface = nullptr; + UInt8 ref = 0; + }; + std::optional in_cmd_pipe; + std::optional in_wlan_pipe; + std::optional out_pipe; + + // One buffer per outstanding async read. Slots are never moved after the + // initial ReadPipeAsync submissions (the refcon is their address). + std::vector slots; + + // Async completion dispatch (one CFRunLoop on a dedicated thread). + std::vector 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 +{ + // make_unique cannot call the private constructor from outside the class. + auto t = std::unique_ptr(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 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 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_->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(nullptr); + HRESULT hr = (*state_->dev_iodev)->QueryInterface(state_->dev_iodev, + CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID197), + reinterpret_cast(&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 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(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(nullptr); + if ((*conn.iodev)->QueryInterface(conn.iodev, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID190), + reinterpret_cast(&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(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(kUSBIn) : static_cast(kUSBOut), + static_cast(kUSBVendor), static_cast(kUSBDevice)); + request.bRequest = static_cast(req); + request.wValue = w_value; + request.wIndex = w_index; + request.wLength = static_cast(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(req), ret); + return -EIO; + } + + return static_cast(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(data), static_cast(len)); + if (ret != kIOReturnSuccess) { + xone::log_msg(log_level::error, "usb: bulk write failed (%d)", ret); + return -EIO; + } + + return static_cast(len); +} + +auto transport::submit_read(read_slot *slot) -> bool +{ + { + std::lock_guard 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(slot->buf.size()), + on_read_completion, slot); + if (ret != kIOReturnSuccess) { + std::lock_guard 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 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 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(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 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(refcon); + slot->owner->handle_read(slot, result, + static_cast(reinterpret_cast(arg0))); +} + +void transport::on_dongle_terminated(void *refcon, io_iterator_t iter) +{ + auto *t = static_cast(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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 821e684..dd27bba 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/test_usb.cpp b/tests/test_usb.cpp new file mode 100644 index 0000000..a669be0 --- /dev/null +++ b/tests/test_usb.cpp @@ -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(xone::usb::vendor_request::dev_mode), 0x01); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::write), 0x02); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::power_on), 0x04); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::multi_write), 0x06); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::multi_read), 0x07); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::read_eeprom), 0x09); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::write_fce), 0x42); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::write_cfg), 0x46); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::read_cfg), 0x47); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::read_ext), 0x63); + TEST_ASSERT_EQUAL_UINT8(static_cast(xone::usb::vendor_request::write_ext), 0x66); + TEST_ASSERT_EQUAL_UINT8(static_cast(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(); +}