Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

f54da

macrumors 6502
Original poster
Dec 22, 2021
347
128
It should not actually be too difficult to get the magic trackpad 2 to work on 10.9 or lower, complete with native multitouch gestures (excluding force touch of course(. Basically, you will need 2 parts:

1) Kernel driver to communicate with MT2 and receive frames containing transducer (finger) locations and click status. This part is already well reverse-engineered thanks to Linux drivers for MT2, so they can directly re-used, with only a bit of effort needed to port the bluetooth (l2cap?) communication to an iokit driver.

2) Simulate a MT1 using the above inputs. A similar project already exists in the hackintosh community, VoodooInput, where arbitrary trackpad devices can map onto a simulated MT2 thereby gaining native multitouch support. IIUC the way that works is that they spoof the device code of the MT2, and then IOKit driver matching will automagically match the native multitouch HID driver to the nub exposed by the simulated hardware device. Then all you have to do is convert the actual device input to the magic trackpad frame format which is sent out from simulated device, and the osx multitouch stack takes care of the rest. While VoodooInput simulates a MT2, we want to simulate a MT1 so we cannot use the code exactly, but since the MT1 frame format is also decently well documented (since it has a linux driver), much of code can be reused here too.

Btw last I checked the MT1 drivers for linux were a bit incomplete in that they didn't reverse engineer some of the fields of the received frame. I think one of them was the finger id. It's actually quite easy to do this reverse engineering work from userspace since the private multitouch framework has a callback you can register to dump the raw frame and compare it against the parsed frame contents.

Here's some random code I threw together while playing around with this, in case it's useful to anyone.

So in principle it should only be a long-weekend project for someone who's decently comfortable with C. I post this here in hopes that it might inspire someone to go ahead and write the thing (it can also be a good learning experience).

Objective-C:
#include <math.h>
#include <unistd.h>
#include <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>

// IOKit Fundamentals: https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/IOKitFundamentals/Introduction/Introduction.html

// https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/IOKit/IOKit.html#//apple_ref/doc/uid/TP30000905-CH213-TPXREF104

// https://encyclopediaofdaniel.com/blog/making-the-magic-trackpad-work/

/**

the IOKit structure is as follows: VoodooI2C publishes an IOHIDDevice (i.e VoodooI2CHIDDevice). IOHIDDevice creates an IOHIDInterface nub. AppleMultitouch(Trackpad|Mouse)HIDEventDriver (which is a subclass of IOHIDEventDriver) attaches to IOHIDInterface.
AppleMultitouch(Trackpad|Mouse)HIDEventDriver is a subclass of AppleMultitouchInputHIDEventDriver which is responsible for interfacing with the mutitouch library - it instanties an AppleMultitouchDevice object and passes the report calls back and forth from the multitouch library to the hid device
AppleMultitouch(Trackpad|Mouse)HIDEventDriver and AppleMultitouchInputHIDEventDriver can be found in AppleTopCaseHIDEventDriver
AppleMultitouchDevice can be found in AppleMultitouchDriver

http://mirror.informatimago.com/next/developer.apple.com/hardwaredrivers/customusbdrivers.html

**/

typedef struct { float x,y; } mtPoint;
typedef struct { mtPoint pos,vel; } mtReadout;

typedef struct {
    int frame;
    double timestamp;
    int identifier, state, fingerid, handid;
    mtReadout normalized;
    float size;
    int zero1;
    float angle, majorAxis, minorAxis; // ellipsoid
    mtReadout mm;
    int zero2[2];
    float density;
} Finger;

typedef void *MTDeviceRef;
typedef int (*MTContactCallbackFunction)(int,Finger*,int,double,int);
typedef void (*MTPathCallbackFunction)(int, long, long,Finger*);
typedef void (*MTFullFrameCallbackFunction)(int /*device*/, uint8_t* /*data*/, int /*size?*/);

MTDeviceRef MTDeviceCreateDefault();

