HID reports issue migrating from IOKit.hid to CoreHID

I have a command line utility I wrote that has been working great up until Sequoia that reads the macro keys from a Logitech G600 gaming mouse and turns it in to custom commands. it was using the following code, checking if usage was 0x80:

IOHIDManagerRegisterInputValueCallback(
    g600HIDManager,
    { _, returnResult, callbackSender, valueRef in
        let elem = IOHIDValueGetElement(valueRef)
        let usage = IOHIDElementGetUsage(elem)
        let pressed = IOHIDValueGetIntegerValue(valueRef)

Now i'm having issues with opening the HID manager:

IOHIDManagerOpen(g600HIDManager, IOOptionBits.zero)

After changing the system security from permissive to restrictive, It's giving the error code 0xE00002E2, or no permission. I can't easily add the sandbox entitlements as this is just a simple CLI application, not a bundled app, and even after setting back to csrutil disable, i'm still getting this error.

So now i'm trying to turn it in to a bundled app and use CoreHID instead. Unfortunately I'm not getting any notifications that aren't the mouse itself. From the above code that was working before, i was looking for usage values of 0x80. I'm guessing that directly corresponds to the usage 0x80 in the HID descriptor. I am receiving notifications via

await deviceClient!.monitorNotifications(reportIDsToMonitor: [] , elementsToMonitor: [] )

which should pick up everything for the device. I know the usage i'm looking for is referenced in the device client because it's in the deviceClient.elements collection.

So is there something in CoreHID that specifically blocks Vendor specified Usage pages from being picked up by notifications?

I've also tried just requesting the elements using

let elemToMon = await deviceClient?.elements.filter({ ele in
     return ele.usage.page == 0xFF80 && ele.usage.usage == 0x80
})

let request = HIDDeviceClient.RequestElementUpdate(elements: elemToMon!)
let results = await deviceClient!.updateElements([request])

but that call errors (still trying to figure out exactly how it errors).

Any help would be appreciated, either in figuring out why i'm not getting the HID reports in question using CoreHID, or even what has changed that is causing me to not be able to use IOKit.hid anymore.

Thanks in advance!

For reference, here's the decoded HID descriptor:

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x06,        // Usage (Keyboard)
0xA1, 0x01,        // Collection (Application)
0x85, 0x01,        //   Report ID (1)
0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
0x19, 0xE0,        //   Usage Minimum (0xE0)
0x29, 0xE7,        //   Usage Maximum (0xE7)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x08,        //   Report Count (8)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x08,        //   Report Size (8)
0x95, 0x05,        //   Report Count (5)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xA4, 0x00,  //   Logical Maximum (164)
0x19, 0x00,        //   Usage Minimum (0x00)
0x2A, 0xA4, 0x00,  //   Usage Maximum (0xA4)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x06, 0x80, 0xFF,  // Usage Page (Vendor Defined 0xFF80)
0x09, 0x80,        // Usage (0x80)
0xA1, 0x01,        // Collection (Application)
0x85, 0x80,        //   Report ID (-128)
0x09, 0x80,        //   Usage (0x80)
0x75, 0x08,        //   Report Size (8)
0x95, 0x05,        //   Report Count (5)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0xF6,        //   Report ID (-10)
0x09, 0xF6,        //   Usage (0xF6)
0x75, 0x08,        //   Report Size (8)
0x95, 0x07,        //   Report Count (7)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0xF0,        //   Report ID (-16)
0x09, 0xF0,        //   Usage (0xF0)
0x95, 0x03,        //   Report Count (3)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF1,        //   Report ID (-15)
0x09, 0xF1,        //   Usage (0xF1)
0x95, 0x07,        //   Report Count (7)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF2,        //   Report ID (-14)
0x09, 0xF2,        //   Usage (0xF2)
0x95, 0x04,        //   Report Count (4)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF3,        //   Report ID (-13)
0x09, 0xF3,        //   Usage (0xF3)
0x95, 0x99,        //   Report Count (-103)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF4,        //   Report ID (-12)
0x09, 0xF4,        //   Usage (0xF4)
0x95, 0x99,        //   Report Count (-103)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF5,        //   Report ID (-11)
0x09, 0xF5,        //   Usage (0xF5)
0x95, 0x99,        //   Report Count (-103)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF6,        //   Report ID (-10)
0x09, 0xF6,        //   Usage (0xF6)
0x95, 0x07,        //   Report Count (7)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF7,        //   Report ID (-9)
0x09, 0xF7,        //   Usage (0xF7)
0x75, 0x08,        //   Report Size (8)
0x95, 0x1F,        //   Report Count (31)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
Answered by DTS Engineer in 828196022

So, I was able to spend some time on test with this morning and, while I'm not sure of exactly what went wrong, I was able to get your CoreHID to return valid data and I think I can give you the path forward. First off, in terms of replicating the problem:

  • I don't have a G600, but I do happen to have a G502. After modifying your code to match my hardware and tweaking some of your element code, I replicated the "kUSBHostReturnPipeStalled" error.

  • That error is in fact "real". Here are the errors coming from the USB stack:

2025-03-07 11:52:54.835554-0800  (IOUSBHostFamily) AppleUSBIORequest: AppleUSBIORequest::complete: device 8 (G502 HERO Gaming Mouse@02113000) endpoint 0x00: status 0xe0005000 (pipe stalled): 0 bytes transferred
2025-03-07 11:52:54.839564-0800  (IOUSBHostFamily) AppleUSBIORequest: AppleUSBIORequest::complete: device 8 (G502 HERO Gaming Mouse@02113000) endpoint 0x00: status 0xe0005000 (pipe stalled): 0 bytes transferred

  • The nature of working with hardware means that this moves the immediate issue out of our software stack (CoreHID or the USB stack). Basically, if you send a particular command to a device and the device chokes, the only choices are "fix the hardware" or "don't do that". Assuming you don't work for Logictech, that means going with "don't do that".

  • Similarly, the reason the IOKit code DID have the same failure... is that it didn't do what your CoreHID code did.

As an aside here, one thing to keep in mind if you're new to working with HID accessories is that the gap between a devices physical design and the HID interface it presents can be very... large. Case in point, the G502 is a lovely and complicated device, but it doesn't have the 956 buttons it's HIDElement list would imply. I didn't investigate further, but it's very likely that one or some combination of those elements are what caused the bus stall and your only option is "no do that".

Shifting to the path forward, the first thing to do is look* at the output of this code snippet:

let allElements = await deviceClient?.elements
print ("Report \(String(describing: allElements))")

*It's actually more helpful to stop in the debugger and print the description by right clicking, as the debugger provides some helpful indenting which "print" completely mangles.

Each element entry contains the data about the relevant report. So, for example, my hardware returned:

    ▿ 1 : CoreHID.HIDElement(client: CoreHID.HIDDeviceClient:(deviceID: 0x1007b952d,
    	primaryUsage: CoreHID.HIDUsage(page: 1, usage: 6), vendorID: 1133, productID: 49291), 
     type: CoreHID.HIDReportType.input, 
     usage: CoreHID.HIDUsage(page: 7, usage: 225), reportID: CoreHID.HIDReportID(1))

When I fed those values back into your (modified) code, everthing worked fine. Here's my output:

DeviceName: G502 HERO Gaming Mouse
dispatchGetReportRequest succeeded: 9 bytes
Report: <Optional([CoreHID.HIDElement.Value(element:...>

And my code:

//
//  HIDExample_CoreHID.swift
//  HIDExample_CoreHID
//
//  Modified by Kevin Elliott on 3/7/25.

import Foundation
import CoreHID

let DeviceVendorId: UInt32 = 0x046D // Logitech
//let DeviceProductId: UInt32 = 0xC24A // Gaming Mouse G600
let DeviceProductId: UInt32 = 0xC08B // Gaming Mouse G502

var deviceClient: HIDDeviceClient? = nil

@main struct HIDExample_CoreHID {

    static func main() async throws {
            let deviceManager = HIDDeviceManager()
            let matchingCriteria : HIDDeviceManager.DeviceMatchingCriteria = .init(primaryUsage: HIDUsage(page: 0x01, usage: 0x06) , deviceUsages: nil, vendorID: DeviceVendorId, productID: DeviceProductId)
            let reportId = HIDReportID(rawValue: 0x80)
            
            var deviceRef: HIDDeviceClient.DeviceReference? = nil
            
            for try await notification in await deviceManager.monitorNotifications(matchingCriteria: [matchingCriteria]) {
                switch notification {
                case .deviceMatched(let dr):
                    if deviceRef != nil {continue}
                    deviceRef = dr
                    deviceClient = HIDDeviceClient(deviceReference: deviceRef!)
                    let deviceName = await deviceClient!.product!
                    print("DeviceName: \(deviceName)")
                    

                    #if true
                    let allElements = await deviceClient?.elements
                    let count = allElements!.count
                    print ("Report \(count) <\(String(describing: allElements))>")
                    #endif
                    
                    let elemToMon = await deviceClient?.elements.filter({ ele in
                        return  ele.usage.page == 7 && ele.usage.usage == 225
                    })
                    assert(!(elemToMon!.isEmpty)) // make sure that this is the correct device.
                    
                    do {
                        let report = try await deviceClient!.dispatchGetReportRequest(type: .input, id: HIDReportID(rawValue: 1), timeout: Duration.seconds(4))
                        assert(!report.isEmpty)
                        NSLog("dispatchGetReportRequest succeeded: %@\n", String(describing: report))
                    }
                    catch {
                        NSLog("dispatchGetReportRequest failed: %@\n", String(describing: error))
                    }
                                        
                    do {
                        let request = HIDDeviceClient.RequestElementUpdate(elements: elemToMon!)
                        let results = await deviceClient!.updateElements([request])
                        let rval = try results[request]?.get() // <-----  error is thrown here
                        print ("Report \(String(describing: rval))")
                    }
                    catch {
                        NSLog("RequestElementUpdate failed: %@\n", String(describing: error))
                    }

                default:
                    continue
                    
                }
            }
        print("Finished")
    }
}

As a quick side note, using NSLog (instead of print) can be EXTREMELY helpful in this sort of code because it includes a timestamp (never a bad thing) and, more importantly, it also prints to the system console. That lets you use your app's own logging to find the broader activity it's generating like, for example, the USB errors I showed above.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

More troubleshooting: running the following line:

let report = try await deviceClient!.dispatchGetReportRequest(type: .input, id: HIDReportID(rawValue: 0x80))

gives me an error of type CoreHID.HIDDeviceError.unknown with error code of 0xE0005000. I cannot find at all what that specific code corresponds to, and the error description is the generic unknown error text.

OK, broke it down with two simple examples. Note that on first run you need to enable Input Monitoring for the application under Privacy settings. For CoreHID, i tried two different ways, both error with the same result. For IOKit, it works just fine. Also attached is the full Device report descriptors for the mouse.

046D C24A: Logitech - Gaming Mouse G600
DESCRIPTOR:
  05  01  09  06  a1  01  85  01  05  07  19  e0  29  e7  15  00
  25  01  75  01  95  08  81  02  75  08  95  05  15  00  26  a4
  00  19  00  2a  a4  00  81  00  c0  06  80  ff  09  80  a1  01
  85  80  09  80  75  08  95  05  81  02  85  f6  09  f6  75  08
  95  07  81  02  85  f0  09  f0  95  03  b1  02  85  f1  09  f1
  95  07  b1  02  85  f2  09  f2  95  04  b1  02  85  f3  09  f3
  95  99  b1  02  85  f4  09  f4  95  99  b1  02  85  f5  09  f5
  95  99  b1  02  85  f6  09  f6  95  07  b1  02  85  f7  09  f7
  75  08  95  1f  81  02  c0
  (135 bytes)

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x06,        // Usage (Keyboard)
0xA1, 0x01,        // Collection (Application)
0x85, 0x01,        //   Report ID (1)
0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
0x19, 0xE0,        //   Usage Minimum (0xE0)
0x29, 0xE7,        //   Usage Maximum (0xE7)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x08,        //   Report Count (8)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x08,        //   Report Size (8)
0x95, 0x05,        //   Report Count (5)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xA4, 0x00,  //   Logical Maximum (164)
0x19, 0x00,        //   Usage Minimum (0x00)
0x2A, 0xA4, 0x00,  //   Usage Maximum (0xA4)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x06, 0x80, 0xFF,  // Usage Page (Vendor Defined 0xFF80)
0x09, 0x80,        // Usage (0x80)
0xA1, 0x01,        // Collection (Application)
0x85, 0x80,        //   Report ID (-128)       *************** THIS IS THE ONE WE CARE ABOUT ***************
0x09, 0x80,        //   Usage (0x80)
0x75, 0x08,        //   Report Size (8)
0x95, 0x05,        //   Report Count (5)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0xF6,        //   Report ID (-10)
0x09, 0xF6,        //   Usage (0xF6)
0x75, 0x08,        //   Report Size (8)
0x95, 0x07,        //   Report Count (7)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0xF0,        //   Report ID (-16)
0x09, 0xF0,        //   Usage (0xF0)
0x95, 0x03,        //   Report Count (3)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF1,        //   Report ID (-15)
0x09, 0xF1,        //   Usage (0xF1)
0x95, 0x07,        //   Report Count (7)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF2,        //   Report ID (-14)
0x09, 0xF2,        //   Usage (0xF2)
0x95, 0x04,        //   Report Count (4)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF3,        //   Report ID (-13)
0x09, 0xF3,        //   Usage (0xF3)
0x95, 0x99,        //   Report Count (-103)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF4,        //   Report ID (-12)
0x09, 0xF4,        //   Usage (0xF4)
0x95, 0x99,        //   Report Count (-103)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF5,        //   Report ID (-11)
0x09, 0xF5,        //   Usage (0xF5)
0x95, 0x99,        //   Report Count (-103)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF6,        //   Report ID (-10)
0x09, 0xF6,        //   Usage (0xF6)
0x95, 0x07,        //   Report Count (7)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0xF7,        //   Report ID (-9)
0x09, 0xF7,        //   Usage (0xF7)
0x75, 0x08,        //   Report Size (8)
0x95, 0x1F,        //   Report Count (31)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection

// 135 bytes




046D C24A: Logitech - Gaming Mouse G600
DESCRIPTOR:
  05  01  09  02  a1  01  09  01  a1  00  05  09  19  01  29  10
  15  00  25  01  75  01  95  10  81  02  05  01  09  30  09  31
  16  01  80  26  ff  7f  75  10  95  02  81  06  09  38  75  08
  95  01  15  81  25  7f  81  06  05  0c  0a  38  02  95  01  81
  06  c0  c0
  (67 bytes)

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x02,        // Usage (Mouse)
0xA1, 0x01,        // Collection (Application)
0x09, 0x01,        //   Usage (Pointer)
0xA1, 0x00,        //   Collection (Physical)
0x05, 0x09,        //     Usage Page (Button)
0x19, 0x01,        //     Usage Minimum (0x01)
0x29, 0x10,        //     Usage Maximum (0x10)
0x15, 0x00,        //     Logical Minimum (0)
0x25, 0x01,        //     Logical Maximum (1)
0x75, 0x01,        //     Report Size (1)
0x95, 0x10,        //     Report Count (16)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01,        //     Usage Page (Generic Desktop Ctrls)
0x09, 0x30,        //     Usage (X)
0x09, 0x31,        //     Usage (Y)
0x16, 0x01, 0x80,  //     Logical Minimum (-32767)
0x26, 0xFF, 0x7F,  //     Logical Maximum (32767)
0x75, 0x10,        //     Report Size (16)
0x95, 0x02,        //     Report Count (2)
0x81, 0x06,        //     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0x09, 0x38,        //     Usage (Wheel)
0x75, 0x08,        //     Report Size (8)
0x95, 0x01,        //     Report Count (1)
0x15, 0x81,        //     Logical Minimum (-127)
0x25, 0x7F,        //     Logical Maximum (127)
0x81, 0x06,        //     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x0C,        //     Usage Page (Consumer)
0x0A, 0x38, 0x02,  //     Usage (AC Pan)
0x95, 0x01,        //     Report Count (1)
0x81, 0x06,        //     Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              //   End Collection
0xC0,              // End Collection

// 67 bytes

//
//  HIDExample_CoreHID.swift
//  HIDExample_CoreHID
//
//  Created by Edgars Klepers on 2/28/25.
//
//   This example demonstrates CoreHID throwing an unknown error when trying to get a specific report.
//   Required Hardware:  A Logitech G600 Gaming Mouse (https://support.logi.com/hc/en-ca/articles/360023465173-Logitech-G600-MMO-Gaming-Mouse-Technical-Specifications)
//   The purpose is to detect the macro buttons labeled G9-G20 on the side.


import Foundation
import CoreHID

@main struct HIDExample_CoreHID {

    static func main() async throws {

        let DeviceVendorId: UInt32 = 0x046D // Logitech
        let DeviceProductId: UInt32 = 0xC24A // Gaming Mouse G600

        let deviceUsages =  [
            HIDUsage(page: 0xFF80, usage: 0x80)
        ]
        let deviceManager = HIDDeviceManager()
        let matchingCriteria : HIDDeviceManager.DeviceMatchingCriteria = .init(primaryUsage: HIDUsage(page: 0x01, usage: 0x06) , deviceUsages:deviceUsages, vendorID: DeviceVendorId, productID: DeviceProductId)
        let reportId = HIDReportID(rawValue: 0x80)

        var deviceRef: HIDDeviceClient.DeviceReference? = nil

        for try await notification in await deviceManager.monitorNotifications(matchingCriteria: [matchingCriteria]) {
            switch notification {
            case .deviceMatched(let dr):
                if deviceRef != nil {continue}
                deviceRef = dr
                let deviceClient = HIDDeviceClient(deviceReference: deviceRef!)
                let deviceName = await deviceClient!.product!
                print("DeviceName: \(deviceName)")
                let elemToMon = await deviceClient?.elements.filter({ ele in
                    return  ele.usage.page == 0xFF80 && ele.usage.usage == 0x80
                })
                assert(!(elemToMon!.isEmpty)) // make sure that this is the correct device.


                // ***** This errors with CoreHid.HIDDeviceError.unknown(0xE0005000) *****
                let report = try await deviceClient!.dispatchGetReportRequest(type: .input, id: reportId)
                assert(!report.isEmpty)


                // ***** This also errors with CoreHid.HIDDeviceError.unknown(0xE0005000) *****
                let request = HIDDeviceClient.RequestElementUpdate(elements: elemToMon!)
                let results = await deviceClient!.updateElements([request])
                let rval = try results[request]?.get() // <-----  error is thrown here
                print ("Report \(String(describing: rval))")

            default:
                continue

            }
        }

    }
}

//  HIDExample_IOKit.swift
//  HIDExample_IOKit
//
//  Created by Edgars Klepers on 2/28/25.
//
//   This example demonstrates a method that works fine using IOKit.hid.
//   Required Hardware:  A Logitech G600 Gaming Mouse (https://support.logi.com/hc/en-ca/articles/360023465173-Logitech-G600-MMO-Gaming-Mouse-Technical-Specifications)
//   The purpose is to detect the macro buttons labeled G9-G20 on the side.

import Foundation
import IOKit.hid

let DeviceVendorId: UInt32 = 0x046D // Logitech
let DeviceProductId: UInt32 = 0xC24A // Gaming Mouse G600


@main struct HIDExample_IOKit {

    static func main() throws {
        let matchDict = [kIOHIDVendorIDKey: DeviceVendorId, kIOHIDProductIDKey: DeviceProductId] as Dictionary
        let g600HIDManager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits.zero)
        assert(CFGetTypeID(g600HIDManager) == IOHIDManagerGetTypeID())

        IOHIDManagerSetDeviceMatching(g600HIDManager,matchDict as! CFMutableDictionary)

        let openHidManagerResult = IOHIDManagerOpen(g600HIDManager, IOOptionBits.zero)
        if (openHidManagerResult != kIOReturnSuccess) {
            print("Error loading IOHIDManagerOpen: \(String(format:"0x%X", openHidManagerResult))")
        }
        assert(openHidManagerResult == kIOReturnSuccess)

        IOHIDManagerScheduleWithRunLoop(g600HIDManager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)
        IOHIDManagerRegisterInputValueCallback(g600HIDManager,{ _, returnResult, callbackSender, valueRef in

            let elem = IOHIDValueGetElement(valueRef)
            let usage = IOHIDElementGetUsage(elem)
            let value = IOHIDValueGetIntegerValue(valueRef)

            if (usage == 0x80) {
                print("Success!, value: \(String(format:"0x%X", value))") // this is successful.
                return
            }
        }, nil)

        CFRunLoopRun()
    }
}

And I can confirm that input reports are being received just fine through a Windows app using the code attached. So there is something in CoreHID that is giving an error when trying to get this report. (using dispatchGetReportRequest in CoreHID just doesn't get any reports).

using Windows.Devices.Enumeration;
using Windows.Devices.HumanInterfaceDevice;
using Windows.Storage;
using Windows.Storage.Streams;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;


namespace HIDdeviceTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            EnumerateHidDevices();

        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }


        // Find HID devices.
        private async void EnumerateHidDevices()
        {
            // Microsoft Input Configuration Device.
            ushort vendorId = 0x046D;
            ushort productId = 0xC24A;
            ushort usagePage = 0xFF80;
            ushort usageId = 0x0080;

            // Create the selector.
            string selector =
                HidDevice.GetDeviceSelector(usagePage, usageId, vendorId, productId);

            // Enumerate devices using the selector.
            var devices = await DeviceInformation.FindAllAsync(selector);

            if (devices.Any())
            {
                // At this point the device is available to communicate with
                // So we can send/receive HID reports from it or 
                // query it for control descriptions.
                System.Diagnostics.Debug.WriteLine("HID devices found: " + devices.Count);

                // Open the target HID device.
                HidDevice device =
                    await HidDevice.FromIdAsync(devices.ElementAt(0).Id,
                    FileAccessMode.Read);

                if (device != null)
                {
                    System.Diagnostics.Debug.WriteLine("Device not null, adding async calls");

                    // Input reports contain data from the device.
                    device.InputReportReceived += async (sender, args) =>
                    {
                        HidInputReport inputReport = args.Report;
                        IBuffer buffer = inputReport.Data;

                        // Create a DispatchedHandler as we are interracting with the UI directly and the
                        // thread that this function is running on might not be the UI thread; 
                        // if a non-UI thread modifies the UI, an exception is thrown.


                        StringBuilder data = new StringBuilder();
                        foreach (var d in buffer.ToArray())
                        {
                            data.Append(d.ToString());
                        }

                        System.Diagnostics.Debug.WriteLine("HID Input Report: " + inputReport.ToString() +
                        "\nTotal number of bytes received: " + buffer.Length.ToString() + 
                        "\n Data: 0x" + data.ToString());
                    };
                    System.Diagnostics.Debug.WriteLine("Added input report received");
                } else
                {
                    System.Diagnostics.Debug.WriteLine("NO DEVICES TO LOOK AT");
                }

            }
            else
            {
                // There were no HID devices that met the selector criteria.
                Console.Error.WriteLine("HID device not found");
            }
        }

    }
}

HID devices found: 1
Device not null, adding async calls
Added input report received
HID Input Report: Windows.Devices.HumanInterfaceDevice.HidInputReport
Total number of bytes received: 32
 Data: 0x12801001600000000000000000000000000
HID Input Report: Windows.Devices.HumanInterfaceDevice.HidInputReport
Total number of bytes received: 32
 Data: 0x12800001600000000000000000000000000
HID Input Report: Windows.Devices.HumanInterfaceDevice.HidInputReport
Total number of bytes received: 32
 Data: 0x12802001600000000000000000000000000
HID Input Report: Windows.Devices.HumanInterfaceDevice.HidInputReport
Total number of bytes received: 32
 Data: 0x12800001600000000000000000000000000

gives me an error of type CoreHID.HIDDeviceError.unknown with error code of 0xE0005000.

For future reference, the kernel uses a structured error code system which makes it easier to divide the error code range between components, but harder to lookup specific error code. The error code "space" is defined by a whole series of nested macro's, so you won't actually fine "0xE0005000" or even "0x5000".

In case, the key points to note:

#define kIOReturnExclusiveAccess iokit_common_err(0x2c5) // exclusive access and

  • The fourth digit is used for the subsystem ("0x5"-> IOUSBHost), but the crucial point is that a non-zero value there means that the error came from a specific family, not the common pool.

Pulling all of that together, in this case, the error is "kUSBHostReturnPipeStalled" defined "IOUSBHostFamilyDefinitions.h" from IOKit.framework.

#define kUSBHostReturnPipeStalled                   iokit_usbhost_err(0x0)  // 0xe0005000  Pipe has issued a STALL handshake.  Use clearStall to clear this condition.
 

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Ah, the 4th digit information is what I was missing. Thank you for that information; that at least gives me something more to look into. I'm trying to solve that 0xE0005000 issues, as I'm trying to clean up the code and switch it to CoreHID. But as I have code that works using IOKit.hid and windows, and as the configuration for CoreHID looks pretty straight forward with minimal options or lower level access, I really don't have an Idea of a direction to go. I can get standard mouse events (movement and standard mouse buttons) just fine with the HIDDeviceClient.monitorNotifications() method. When asking for this usage, it doesn't get any events at all (no errors that i can see, just no events).

Ah, the 4th digit information is what I was missing. Thank you for that information; that at least gives me something more to look into. But as I have code that works using IOKit.hid

Looking at your IOKit code, you're not actually retrieving the descriptor. Have you tried just registering notifications against the elements the device returned? I think that's what your IOHIDManager code is doing.

One the to understand here is that CoreHID and IOHIDManager are both interacting with exactly the same underlying USB object, so when you're seeing different behavior you're either not doing the "same" thing or your doing it when the device is a fundamentally different state.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

My line for notifications is:

for try await notification in await deviceClient!.monitorNotifications(reportIDsToMonitor: [] , elementsToMonitor: [] ) { ...

which sound like it should grab both reports and elements. I get NO reports from that usage. My windows test code looks to be getting an input report, making me think that i SHOULD be getting a report from CoreHID as well. And in my CoreHID test code, i also have a second attempt using HIDDeviceClient.RequestElementUpdate requesting that Element, and that also returns the kUSBHostReturnPipeStalled error.

And i can confirm, IOKit version also works with the HID reports. Attached code works without issue, output also attached.

//  HIDExample_IOKit.swift
//  HIDExample_IOKit
//
//  Created by Edgars Klepers on 2/28/25.
//
//   This example demonstrates a method that works fine using IOKit.hid.
//   Required Hardware:  A Logitech G600 Gaming Mouse (https://support.logi.com/hc/en-ca/articles/360023465173-Logitech-G600-MMO-Gaming-Mouse-Technical-Specifications)
//   The purpose is to detect the macro buttons labeled G9-G20 on the side.

import Foundation
import IOKit.hid

let DeviceVendorId: UInt32 = 0x046D // Logitech
let DeviceProductId: UInt32 = 0xC24A // Gaming Mouse G600


@main struct HIDExample_IOKit {

    static func main() throws {
        let matchDict = [kIOHIDVendorIDKey: DeviceVendorId, kIOHIDProductIDKey: DeviceProductId] as Dictionary
        let g600HIDManager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits.zero)
        assert(CFGetTypeID(g600HIDManager) == IOHIDManagerGetTypeID())

        IOHIDManagerSetDeviceMatching(g600HIDManager,matchDict as! CFMutableDictionary)

        let openHidManagerResult = IOHIDManagerOpen(g600HIDManager, IOOptionBits.zero)
        if (openHidManagerResult != kIOReturnSuccess) {
            print("Error loading IOHIDManagerOpen: \(String(format:"0x%X", openHidManagerResult))")
        }
        assert(openHidManagerResult == kIOReturnSuccess)

        IOHIDManagerScheduleWithRunLoop(g600HIDManager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)

        IOHIDManagerRegisterInputReportWithTimeStampCallback(g600HIDManager,{ _, returnResult, callbackSender, type, reportId, reportData, reportLength, timestamp in

            let value = Data(bytes: reportData, count: Int(reportLength))
            if (reportId == 0x80) {
            print("Success! Value: \(value.map { String(format: "%02x", $0)} )")
                return
            }
        }, nil)


        CFRunLoopRun()
    }
}

Success! Value: ["80", "00", "01", "00", "00", "10"]
Success! Value: ["80", "00", "00", "00", "00", "10"]
Success! Value: ["80", "00", "02", "00", "00", "10"]
Success! Value: ["80", "00", "12", "00", "00", "10"]
Success! Value: ["80", "00", "1a", "00", "00", "10"]
Success! Value: ["80", "00", "9a", "00", "00", "10"]
Success! Value: ["80", "00", "da", "00", "00", "10"]

I cannot see anything that is saying this shouldn't work. My guess is this is bug in CoreHID, most likely because the UsagePage in question is two bytes long instead of the usual one byte:

0x06, 0x80, 0xFF,  // Usage Page (Vendor Defined 0xFF80)

If you or anyone else has other ideas to try, I would definitely appreciate them.

Accepted Answer

So, I was able to spend some time on test with this morning and, while I'm not sure of exactly what went wrong, I was able to get your CoreHID to return valid data and I think I can give you the path forward. First off, in terms of replicating the problem:

  • I don't have a G600, but I do happen to have a G502. After modifying your code to match my hardware and tweaking some of your element code, I replicated the "kUSBHostReturnPipeStalled" error.

  • That error is in fact "real". Here are the errors coming from the USB stack:

2025-03-07 11:52:54.835554-0800  (IOUSBHostFamily) AppleUSBIORequest: AppleUSBIORequest::complete: device 8 (G502 HERO Gaming Mouse@02113000) endpoint 0x00: status 0xe0005000 (pipe stalled): 0 bytes transferred
2025-03-07 11:52:54.839564-0800  (IOUSBHostFamily) AppleUSBIORequest: AppleUSBIORequest::complete: device 8 (G502 HERO Gaming Mouse@02113000) endpoint 0x00: status 0xe0005000 (pipe stalled): 0 bytes transferred

  • The nature of working with hardware means that this moves the immediate issue out of our software stack (CoreHID or the USB stack). Basically, if you send a particular command to a device and the device chokes, the only choices are "fix the hardware" or "don't do that". Assuming you don't work for Logictech, that means going with "don't do that".

  • Similarly, the reason the IOKit code DID have the same failure... is that it didn't do what your CoreHID code did.

As an aside here, one thing to keep in mind if you're new to working with HID accessories is that the gap between a devices physical design and the HID interface it presents can be very... large. Case in point, the G502 is a lovely and complicated device, but it doesn't have the 956 buttons it's HIDElement list would imply. I didn't investigate further, but it's very likely that one or some combination of those elements are what caused the bus stall and your only option is "no do that".

Shifting to the path forward, the first thing to do is look* at the output of this code snippet:

let allElements = await deviceClient?.elements
print ("Report \(String(describing: allElements))")

*It's actually more helpful to stop in the debugger and print the description by right clicking, as the debugger provides some helpful indenting which "print" completely mangles.

Each element entry contains the data about the relevant report. So, for example, my hardware returned:

    ▿ 1 : CoreHID.HIDElement(client: CoreHID.HIDDeviceClient:(deviceID: 0x1007b952d,
    	primaryUsage: CoreHID.HIDUsage(page: 1, usage: 6), vendorID: 1133, productID: 49291), 
     type: CoreHID.HIDReportType.input, 
     usage: CoreHID.HIDUsage(page: 7, usage: 225), reportID: CoreHID.HIDReportID(1))

When I fed those values back into your (modified) code, everthing worked fine. Here's my output:

DeviceName: G502 HERO Gaming Mouse
dispatchGetReportRequest succeeded: 9 bytes
Report: <Optional([CoreHID.HIDElement.Value(element:...>

And my code:

//
//  HIDExample_CoreHID.swift
//  HIDExample_CoreHID
//
//  Modified by Kevin Elliott on 3/7/25.

import Foundation
import CoreHID

let DeviceVendorId: UInt32 = 0x046D // Logitech
//let DeviceProductId: UInt32 = 0xC24A // Gaming Mouse G600
let DeviceProductId: UInt32 = 0xC08B // Gaming Mouse G502

var deviceClient: HIDDeviceClient? = nil

@main struct HIDExample_CoreHID {

    static func main() async throws {
            let deviceManager = HIDDeviceManager()
            let matchingCriteria : HIDDeviceManager.DeviceMatchingCriteria = .init(primaryUsage: HIDUsage(page: 0x01, usage: 0x06) , deviceUsages: nil, vendorID: DeviceVendorId, productID: DeviceProductId)
            let reportId = HIDReportID(rawValue: 0x80)
            
            var deviceRef: HIDDeviceClient.DeviceReference? = nil
            
            for try await notification in await deviceManager.monitorNotifications(matchingCriteria: [matchingCriteria]) {
                switch notification {
                case .deviceMatched(let dr):
                    if deviceRef != nil {continue}
                    deviceRef = dr
                    deviceClient = HIDDeviceClient(deviceReference: deviceRef!)
                    let deviceName = await deviceClient!.product!
                    print("DeviceName: \(deviceName)")
                    

                    #if true
                    let allElements = await deviceClient?.elements
                    let count = allElements!.count
                    print ("Report \(count) <\(String(describing: allElements))>")
                    #endif
                    
                    let elemToMon = await deviceClient?.elements.filter({ ele in
                        return  ele.usage.page == 7 && ele.usage.usage == 225
                    })
                    assert(!(elemToMon!.isEmpty)) // make sure that this is the correct device.
                    
                    do {
                        let report = try await deviceClient!.dispatchGetReportRequest(type: .input, id: HIDReportID(rawValue: 1), timeout: Duration.seconds(4))
                        assert(!report.isEmpty)
                        NSLog("dispatchGetReportRequest succeeded: %@\n", String(describing: report))
                    }
                    catch {
                        NSLog("dispatchGetReportRequest failed: %@\n", String(describing: error))
                    }
                                        
                    do {
                        let request = HIDDeviceClient.RequestElementUpdate(elements: elemToMon!)
                        let results = await deviceClient!.updateElements([request])
                        let rval = try results[request]?.get() // <-----  error is thrown here
                        print ("Report \(String(describing: rval))")
                    }
                    catch {
                        NSLog("RequestElementUpdate failed: %@\n", String(describing: error))
                    }

                default:
                    continue
                    
                }
            }
        print("Finished")
    }
}

As a quick side note, using NSLog (instead of print) can be EXTREMELY helpful in this sort of code because it includes a timestamp (never a bad thing) and, more importantly, it also prints to the system console. That lets you use your app's own logging to find the broader activity it's generating like, for example, the USB errors I showed above.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I'll look deeper when I get home, but i do see one key difference; the buttons i'm trying to capture on the G600 is a 3x4 grid of 'macro' buttons use many times in MMORPGs, labels G9 to G20. I'm specifically un-mapping them from any standard inputs. As a result, the reports are not coming from the standard page/usage as in your code: HIDUsage(page: 0x01, usage: 0x06) I can get HID reports from the mouse and keyboard Input reports without issue with my mouse.

So where there are no standard mouse and keyboard reports coming from these buttons now, the mouse still does send a HID report under the 'Vendor Defined' Usage Page of 0xFF80(note, that unlike your standard keyboard and mouse usage pages, this is a TWO byte value), and the usage of 0x80 (still one byte). Looking at your mouse, you might want to see if you have a similar entry in your HID report descriptor, as it does have extra buttons on the side 'G4' and 'G5'. My latest post above is a modified IOKit code that looks to use the standard input reports, showing they are in fact being generated

Now that I"m at the computer with the mouse, the primary HIDUsage from my last comment isn't the issue, that needs to be 0x01, 0x06 because the second usage page is under that primary usage. The issue is when I change your code fromdispatchGetReportRequest(type: .input, id: HIDReportID(rawValue: 1) to a rawValue: 0x80, then i get the following error:

dispatchGetReportRequest failed: unknown(-536850432)

And if you look at the last IOKit code, i did update that to use

IOHIDManagerRegisterInputReportWithTimeStampCallback

and it gets the report just fine.

And forgot to attach the full output:

DeviceName: Gaming Mouse G600
Report 248 
dispatchGetReportRequest failed: unknown(-536850432)
Report Optional([CoreHID.HIDElement.Value(element: CoreHID.HIDElement(client: CoreHID.HIDDeviceClient:(deviceID: 0x1008f1c96, primaryUsage: CoreHID.HIDUsage(page: 1, usage: 6), vendorID: 1133, productID: 49738), type: CoreHID.HIDReportType.input, usage: CoreHID.HIDUsage(page: 7, usage: 225), reportID: CoreHID.HIDReportID(1)), timestamp: Instant(_value: 517820.839607083 seconds), bytes:[00])])

OK, I don't know what all changed, but I went back from monitoring for the report event to the element update and it's working now. I'm still wondering why the report isn't picking up, but i can read the element and get the information I need now.

Thanks again for your help, Kevin! You definitely helped fill a couple holes in knowledge for me in this.

HID reports issue migrating from IOKit.hid to CoreHID
 
 
Q