HIDFrameworkExamples.m   [plain text]


//
//  HIDFrameworkExamples.m
//  
//
//  Created by dekom on 11/11/17.
//  Copyright © 2017 Apple. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <HID/HID.h>

static uint8_t kbdDescriptor[] = {
    0x05, 0x01,         // Usage Page (Generic Desktop)
    0x09, 0x06,         // Usage (Keyboard)
    0xA1, 0x01,         // Collection (Application)
    0x05, 0x07,         //   Usage Page (Keyboard/Keypad)
    0x19, 0xE0,         //   Usage Minimum........... (224)
    0x29, 0xE7,         //   Usage Maximum........... (231)
    0x15, 0x00,         //   Logical Minimum......... (0)
    0x25, 0x01,         //   Logical Maximum......... (1)
    0x75, 0x01,         //   Report Size............. (1)
    0x95, 0x08,         //   Report Count............ (8)
    0x81, 0x02,         //   Input...................(Data, Variable, Absolute)
    0x95, 0x01,         //   Report Count............ (1)
    0x75, 0x08,         //   Report Size............. (8)
    0x81, 0x01,         //   Input...................(Constant)
    0x05, 0x08,         //   Usage Page (LED)
    0x19, 0x01,         //   Usage Minimum........... (1)
    0x29, 0x05,         //   Usage Maximum........... (5)
    0x95, 0x05,         //   Report Count............ (5)
    0x75, 0x01,         //   Report Size............. (1)
    0x91, 0x02,         //   Output..................(Data, Variable, Absolute)
    0x95, 0x01,         //   Report Count............ (1)
    0x75, 0x03,         //   Report Size............. (3)
    0x91, 0x01,         //   Output..................(Constant)
    0x05, 0x07,         //   Usage Page (Keyboard/Keypad)
    0x19, 0x00,         //   Usage Minimum........... (0)
    0x2A, 0xFF, 0x00,   //   Usage Maximum........... (255)
    0x95, 0x05,         //   Report Count............ (5)
    0x75, 0x08,         //   Report Size............. (8)
    0x15, 0x00,         //   Logical Minimum......... (0)
    0x26, 0xFF, 0x00,   //   Logical Maximum......... (255)
    0x81, 0x00,         //   Input...................(Data, Array, Absolute)
    0xC0,               // End Collection
};

typedef struct __attribute__((packed))
{
    uint8_t  leftControl : 1;
    uint8_t  leftShift : 1;
    uint8_t  leftAlt : 1;
    uint8_t  leftGUI : 1;
    uint8_t  rightControl : 1;
    uint8_t  rightShift : 1;
    uint8_t  rightAlt : 1;
    uint8_t  rightGUI : 1;
    uint8_t  : 8;
    uint8_t  keys[5];
} kbdInputReport;

NSInteger _kbdInputReportLength = sizeof(kbdInputReport); // 7 bytes

typedef struct __attribute__((packed))
{
    uint8_t  numLock : 1;
    uint8_t  capsLock : 1;
    uint8_t  scrollLock : 1;
    uint8_t  compose : 1;
    uint8_t  kana : 1;
    uint8_t  : 3;
} kbdOutputReport;

NSInteger _kbdOutReportLength = sizeof(kbdOutputReport); // 1 bytes

dispatch_queue_t        _dispatchQueue;
NSString                *_uniqueID;
HIDUserDevice           *_userDevice;
HIDDevice               *_device;
HIDElement              *_ledElement;
HIDManager              *_manager;
HIDEventSystemClient    *_client;
HIDServiceClient        *_serviceClient;