CFMutableArrayRef MTDeviceCreateList(void);
CFStringRef mt_CreateSavedNameForDevice(MTDeviceRef);
void MTDeviceGetTransportMethod(MTDeviceRef, int *);
int MTDeviceGetParserType(MTDeviceRef);
int MTDeviceGetParserOptions(MTDeviceRef);
void MTRegisterContactFrameCallback(MTDeviceRef, MTContactCallbackFunction);
void MTRegisterPathCallback(MTDeviceRef, MTPathCallbackFunction);
void MTRegisterFullFrameCallback(MTDeviceRef, MTFullFrameCallbackFunction, int /*0x0 in practice*/, int /*unused?*/);
void MTDeviceStart(MTDeviceRef, int); // thanks comex
void mt_HandleMultitouchFrame(int, int);

// Sample frame data:
// |28| |68| |10 28 77| |5a 90| |b| |17 ellipse major 35 ellipse minor| |c5 = size in 1/8 increment| |82 45 = /*state + figner id*/|
void fullframeCallback(int device, uint8_t* data, int size) {
    if (size == 13) {
        printf("%d ", data[12] & 0xF);
    }
//    for(int i = 0; i < size; i++)
//        printf("%x ", data[i]);
//    printf("\n");
}



void pathCallback(int device, long pathID, long state, Finger* touch) {
    //printf("Inside callback\n");
    if (state == 3 || state == 4)
        printf("%d %d %d\n", touch->identifier, touch->fingerid, touch->handid);
}

int callback(int device, Finger *data, int nFingers, double timestamp, int frame) {
    for (int i=0; i<nFingers; i++) {
        Finger *f = &data[i];
        //if (f->state == 3 || f->state == 4)
            printf("%d\n", f->fingerid);
//        printf("Frame %7d: Angle %6.2f, ellipse %6.3f x%6.3f; "
//               "position (%6.3f,%6.3f) vel (%6.3f,%6.3f) "
//               "ID %d, state %d [finger id %d / hand id %d?] size %6.3f, density %6.3f?\n",
//       f->frame,
//       f->angle * 90 / atan2(1,0),
//       f->majorAxis,
//       f->minorAxis,
//       f->normalized.pos.x,
//       f->normalized.pos.y,
//       f->normalized.vel.x,
//       f->normalized.vel.y,
//       f->identifier, f->state, f->fingerid, f->handid,
//       f->size, f->density);
    }

    return 0;
}

int main() {
    printf("%p\n", (void *)(size_t) &mt_HandleMultitouchFrame);
    NSArray* devices = CFBridgingRelease(MTDeviceCreateList());
    for(int i = 0; i < devices.count; i++)
    {
        MTDeviceRef device = (__bridge MTDeviceRef)devices[i];
        int x;
        MTDeviceGetTransportMethod(device, &x);
        printf("%d ", x);
        int y = MTDeviceGetParserType(device);
        int z = MTDeviceGetParserOptions(device);
        printf("%d %d\n", y, z);
                if (x != 4) continue;
        //NSString *yourFriendlyNSString = (__bridge NSString *) mt_CreateSavedNameForDevice(device);
        //NSLog(@"%@",yourFriendlyNSString);
        MTRegisterContactFrameCallback(device, callback);
        MTRegisterFullFrameCallback(device, fullframeCallback, 0, 0);  
//        MTRegisterPathCallback(device, pathCallback);
        MTDeviceStart(device, 0);
    }
    printf("Ctrl-C to abort\n");
    sleep(-1);
    return 0;
}
 
Last edited:
  • Like
Reactions: Wowfunhappy

f54da

macrumors 6502
Original poster
Dec 22, 2021
347
128
Was bored so I decided to just document a bit more. Here's the packet structure which I basically modified from https://github.com/acidanthera/Vood...InputSimulator/VoodooInputSimulatorDevice.hpp

And here's some rough code that just memcpys the frame into the struct so you can play around with it. Not too shabby.. I still don't know what Unk1 and Unk2 are though. Can you figure out what they might mean by playing around with it?

Note that the packet structure only works for the magic trackpad 1, the internal trackpad uses a different structure (but the contact frame callback should still work for that, since all data is already parsed).

