//
// DeviceClient.m
// HIDRelay
//
// Created by Roberto Yepez on 8/5/14.
// Copyright (c) 2014 Roberto Yepez. All rights reserved.
//
#import "DeviceClient.h"
#import <IOKit/hid/IOHIDManager.h>
#import <IOKit/hid/IOHIDUserDevice.h>
@interface DeviceClient ()
@property (nonatomic, strong, readwrite) NSDictionary * matching;
@property (nonatomic, strong, readwrite) NSMutableDictionary * devices;
@property (nonatomic, assign, readwrite) BOOL matchingDispatched;
@property (nonatomic, assign, readwrite) NSString * interfaceName;
@end
@implementation DeviceClient
- (id)initWithMatchingDictionary:(NSDictionary *)matching withBonjour:(BOOL)bonjour withInterface:(NSString *)interface
// See comment in header.
{
self = [super initWithServerType:nil port:kHIDRelayServerPort withBonjour:bonjour];
if (self != nil) {
self.matching = matching ? matching : [NSDictionary dictionary];
self.devices = [NSMutableDictionary dictionaryWithCapacity:0];
self.interfaceName = interface;
[self startBrowserWithType:kHIDRelayServerBonjourType];
}
return self;
}
- (void)startBrowserWithType:(NSString *) type
{
if ( self.useBonjour ) {
[self.browser stop];
self.browser = nil;
[self.services removeAllObjects];
self.browser = [[NSNetServiceBrowser alloc] init];
[self.browser setDelegate:self];
[self.browser searchForServicesOfType:type inDomain:@"local"];
[self.browser scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
} else {
NSLog(@"Starting Multicast discovery\n");
self.matchingDispatched = NO;
self.mcDiscovery = [[MCDeviceDiscovery alloc] initWithInterface:self.interfaceName];
[self.mcDiscovery performHandshakeWithCompletionBlock:^(BOOL didComplete, NSString *localInterfaceName __unused, NSString *remoteIP) {
if (didComplete) {
NSInputStream * inStream;
NSOutputStream * outStream;
#if TARGET_OS_EMBEDDED
[NSStream getStreamsToHostWithName:remoteIP port:self.port inputStream:&inStream outputStream:&outStream];
#else
[NSStream getStreamsToHost:[NSHost hostWithAddress:remoteIP] port:self.port inputStream:&inStream outputStream:&outStream];
#endif
if (self.inputStream != inStream && self.outputStream != outStream){
[self closeStreams];
self.inputStream = inStream;
self.outputStream = outStream;
[self openStreams];
}
}
}];
}
}
#pragma mark - Connection management
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
#pragma unused(stream)
switch(eventCode) {
case NSStreamEventOpenCompleted: {
/* start the transmission of HID reports */
self.streamOpenCount += 1;
assert(self.streamOpenCount <= 2);
} break;
case NSStreamEventHasSpaceAvailable: {
assert(stream == self.outputStream);
if ( self.matchingDispatched == NO ) {
[self sendObjectPayload:self.matching forType:kHIDPayloadTypeMatching forDeviceID:0];
self.matchingDispatched = YES;
}
} break;
case NSStreamEventHasBytesAvailable: {
HIDPayloadHeader header = {};
NSInteger bytesRead;
assert(stream == self.inputStream);
bytesRead = [self.inputStream read:(uint8_t *)&header maxLength:sizeof(HIDPayloadHeader)];
if (bytesRead < sizeof(HIDPayloadHeader)) {
// Do nothing; we'll handle EOF and error in the
// NSStreamEventEndEncountered and NSStreamEventErrorOccurred case,
// respectively.
} else {
switch ( header.type ) {
case kHIDPayloadTypeEnumeration: {
HIDPayloadGeneric descriptorHeader = {};
bytesRead = [self.inputStream read:(uint8_t *)&descriptorHeader maxLength:sizeof(HIDPayloadGeneric)];
if ( bytesRead >= sizeof(HIDPayloadGeneric)) {
uint8_t * descriptor = malloc(descriptorHeader.length);
if ( descriptor ) {
bytesRead = [self.inputStream read:descriptor maxLength:descriptorHeader.length];
if ( bytesRead >= descriptorHeader.length) {
CFReadStreamRef stream = CFReadStreamCreateWithBytesNoCopy(kCFAllocatorDefault, descriptor, descriptorHeader.length, kCFAllocatorNull);
if ( stream ) {
if ( CFReadStreamOpen(stream) ) {
CFPropertyListFormat format = kCFPropertyListBinaryFormat_v1_0;
CFTypeRef object;
object = CFPropertyListCreateFromStream(kCFAllocatorDefault,stream,descriptorHeader.length,kCFPropertyListMutableContainersAndLeaves,&format,NULL);
if ( object ) {
if (CFGetTypeID(object) == CFDictionaryGetTypeID()) {
IOHIDUserDeviceRef device = IOHIDUserDeviceCreate(kCFAllocatorDefault, (CFDictionaryRef)(object));
if ( device ) {
NSLog(@"Device created for 0x [self.devices setObject:(__bridge id)(device) forKey:[NSNumber numberWithLongLong:header.deviceID]];
CFRelease(device);
}
}
CFRelease(object);
}
CFReadStreamClose(stream);
}
CFRelease(stream);
}
}
free(descriptor);
}
}
} break;
case kHIDPayloadTypeReport: {
IOHIDUserDeviceRef device = (__bridge IOHIDUserDeviceRef)[self.devices objectForKey: [NSNumber numberWithLongLong:header.deviceID]];
HIDPayloadReport reportHeader = {};
bytesRead = [self.inputStream read:(uint8_t *)&reportHeader maxLength:sizeof(HIDPayloadReport)];
if ( bytesRead >= sizeof(HIDPayloadReport)) {
uint8_t * report = malloc(reportHeader.length);
if ( report ) {
bytesRead = [self.inputStream read:report maxLength:reportHeader.length];
if ( bytesRead >= reportHeader.length) {
IOHIDUserDeviceHandleReport(device, report, reportHeader.length);
}
free(report);
}
}
} break;
case kHIDPayloadTypeTermination: {
NSLog(@"Device terminated for 0x [self.devices removeObjectForKey:[NSNumber numberWithLongLong:header.deviceID]];
} break;
default:
break;
}
}
} break;
case NSStreamEventErrorOccurred:
case NSStreamEventEndEncountered:
[self closeStreams];
[self.devices removeAllObjects];
if ( !self.useBonjour ) {
[self startBrowserWithType: kHIDRelayServerBonjourType];
}
break;
}
}
- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing
{
self.matchingDispatched = NO;
[super netServiceBrowser: browser didRemoveService: service moreComing:moreComing];
}
@end