void createUserDevice() {
    NSData *descriptor = [[NSData alloc] initWithBytes:kbdDescriptor
                                                length:sizeof(kbdDescriptor)];
    NSMutableDictionary *properties = [[NSMutableDictionary alloc] init];
    
    properties[@kIOHIDReportDescriptorKey] = descriptor;
    properties[@kIOHIDPhysicalDeviceUniqueIDKey] = _uniqueID;
    
    _userDevice = [[HIDUserDevice alloc] initWithProperties:properties];
    assert(_userDevice);
    
    [_userDevice setGetReportHandler:^IOReturn(IOHIDReportType type,
                                               uint32_t reportID,
                                               uint8_t *report,
                                               NSUInteger *reportLength) {
        kbdOutputReport outReport = { 0 };
        
        assert(type == kIOHIDReportTypeOutput);
        
        outReport.capsLock = 1;
        bcopy((const void *)&outReport, report, _kbdOutReportLength);
        
        NSData *reportData = [[NSData alloc] initWithBytes:report
                                                    length:*reportLength];
        
        NSLog(@"HIDUserDevice getReport type: %d reportID: %d report: %@",
              type, reportID, reportData);
        
        return kIOReturnSuccess;
    }];
    
    [_userDevice setSetReportHandler:^IOReturn(IOHIDReportType type,
                                               uint32_t reportID,
                                               uint8_t *report,
                                               NSUInteger reportLength) {
        kbdOutputReport *outReport;
        
        assert(type == kIOHIDReportTypeOutput);
        
        NSData *reportData = [[NSData alloc] initWithBytes:report
                                                    length:reportLength];
        
        outReport = (kbdOutputReport *)[reportData bytes];
        assert(outReport->capsLock == 1);
        
        NSLog(@"HIDUserDevice setReport type: %d reportID: %d report: %@",
              type, reportID, reportData);
        
        return kIOReturnSuccess;
    }];
    
    [_userDevice setCancelHandler:^{
        NSLog(@"HIDUserDevice cancel handler called");
        _userDevice = nil;
    }];
    
    [_userDevice setDispatchQueue:_dispatchQueue];
    
    // all handler / matching calls should happene before activate
    [_userDevice activate];
    
    // allow some time for enumeration
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, false);
}

void destroyUserDevice() {
    [_userDevice cancel];
    
    // allow some time for cancellation
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, false);
    assert(_userDevice == nil);
}

void createDevice() {
    NSDictionary *matching = nil;
    
    _device = [[HIDDevice alloc] initWithService:_userDevice.service];
    assert(_device);
    
    // Find caps lock LED element. This will be an output element type
    // so we can call set/getReport against it.
    matching = @{ @kIOHIDElementUsagePageKey : @(kHIDPage_LEDs),
                   @kIOHIDElementUsageKey : @(kHIDUsage_LED_CapsLock) };
    
    _ledElement = [[_device copyMatchingElements:matching] objectAtIndex:0];
    assert(_ledElement);
    
    // match against caps lock key. This will be an input element type
    // so we can use it with the input element handler.
    matching = @{ @kIOHIDElementUsagePageKey : @(kHIDPage_KeyboardOrKeypad),
                   @kIOHIDElementUsageKey : @(kHIDUsage_KeyboardCapsLock) };
    
    [_device setInputElementMatching:matching];
    
    [_device setInputElementHandler:^(HIDDevice *sender, HIDElement *element)
     {
         NSLog(@"HIDDevice input element sender: %@ element: %@",
               sender, element);
         
         assert(element.usagePage == kHIDPage_KeyboardOrKeypad &&
                element.usage == kHIDUsage_KeyboardCapsLock);
     }];
    
    [_device setCancelHandler:^{
        NSLog(@"HIDDevice cancel handler called");
        _device = nil;
    }];
    
    [_device setRemovalHandler:^{
        NSLog(@"HIDDevice removal handler called");
    }];
    
    [_device setInputReportHandler:^(HIDDevice *sender,
                                     IOHIDReportType type,
                                     uint32_t reportID,
                                     uint8_t *report,
                                     NSUInteger reportLength)
     {
         kbdInputReport *inReport;
         NSData *reportData = [[NSData alloc] initWithBytes:report
                                                     length:reportLength];
         inReport = (kbdInputReport *)[reportData bytes];
         
         NSLog(@"HIDDevice report sender: %@ type: %d reportID: %d report: %@",
               sender, type, reportID, reportData);
         
         assert(reportLength == 7 &&
                inReport->keys[0] == kHIDUsage_KeyboardCapsLock);
     }];
    
    [_device setDispatchQueue:_dispatchQueue];
    
    [_device open];
    
    // all handler / matching calls should happene before activate
    [_device activate];
}

void destroyDevice() {
    [_device close];
    [_device cancel];
    
    // allow some time for cancellation
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, false);
    assert(_device == nil);
}