Both Unk1 and Unk2 seem to be 7 for the top 1/3 of the trackpad. To me Unk2 seems somehow related coordinates as a rough granularity since it increases as you move your finger up and right, and decreases as you move your finger down and left. Unk1 shows a very similar pattern but is even more sensitive, like a more fine-grained version.

C++:
#include <math.h>
#include <unistd.h>
#include <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>


typedef struct { float x,y; } mtPoint;
typedef struct { mtPoint pos,vel; } mtReadout;

typedef struct {
    int frame;
    double timestamp;
    int identifier, state, fingerid, handid;
    mtReadout normalized;
    float size;
    int zero1;
    float angle, majorAxis, minorAxis; // ellipsoid
    mtReadout mm;
    int zero2[2];
    float density;
} Finger;

typedef void *MTDeviceRef;
typedef int (*MTContactCallbackFunction)(int,Finger*,int,double,int);
typedef void (*MTPathCallbackFunction)(int, long, long,Finger*);
typedef void (*MTFullFrameCallbackFunction)(int /*device*/, uint8_t* /*data*/, int /*size?*/);


MTDeviceRef MTDeviceCreateDefault();

extern "C" {
    CFMutableArrayRef MTDeviceCreateList(void);
    CFStringRef mt_CreateSavedNameForDevice(MTDeviceRef);
    void MTDeviceGetTransportMethod(MTDeviceRef, int *);
    int MTDeviceGetParserType(MTDeviceRef);
    int MTDeviceGetParserOptions(MTDeviceRef);
    void MTRegisterContactFrameCallback(MTDeviceRef, MTContactCallbackFunction);
    void MTRegisterPathCallback(MTDeviceRef, MTPathCallbackFunction);
    void MTRegisterFullFrameCallback(MTDeviceRef, MTFullFrameCallbackFunction, int /*0x0 in practice*/, int /*unused?*/);
    void MTDeviceStart(MTDeviceRef, int); // thanks comex
    void mt_HandleMultitouchFrame(int, int);
}

/**
| 77| |5a 90| |b| |17 ellipse major 35 ellipse minor| |c5 = size in 1/8 increment| |82 45 = (state + figner id)|
*/

struct __attribute__((__packed__)) MAGIC_TRACKPAD_INPUT_REPORT_FINGER {
    SInt16 X: 13;
    SInt16 Y: 13;
    UInt8 Unk1: 3;
    UInt8 Unk2: 3;
    UInt8 Touch_Major: 8;
    UInt8 Touch_Minor: 8;
    UInt8 Sz: 6;
    UInt8 TrackingId: 4;
    UInt8 Orientation: 6;
     UInt8 FingerId: 4;
    UInt8 HoverState: 4;
};

void parseTouch(int idx, uint8_t* tdata) {
//        x = (tdata[1] << 27 | tdata[0] << 19) >> 19;
//        y = -((tdata[3] << 30 | tdata[2] << 22 | tdata[1] << 14) >> 19);
//        touch_major = tdata[4];
//        touch_minor = tdata[5];
//        size = tdata[6] & 0x3f;
//        id = (tdata[7] << 2 | tdata[6] >> 6) & 0xf;
        int     orientation = (tdata[7] >> 2) - 32;
//        state = tdata[8] & TOUCH_STATE_MASK;
//        down = state != TOUCH_STATE_NONE;
    MAGIC_TRACKPAD_INPUT_REPORT_FINGER finger;
    memcpy(&finger, tdata, 9);
    printf("%d %d\n", finger.Unk1, finger.Unk2);    

}

