How to tap/hook keyboard events in OSX and record which keyboard fires each event
Asked Answered
I

2

7

I've now discovered how to hook/tap keyboard events on OS X at a low level: How to tap (hook) F7 through F12 and Power/Eject on a MacBook keyboard

Printing out the code from that answer:

// compile and run from the commandline with:
//    clang  -framework coreFoundation  -framework IOKit  ./HID.c  -o hid
//    sudo ./hid

// This code works with the IOHID library to get notified of keys.
//   Still haven't figured out how to truly intercept with
//   substitution.

#include <IOKit/hid/IOHIDValue.h>
#include <IOKit/hid/IOHIDManager.h>

void myHIDKeyboardCallback( void* context,  IOReturn result,  void* sender,  IOHIDValueRef value )
{
    IOHIDElementRef elem = IOHIDValueGetElement( value );

    if (IOHIDElementGetUsagePage(elem) != 0x07)
        return;

    uint32_t scancode = IOHIDElementGetUsage( elem );

    if (scancode < 4 || scancode > 231)
        return;

    long pressed = IOHIDValueGetIntegerValue( value );

    printf( "scancode: %d, pressed: %ld\n", scancode, pressed );
}


CFMutableDictionaryRef myCreateDeviceMatchingDictionary( UInt32 usagePage,  UInt32 usage )
{
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(
                                                            kCFAllocatorDefault, 0
                                                        , & kCFTypeDictionaryKeyCallBacks
                                                        , & kCFTypeDictionaryValueCallBacks );
    if ( ! dict )
        return NULL;

    CFNumberRef pageNumberRef = CFNumberCreate( kCFAllocatorDefault, kCFNumberIntType, & usagePage );
    if ( ! pageNumberRef ) {
        CFRelease( dict );
        return NULL;
    }

    CFDictionarySetValue( dict, CFSTR(kIOHIDDeviceUsagePageKey), pageNumberRef );
    CFRelease( pageNumberRef );

    CFNumberRef usageNumberRef = CFNumberCreate( kCFAllocatorDefault, kCFNumberIntType, & usage );

    if ( ! usageNumberRef ) {
        CFRelease( dict );
        return NULL;
    }

    CFDictionarySetValue( dict, CFSTR(kIOHIDDeviceUsageKey), usageNumberRef );
    CFRelease( usageNumberRef );

    return dict;
}


int main(void)
{
    IOHIDManagerRef hidManager = IOHIDManagerCreate( kCFAllocatorDefault, kIOHIDOptionsTypeNone );

    CFArrayRef matches;
    {
        CFMutableDictionaryRef keyboard = myCreateDeviceMatchingDictionary( 0x01, 6 );
        CFMutableDictionaryRef keypad   = myCreateDeviceMatchingDictionary( 0x01, 7 );

        CFMutableDictionaryRef matchesList[] = { keyboard, keypad };

        matches = CFArrayCreate( kCFAllocatorDefault, (const void **)matchesList, 2, NULL );
    }

    IOHIDManagerSetDeviceMatchingMultiple( hidManager, matches );

    IOHIDManagerRegisterInputValueCallback( hidManager, myHIDKeyboardCallback, NULL );

    IOHIDManagerScheduleWithRunLoop( hidManager, CFRunLoopGetMain(), kCFRunLoopDefaultMode );

    IOHIDManagerOpen( hidManager, kIOHIDOptionsTypeNone );

    CFRunLoopRun(); // spins
}

How can I (maybe adapt that code to) identify which keyboard is responsible for a particular event?

The use case is that I am planning to use an external keyboard which will be remapped, but at the same time retaining the original mapping for my inbuilt MacBook keyboard.

EDIT:
OSX HID Filter for Secondary Keyboard?
https://github.com/candera/khordr/blob/master/src/c/keygrab/hid-scratch.c
http://ianjoker.googlecode.com/svn/trunk/Joker/Joker/hid_test.cpp
http://www.cplusplusdevelop.com/72_17345226/
http://www.cocoabuilder.com/archive/cocoa/229902-which-keyboard-barcode-scanner-did-the-event-come-from.html

Ite answered 21/5, 2015 at 17:19 Comment(0)
T
2

I was working on this problem, and finally got the solution. OP's code is correct, if you'd like the product ID of the keyboard/pad, add lines to the myHIDKeyboardCallback() function:

void myHIDKeyboardCallback(void* context,  IOReturn result,  void* sender,  IOHIDValueRef value){

    IOHIDElementRef elem = IOHIDValueGetElement(value);
    if (IOHIDElementGetUsagePage(elem) != 0x07)
        return;

    IOHIDDeviceRef device = sender;
    int32_t pid = 1;
    CFNumberGetValue(IOHIDDeviceGetProperty(device, CFSTR("idProduct")), kCFNumberSInt32Type, &pid);

    uint32_t scancode = IOHIDElementGetUsage(elem);

    if (scancode < 4 || scancode > 231)
        return;

    long pressed = IOHIDValueGetIntegerValue(value);

    printf("scancode: %d, pressed: %ld, keyboardId=%d\n", scancode, pressed, pid);
}

As @pmdj said you can use IOHIDDeviceRegisterInputValueCallback(), I was having trouble with this, and found that sender argument provided keyboard product id anyways.

Triny answered 26/4, 2017 at 23:40 Comment(0)
G
1

If you register the callback on each device of interest individually with IOHIDDeviceRegisterInputValueCallback then the sender argument will be a IOHIDDeviceRef indicating the device. (instead of using IOHIDManagerRegisterInputValueCallback where the sender will be the HID manager reference)

The only downside for this is that you'll need to register for and handle notification of hotplugging events for matching devices. (register whenever a new device appears, and deregister when a device disappears)

You can get HID device references using IOHIDDeviceCreate() - this takes an io_service_t as an argument. This in turn means you need to use the standard IOKit IOService matching functions to obtain and watch your list of devices, but you do indeed get an explicit list of individual devices, which you can query for names to show to the user, etc. The key function for this is IOServiceAddMatchingNotification.

Grosso answered 21/5, 2015 at 21:14 Comment(5)
Thanks for picking up. I should have thought of logging sender. Just tried it with my code example, and it does indeed give a different value depending on which keyboard I'm using (1800442080 inbuilt, 1800440000 wireless). Can I enumerate my keyboards in such a way that I can get the associated ID for the inbuilt keyboard?Ite
Note that you're using IOHIDManagerRegisterInputValueCallback, whereas I'm suggesting IOHIDDeviceRegisterInputValueCallback - subtle but important difference. I'll update the answer with some more details on HID device enumeration.Grosso
You say that with my current (IOHIDManagerRegisterInputValueCallback) set up that sender will be the HID manager reference. But it actually report differently for each keyboard, which seems to suggest otherwise, as there is only one manager. This makes me wonder if it is really necessary to set the callback for a particular device.Ite
Strange. The documentation here: developer.apple.com/library/mac/documentation/DeviceDrivers/… indicates that the sender should be the HID manager in this case. I guess cast it and try to do something useful with it, and see what happens…Grosso
Yes there is a contradiction between the documentation and the actual behaviour! I'm hoping the documentation is in error, because it's far more useful to receive the deviceID responsible for the event. The HID Manager could probably be passed through using the inContext parameter.Ite

© 2022 - 2024 — McMap. All rights reserved.