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

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
HID reports issue migrating from IOKit.hid to CoreHID
 
 
Q