// Sample frame data:
// |28 68 10 28| | 77| |5a 90| |b| |17 ellipse major 35 ellipse minor| |c5 = size in 1/8 increment| |82 45 = /*state + figner id*/|
// Ref: https://github.com/torvalds/linux/blob/master/drivers/hid/hid-magicmouse.c
void fullframeCallback(int device, uint8_t* data, int size) {
//    if (size == 13) {
//        printf("%d %d\n", data[4] & 0xF, data[6] & 0xF);
//    }


//    return;
    const int header_size = 4; // First 4 bytes are header
    const int finger_frame_size = 9; // 9 bytes
 
    /* Expect four bytes of prefix, and N*9 bytes of touch data. */
    if (size < header_size || ((size - header_size) % finger_frame_size) != 0)
        return;
    int nfingers = (size - header_size) / finger_frame_size;
    int trackpadclicked = data[1] & 1;
 
    for (int i = 0; i < nfingers; i++) {
        parseTouch(i, data + header_size + i * finger_frame_size);
    }
 
    /* The following brovide a device speits pcific timestamp. They
     * are unused here.
     *
     * ts = data[1] >> 6 | data[2] << 2 |
     */
 
    //printf("%d %d\n", nfingers, clicks & 1);
//    for(int i = 0; i < size; i++)
//        printf("%x ", data[i]);
}



void pathCallback(int device, long pathID, long state, Finger* touch) {
    //printf("Inside callback\n");
    if (state == 3 || state == 4)
        printf("%d %d %d\n", touch->identifier, touch->fingerid, touch->handid);
}

int callback(int device, Finger *data, int nFingers, double timestamp, int frame) {
    for (int i=0; i<nFingers; i++) {
        Finger *f = &data[i];
        //if (f->state == 3 || f->state == 4)
            printf("%d\n", f->fingerid);
//        printf("Frame %7d: Angle %6.2f, ellipse %6.3f x%6.3f; "
//               "position (%6.3f,%6.3f) vel (%6.3f,%6.3f) "
//               "ID %d, state %d [finger id %d / hand id %d?] size %6.3f, density %6.3f?\n",
//       f->frame,
//       f->angle * 90 / atan2(1,0),
//       f->majorAxis,
//       f->minorAxis,
//       f->normalized.pos.x,
//       f->normalized.pos.y,
//       f->normalized.vel.x,
//       f->normalized.vel.y,
//       f->identifier, f->state, f->fingerid, f->handid,
//       f->size, f->density);
    }

    return 0;
}

int main() {
    printf("%p\n", (void *)(size_t) &mt_HandleMultitouchFrame);
    NSArray* devices = CFBridgingRelease(MTDeviceCreateList());
    for(int i = 0; i < devices.count; i++)
    {
        MTDeviceRef device = (__bridge MTDeviceRef)devices[i];
        int x;
        MTDeviceGetTransportMethod(device, &x);
        printf("%d ", x);
        int y = MTDeviceGetParserType(device);
        int z = MTDeviceGetParserOptions(device);
        printf("%d %d\n", y, z);
                if (x != 4) continue;
        //NSString *yourFriendlyNSString = (__bridge NSString *) mt_CreateSavedNameForDevice(device);
        //NSLog(@"%@",yourFriendlyNSString);
    //    MTRegisterContactFrameCallback(device, callback);
        MTRegisterFullFrameCallback(device, fullframeCallback, 0, 0);
//        MTRegisterPathCallback(device, pathCallback);
        MTDeviceStart(device, 0);
    }
    printf("Ctrl-C to abort\n");
    sleep(-1);
    return 0;
}

Code:
/* Finger Packet
+---+---+---+---+---+---+---+---+---+
|   | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+---+---+---+---+---+---+---+---+---+
| 0 |           x: SInt13           |
+---+-----------+                   +
| 1 |           |                   |
+---+           +-------------------+
| 2 |           y: SInt13           |
+---+-----------+-----------+       +
| 3 |   Unk1    |   Unk2    |       |
|   |   UInt3   |   UInt3   |       |
+---+-----------+-----------+-------+
| 4 |       touchMajor: UInt8       |
+---+-------------------------------+
| 5 |       touchMinor: UInt8       |
+---+-------------------------------+
| 6 |  TId   |         Sz           |
+---+-------+-----------------------+
| 7 |       angl            | FId   |
+---+-----------+---+---------------+
| 8 |   Hover     |      FingId     |
|   |   UInt4     |       UInt4     |
+---+-----------+---+---------------+
*/