void createManager() {
    NSDictionary *matching = nil;
    
    _manager = [[HIDManager alloc] init];
    assert(_manager);
    
    // match against caps lock key. This will be an input element type
    // so we can use it with the input element handler.
    matching = @{ @kIOHIDElementUsagePageKey : @(kHIDPage_KeyboardOrKeypad),
                   @kIOHIDElementUsageKey : @(kHIDUsage_KeyboardCapsLock) };
    
    [_manager setInputElementMatching:matching];
    
    [_manager setInputElementHandler:^(HIDDevice *sender, HIDElement *element)
     {
         NSLog(@"HIDManager input element sender: %@ element: %@",
               sender, element);
         
         assert(element.usagePage == kHIDPage_KeyboardOrKeypad &&
                element.usage == kHIDUsage_KeyboardCapsLock);
     }];
    
    matching = @{ @kIOHIDPhysicalDeviceUniqueIDKey : _uniqueID };
    [_manager setDeviceMatching:matching];
    
    [_manager setDeviceNotificationHandler:^(HIDDevice * _Nonnull device,
                                             bool added)
     {
         NSString *deviceID = nil;
         
         NSLog(@"HIDManager device notification device: %@ added: %d",
               device, added);
         
         deviceID = [_device getProperty:@kIOHIDPhysicalDeviceUniqueIDKey];
         assert(deviceID && [deviceID isEqualToString:_uniqueID]);
     }];
    
    [_manager setInputReportHandler:^(HIDDevice *sender,
                                      IOHIDReportType type,
                                      uint32_t reportID,
                                      uint8_t *report,
                                      NSUInteger reportLength)
     {
         kbdInputReport *inReport;
         NSData *reportData = [[NSData alloc] initWithBytes:report
                                                     length:reportLength];
         inReport = (kbdInputReport *)[reportData bytes];
         
         NSLog(@"HIDManager report sender: %@ type: %d reportID: %d report: %@",
               sender, type, reportID, reportData);
         
         assert(reportLength == 7 &&
                inReport->keys[0] == kHIDUsage_KeyboardCapsLock);
     }];
    
    [_manager setCancelHandler:^{
        NSLog(@"HIDManager cancel handler called");
        _manager = nil;
    }];
    
    [_manager setDispatchQueue:_dispatchQueue];
    
    [_manager open];
    
    // all handler / matching calls should happene before activate
    [_manager activate];
}

void destroyManager() {
    [_manager close];
    [_manager cancel];
    
    // allow some time for cancellation
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, false);
    assert(_manager == nil);
}

void createClient() {
    _client = [[HIDEventSystemClient alloc] initWithType:kHIDEventSystemClientTypeMonitor];
    assert(_client);
    
    NSDictionary *matching = @{ @kIOHIDPhysicalDeviceUniqueIDKey : _uniqueID };
    [_client setMatching:matching];
    
    [_client setCancelHandler:^{
        NSLog(@"Client cancel handler called");
        _client = nil;
    }];
    
    [_client setDispatchQueue:_dispatchQueue];
    
    [_client setEventHandler:^(HIDServiceClient *service, HIDEvent *event) {
        NSLog(@"Received event: %@ from service: %@", event, service);
        
        NSLog(@"type: %d uP: %ld u: %ld down: %ld", event.type,
              (long)[event getIntegerField:kIOHIDEventFieldKeyboardUsage],
              (long)[event getIntegerField:kIOHIDEventFieldKeyboardUsagePage],
              (long)[event getIntegerField:kIOHIDEventFieldKeyboardDown]);
    }];
    
    [_client setResetHandler:^{
        NSLog(@"Client reset handler called");
    }];
    
    [_client setEventFilterHandler:^BOOL(HIDServiceClient *service,
                                         HIDEvent *event) {
        bool filter = false;
        
        if (event.type == kIOHIDEventTypeNULL) {
            filter = true;
        }
        
        NSLog(@"Client filter handler called for event: %@ service: %@",
              event, service);
        
        return filter;
    }];
    
    _serviceClient = nil;
    [_client setServiceNotificationHandler:^(HIDServiceClient *service) {
        NSLog(@"Received service added notification");
        _serviceClient = service;
        
        [_serviceClient setRemovalHandler:^{
            NSLog(@"Received service removal notification");
            _serviceClient = nil;
        }];
    }];
    
    // all handler / matching calls should happene before activate
    [_client activate];
}

void destroyClient() {
    [_client cancel];
    
    // allow some time for cancellation
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, false);
    assert(_client == nil);
}

void testDevice() {
    NSLog(@"+ Test Device");
    createUserDevice();
    createDevice();
    
    assert([_device conformsTo:kHIDPage_GenericDesktop
                         usage:kHIDUsage_GD_Keyboard]);
    
    NSString *deviceID = [_device getProperty:@kIOHIDPhysicalDeviceUniqueIDKey];
    assert(deviceID && [deviceID isEqualToString:_uniqueID]);
    
    // test set/getReport
    kbdOutputReport outReport = { 0 };
    outReport.capsLock = 1;
    
    IOReturn ret = [_device setReport:kIOHIDReportTypeOutput
                             reportID:0
                               report:(uint8_t *)&outReport
                         reportLength:_kbdOutReportLength];
    assert(ret == kIOReturnSuccess);
    
    outReport.capsLock = 0;
    ret = [_device getReport:kIOHIDReportTypeOutput
                    reportID:0
                      report:(uint8_t *)&outReport
                reportLength:&_kbdOutReportLength];
    assert(ret == kIOReturnSuccess && outReport.capsLock == 1);
    
    // test set/get element
    _ledElement.integerValue = 1;
    
    // this will call setReport
    ret = [_device setElement:_ledElement];
    assert(ret == kIOReturnSuccess);
    
    _ledElement.integerValue = 0;
    ret = [_device updateElement:_ledElement options:0];
    assert(ret == kIOReturnSuccess && _ledElement.integerValue == 1);
    
    // test input reports / input elements
    kbdInputReport inReport = { 0 };
    inReport.keys[0] = kHIDUsage_KeyboardCapsLock;
    
    [_userDevice handleReport:(uint8_t *)&inReport
                 reportLength:_kbdInputReportLength];
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, false);
    
    destroyUserDevice();
    destroyDevice();
    NSLog(@"- Test Device");
}

