// USB transport (IOKit / IOUSBFamily) // 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 { // 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 { // Numeric property matching on "idVendor"/"idProduct" is unreliable on some // macOS versions (single-key matches fail while combined matches work), so // we match the IOUSBDevice class and filter VID/PID in user space. The // numeric values live in the "idVendor"/"idProduct" registry properties. struct vid_pid { std::uint16_t vid = 0; std::uint16_t pid = 0; }; auto read_vid_pid(io_service_t service) -> std::optional { CFMutableDictionaryRef props = nullptr; if (IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0) != kIOReturnSuccess || !props) return std::nullopt; auto *vid_number = static_cast(CFDictionaryGetValue(props, CFSTR("idVendor"))); auto *pid_number = static_cast(CFDictionaryGetValue(props, CFSTR("idProduct"))); SInt32 v = 0, p = 0; std::optional result; if (vid_number && pid_number && CFNumberGetValue(vid_number, kCFNumberSInt32Type, &v) && CFNumberGetValue(pid_number, kCFNumberSInt32Type, &p)) { result = { static_cast(v), static_cast(p) }; } CFRelease(props); return result; } 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; } auto is_dongle(vid_pid const &vp) -> bool { return vp.vid == vid && is_supported_pid(vp.pid); } } // namespace struct transport::read_slot { transport *owner = nullptr; std::uint8_t ep = 0; IOUSBInterfaceInterface190 **iface_ref = 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). // All interface references are double pointers: *ref is the interface // struct, and methods are called as (*ref)->Method(ref, ...). io_service_t service = 0; IOCFPlugInInterface **dev_iodev = nullptr; IOUSBDeviceInterface197 **dev_ref = nullptr; bool dev_opened = false; struct iface_conn { IOCFPlugInInterface **iodev = nullptr; IOUSBInterfaceInterface190 **iface_ref = nullptr; }; std::vector ifaces; struct pipe_ref { IOUSBInterfaceInterface190 **iface_ref = 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_ref)->AbortPipe(slot.iface_ref, 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_ref)->USBInterfaceClose(conn.iface_ref); (*conn.iface_ref)->Release(conn.iface_ref); IODestroyPlugInInterface(conn.iodev); } // Close the device connection. if (state_->dev_ref) { if (state_->dev_opened) (*state_->dev_ref)->USBDeviceClose(state_->dev_ref); (*state_->dev_ref)->Release(state_->dev_ref); } 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 (class match, filter by VID/PID). io_iterator_t iter = 0; kern_return_t kr = IOServiceGetMatchingServices(0, IOServiceMatching("IOUSBDevice"), &iter); if (kr != kIOReturnSuccess) return false; bool found = false; while (true) { io_service_t service = IOIteratorNext(iter); if (!service) break; auto vp = read_vid_pid(service); if (vp && is_dongle(*vp)) { state_->service = service; state_->pid = vp->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; } void *slot = nullptr; HRESULT hr = (*state_->dev_iodev)->QueryInterface(state_->dev_iodev, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID197), &slot); if (hr != S_OK || !slot) { xone::log_msg(log_level::error, "usb: query device interface failed"); return false; } state_->dev_ref = static_cast(slot); kr = (*state_->dev_ref)->USBDeviceOpen(state_->dev_ref); 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_ref)->CreateInterfaceAsyncEventSource(conn.iface_ref, &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, IOServiceMatching("IOUSBDevice"), 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_ref, 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; void *slot = nullptr; if ((*conn.iodev)->QueryInterface(conn.iodev, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID190), &slot) != S_OK || !slot) { IODestroyPlugInInterface(conn.iodev); return false; } conn.iface_ref = static_cast(slot); if ((*conn.iface_ref)->USBInterfaceOpen(conn.iface_ref) != kIOReturnSuccess) { (*conn.iface_ref)->Release(conn.iface_ref); 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 ((*conn.iface_ref)->GetNumEndpoints(conn.iface_ref, &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 ((*conn.iface_ref)->GetPipeProperties(conn.iface_ref, 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 = { conn.iface_ref, i }; else if (direction == kUSBIn && ep == ep_in_wlan && !state_->in_wlan_pipe.has_value()) state_->in_wlan_pipe = { conn.iface_ref, i }; else if (direction == kUSBOut && ep == ep_out && !state_->out_pipe.has_value()) state_->out_pipe = { conn.iface_ref, 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_ref)->DeviceRequest(state_->dev_ref, &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_ref)->WritePipe(pipe.iface_ref, 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_ref)->ReadPipeAsync(slot->iface_ref, 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_ref)->ReadPipeAsync(slot->iface_ref, 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; auto vp = read_vid_pid(service); if (vp && vp->vid == vid && vp->pid == 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, IOServiceMatching("IOUSBDevice"), &iter); if (kr != kIOReturnSuccess) return false; bool present = false; while (true) { io_service_t service = IOIteratorNext(iter); if (!service) break; auto vp = read_vid_pid(service); if (vp && is_dongle(*vp)) present = true; IOObjectRelease(service); } IOObjectRelease(iter); return present; } } // namespace xone::usb