I think the hardest part remaining is actually figuring out how you even write an IOKit driver. I can find 0 documentation on how to do this, and the few examples I can find are for non-bluetooth devices. I know there exists an IOBluetoothHIDDriver, and there's a minimal header file which seems to indicate that it exposes a get/set report function which should be all we need to communicate with the magic trackpad. Looking at the linux source, it seems to enable multitouch frames you've got to send the trackpad a sequence of magic bytes.

The other missing part is the hid descriptors. There's a fixed sequence of bytes we need to return for newReportDescriptor() and we need to handle getReport() appropriately. I'm not sure how we can find the existing values for these.
 
Last edited:

f54da

macrumors 6502
Original poster
Dec 22, 2021
347
128
Forgot to include the header format

Code:
struct __attribute__((__packed__)) HEADER {
    UInt8 report_id: 8;
    UInt8 button_data: 6;
    UInt32 timestamp: 18;
};

Also you can use PacketLogger (included in xcode tools) to sniff the bluetooth traffic. We see the following set_feature_reports sent to the MT1 device {0xf1, 0xdb}, {0xf1, 0xdc}, {0xf0, 0x02}, etc. Basically the same ones found in this https://lkml.org/lkml/2010/8/31/324 – for the shim driver we just need to respond ok.

I also see some get feature_reports: {0x60}, {0x61}, etc. It seems that some of these are the sensor rows/cols, sensior region params, but others I'm not sure. I think we can just hard code the responses for these since they should be constant.

Finally here's a small bonus. Did you ever feel that magic trackpad palm recognition was terrible (especially when using tap to click)? Turns out that this is because it seems to always think the palm is a thumb. By combining the frame callback with an event tap, we can block these erroneous tap events. Enjoy

Code:
int lastFinger = -1;


CGEventRef eventOccurred(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon) {
    if (type == kCGEventLeftMouseDown) {
        if (lastFinger == 1 || lastFinger == 7) {
            event = NULL;
            printf("Filtered click\n");
            lastFinger = -1;
        }
    }
    return event;
}


CFMachPortRef createEventTap() {
    CGEventMask eventMask = CGEventMaskBit(kCGEventLeftMouseDown);
   
    if (!AXAPIEnabled() && !AXIsProcessTrusted()) {
        printf("axapi not enabled\n");
    }
   
    return CGEventTapCreate(kCGHIDEventTap,
                            kCGHeadInsertEventTap,
                            kCGEventTapOptionDefault,
                            eventMask,
                            eventOccurred,
                            NULL);
}

int callback(int device, Finger *data, int nFingers, double timestamp, int frame) {
    if (nFingers == 1) {
        lastFinger = data[0].fingerid;
        //printf("%d\n", lastFinger);
    }
    else {
        lastFinger = -1;
    }

    return 0;
}


MTDeviceRef device = NULL;
void initMagicTrackpad() {
    printf("Init device\n");
    NSArray* devices = (__bridge_transfer NSArray *) MTDeviceCreateList();
        for(int i = 0; i < devices.count; i++)
        {
            MTDeviceRef deviceI = (__bridge MTDeviceRef) devices[i];
            int x;
            MTDeviceGetTransportMethod(deviceI, &x);
            printf("%d ", x);
            int y = MTDeviceGetParserType(deviceI);
            int z = MTDeviceGetParserOptions(deviceI);
            printf("%d %d\n", y, z);
            if (x != 4) {
                continue;
            }
            device = (__bridge_retained MTDeviceRef) devices[i];
            MTRegisterContactFrameCallback(device, callback);
            MTDeviceStart(device, 0);
            break;
        }
    if (device == NULL) {
        printf("No BT trackpad found\n");
    }
}

void stopMagicTrackpad() {
    if (device == NULL) return;
    printf("Stop magic trackpad\n");
    MTUnregisterContactFrameCallback(device, callback); // work
    MTDeviceStop(device);
    MTDeviceRelease(device);
}


