objective-cmacoscocoacore-graphicscgeventtap

Cocoa Event Taps


I'm trying to use Quartz Event Services, specifically event taps, on OS X to capture simulated key presses generated by a passive infrared sensor wired to a stripped keyboard PCB. Theoretically, with event taps I can choose to swallow and ignore the key presses and simply use the keyboard input to "wake" the computer. I can also use the event tap (only one "key" is wired so the motion detector always sends a "0") to modify the input to an arbitrary key or key combination that will, in turn, invoke arbitrary functionality on the host machine. To make things more complicated, the end host machine is an old G4 Powerbook (PPC) that can't really run anything beyond OS X 10.4, and my current development machine is a much newer Intel-based iMac running 10.9 and using Xcode 5.1.1.

I found the following code from an answer here, and it seems like it would be perfect. If I create a terminal application in Cocoa, I can use the code basically as-is. I also tried creating a window-based Cocoa application in case this was necessary for adding the application to the list of Accessibility apps that can "control" the computer under the Security/Privacy Preferences (Enable Access for Assistive Devices is deprecated in Mavericks). Neither app-type worked completely. The best I have been able to achieve is to get the program to recognize key presses of modifier keys (command, option, shift, ctl, etc), but I cannot get any response from key presses of "regular" keyboard keys, and I cannot seem to enact actually changing the captured key event. Furthermore, it seems like mouse events, specifically,mouse clicks, register as KeyUp and KeyDown events - even if I change the event mask from 'AllEvents to just 'KeyUp or 'KeyDown.

Here is my slightly modified version of the original code that is adapted to try to conform to Xcode 5 conventions. As you can see the heavy lifting is in the applicationDidFinishLaunching method rather than main() as in the original. Obviously, I had to create a method declaration in the AppDelegate.h file, which is also not part of the original code.

#import "AppDelegate.h"
#import <Cocoa/Cocoa.h>
#import <ApplicationServices/ApplicationServices.h>
#include <Carbon/Carbon.h>


@implementation AppDelegate

CGEventRef myCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
    NSLog(@"In the callback");
    //0x0b is the virtual keycode for "b"
    //0x09 is the virtual keycode for "v"
    if ((type != kCGEventKeyDown) && (type != kCGEventKeyUp))
        NSLog(@"event: %@", event);
    if (CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode) == kVK_ANSI_S) {
        NSLog(@"event matched");
        CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, kVK_ANSI_1);
    }

    return event;
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSLog(@"Hello?");

    CFRunLoopSourceRef runLoopSource;

    CFMachPortRef eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, kCGEventMaskForAllEvents, myCGEventCallback, NULL);

    if (!eventTap) {
        NSLog(@"Couldn't create event tap!");
        exit(1);
    }

    runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);

    CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);

    CGEventTapEnable(eventTap, true);

    CFRunLoopRun();

    CFRelease(eventTap);
    CFRelease(runLoopSource);

    NSLog(@"goodbye");
}

@end

At this point, I'm just trying to get something working on 10.9 as a proof of concept before I attempt to backport to 10.4 compatibility. Sorry for the long post. First time asking for help here and just trying to be thorough. Thanks in advance for any and all responses!


Solution

  • OK, so my quest for a solution lead me to this post on stack overflow. Of particular interest is the following code highlighted there.

    NSDictionary *options = @{(id)kAXTrustedCheckOptionPrompt: @YES};
    BOOL accessibilityEnabled = AXIsProcessTrustedWithOptions((CFDictionaryRef)options);
    

    This is part of a new API in 10.9 Mavericks. Adding this into the mix caused the Sys Pref/Deny prompt to appear and, ultimately, Xcode itself was added to the list, not the compiled app I was working on (this never crossed my mind). Even after commenting this code out, it still works, obviously since Xcode is now in the white list and this only needs to be done once. So the proof of concept is in the bag.

    It seems to be working on 10.4 too. There were several mistakes. I'm not sure if this part was necessary, but I downloaded and installed Xcode 2.5 on the Powerbook and created and compiled the app directly on the host machine. Two issues seem to be related to constants. The kCGEventTapOptionDefault constant from the CGEventTapCreate function (seems like it could just as easily have been a BOOL instead of a uINT) is not defined in the 10.4 framework headers so unless you change it to a 0 or 1 there will be errors. The same seems to apply to the kVC virtual key code constants which are defined in the Carbon headers on newer systems. Instead, I just stuck with the hex values. Finally, drumroll please, don't make the mistake of trying to get Remote Desktop key presses to register as KeyUp or KeyDown events on the remote system. It wasn't working so finally I had the bright idea of walking over to the G4 and plugging in a keyboard (G4 is bent in half backwards and hanging on the wall so, from the front, it looks like it's just an LCD mounted on the wall). Voila. Perfect.