void testManager() {
    NSLog(@"+ Test Manager");
    createUserDevice();
    createDevice();
    createManager();
    
    NSArray *devices = [_manager copyDevices];
    assert(devices && devices.count == 1);
    
    HIDDevice *device = [devices objectAtIndex:0];
    NSString *deviceID = [device getProperty:@kIOHIDPhysicalDeviceUniqueIDKey];
    assert(deviceID && [deviceID isEqualToString:_uniqueID]);
    
    // test input reports / input elements
    kbdInputReport inReport = { 0 };
    inReport.keys[0] = kHIDUsage_KeyboardCapsLock;
    
    [_userDevice handleReport:(uint8_t *)&inReport
                 reportLength:_kbdInputReportLength];
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, false);
    
    destroyUserDevice();
    destroyDevice();
    destroyManager();
    NSLog(@"- Test Manager");
}

void testTransaction() {
    NSLog(@"+ Test Transaction");
    HIDTransaction *transaction = nil;
    
    createUserDevice();
    createDevice();
    
    transaction = [[HIDTransaction alloc] initWithDevice:_device];
    assert(transaction);
    
    _ledElement.integerValue = 1;
    
    // test output transaction
    transaction.direction = kIOHIDTransactionDirectionTypeOutput;
    
    // this will call setReport
    IOReturn ret = [transaction commit:[NSArray arrayWithObject:_ledElement]];
    assert(ret == kIOReturnSuccess);
    
    // test input transaction
    transaction.direction = kIOHIDTransactionDirectionTypeInput;
    
    _ledElement.integerValue = 0;
    
    // this will call getReport
    ret = [transaction commit:[NSArray arrayWithObject:_ledElement]];
    assert(ret == kIOReturnSuccess && _ledElement.integerValue == 1);
    
    destroyDevice();
    destroyUserDevice();
    NSLog(@"- Test Transaction");
}

void testClient() {
    NSString *uniqueID;
    NSLog(@"+ Test Client");
    
    createClient();
    createUserDevice();
    
    assert(_serviceClient && _serviceClient.serviceID != 0);
    
    uniqueID = [_serviceClient copyProperty:@kIOHIDPhysicalDeviceUniqueIDKey];
    assert([uniqueID isEqualToString:_uniqueID]);
    
    assert([_serviceClient setProperty:@"TestProperty" value:@123]);
    
    NSNumber *prop = [_serviceClient copyProperty:@"TestProperty"];
    assert([prop unsignedIntValue] == 123);
    
    assert([_client setProperty:@"TestProperty" value:@123]);
    
    prop = [_client copyProperty:@"TestProperty"];
    assert([prop unsignedIntValue] == 123);
    
    NSArray *services = [_client copyServices];
    assert(services.count == 1);
    
    HIDServiceClient *kbdService = [services objectAtIndex:0];
    assert([kbdService conformsTo:kHIDPage_GenericDesktop
                            usage:kHIDUsage_GD_Keyboard]);
    
    assert([kbdService isEqualToHIDServiceClient:_serviceClient]);
    
    kbdInputReport inReport = { 0 };
    inReport.keys[0] = kHIDUsage_KeyboardCapsLock;
    
    [_userDevice handleReport:(uint8_t *)&inReport
                 reportLength:_kbdInputReportLength];
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, false);
    
    inReport.keys[0] = 0;
    
    [_userDevice handleReport:(uint8_t *)&inReport
                 reportLength:_kbdInputReportLength];
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, false);
    
    destroyUserDevice();
    assert(_serviceClient == nil);
    
    destroyClient();
    NSLog(@"- Test Client");
}

int main(int argc, const char * argv[]) {
    _dispatchQueue = dispatch_queue_create(0, DISPATCH_QUEUE_SERIAL);
    _uniqueID = [[[NSUUID alloc] init] UUIDString];
    
    testDevice();
    testManager();
    testTransaction();
    testClient();
    
    return 0;
}