static void magicTrackpadAdded(void* refCon, io_iterator_t iterator) {
    io_service_t device;
    printf("Magic trackpad added\n");
    while ((device = IOIteratorNext(iterator))) {
        IOObjectRelease(device);
    }
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        initMagicTrackpad();
    });
}

static void magicTrackpadRemoved(void* refCon, io_iterator_t iterator) {
    io_service_t object;
        printf("Magic trackpad removed\n");
    while ((object = IOIteratorNext(iterator))) {
        IOObjectRelease(object);
    }
    stopMagicTrackpad();
}

void registerMTNotification() {
    IONotificationPortRef notificationObject = IONotificationPortCreate(kIOMasterPortDefault);
    CFRunLoopSourceRef notificationRunLoopSource = IONotificationPortGetRunLoopSource(notificationObject);
    CFRunLoopAddSource(CFRunLoopGetMain(), notificationRunLoopSource, kCFRunLoopDefaultMode);
       
    CFMutableDictionaryRef matchingDict = IOServiceNameMatching("BNBTrackpadDevice");
    matchingDict = (CFMutableDictionaryRef) CFRetain(matchingDict);

    //MagicTrackpad added notification
    io_iterator_t magicTrackpadAddedIterator;
    IOServiceAddMatchingNotification(notificationObject, kIOFirstMatchNotification, matchingDict, magicTrackpadAdded, NULL, &magicTrackpadAddedIterator);
    // Run out the iterator or notifications won't start (you can also use it to iterate the available devices).
    io_service_t d;
    while ((d = IOIteratorNext(magicTrackpadAddedIterator))) { IOObjectRelease(d); }

    //MagicTrackpad removed notification
    io_iterator_t magicTrackpadRemovedIterator;
    IOServiceAddMatchingNotification(notificationObject, kIOTerminatedNotification, matchingDict, magicTrackpadRemoved, NULL, &magicTrackpadRemovedIterator);
    while ((d = IOIteratorNext(magicTrackpadRemovedIterator))) { IOObjectRelease(d); }
   
}
   

int main() {
    initMagicTrackpad();
    registerMTNotification();
   
    CFMachPortRef tap = createEventTap();
    if (!tap) {
        return -1;
    }
    if (tap) {
        CFRunLoopSourceRef rl = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0);
        CFRunLoopAddSource(CFRunLoopGetMain(), rl, kCFRunLoopCommonModes);
        CGEventTapEnable(tap, true);
        CFRunLoopRun();
       
        printf("Tap created.\n");
    } else {
        printf("failed!\n");
        return -1;
    }
       
   
    printf("Ctrl-C to abort\n");
    sleep(-1);
    return 0;
}

Edit: For the parsed contact frame callback there's also a perhaps better described struct at https://gist.github.com/rmhsilva/61cc45587ed34707da34818a76476e11
 
Last edited:

f54da

macrumors 6502
Original poster
Dec 22, 2021
347
128
Also I forgot to document this back when I discovered it, but in case you are one of the dozens of people on mavericks AND using a Magic Trackpad 1, you might have noticed that the trackpad is less responsive at the right edge, for maybe a ~2cm zone.

This exists on the magic trackpad 2 as well as documented in https://forums.macrumors.com/thread...ght-side.2289699/?post=30878184#post-30878184 but there's a bug in 10.9 where the dead zone exists even with notification center gesture disabled. If this bothers you as much as it bothered me, then you should patch the jump at 0xf8a4 in /System/Library/Extensions/AppleMultitouchDriver.kext/Contents/PlugIns/MultitouchHID.plugin/Contents/MacOS/MultitouchHID (this is assuming latest 10.9.5, you'll have to discover the exact offset if you're on an older OS). That corresponds to a conditional in `MTSlideGesture::isBlocked` that checks if the current gesture is horizontal at the right edge (and that basic block is itself only reached for 1 finger mouse pointevents under bluetooth transport).

I could ramble more about how gestures are internally represented but I don't think it's really useful to anyone. Let me know if anyone wants to hear more about it though, and I can polish up a draft of some exploration notes of the multitouch stack that I have.
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.