diff --git a/OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.h b/OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.h index 056eecb7f..114958a2e 100644 --- a/OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.h +++ b/OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.h @@ -8,8 +8,72 @@ #import -@interface CSJoystickManager : NSObject - -- (instancetype)init; - +/*! + Models a single joystick button. + Buttons have an index and are either currently pressed, or not. +*/ +@interface CSJoystickButton: NSObject +@property(nonatomic, readonly) NSInteger index; +@property(nonatomic, readonly) bool isPressed; +@end + +typedef NS_ENUM(NSInteger, CSJoystickAxisType) { + CSJoystickAxisTypeX, + CSJoystickAxisTypeY, + CSJoystickAxisTypeZ, +}; + +/*! + Models a joystick axis. + Axes have a nominated type and a continuous value between 0 and 1. +*/ +@interface CSJoystickAxis: NSObject +@property(nonatomic, readonly) CSJoystickAxisType type; +/// The current position of this axis in the range [0, 1]. +@property(nonatomic, readonly) float position; +@end + +typedef NS_OPTIONS(NSInteger, CSJoystickHatDirection) { + CSJoystickHatDirectionUp = 1 << 0, + CSJoystickHatDirectionDown = 1 << 1, + CSJoystickHatDirectionLeft = 1 << 2, + CSJoystickHatDirectionRight = 1 << 3, +}; + +/*! + Models a joystick hat. + A hat is a digital directional input, so e.g. this is how thumbpads are represented. +*/ +@interface CSJoystickHat: NSObject +@property(nonatomic, readonly) CSJoystickHatDirection direction; +@end + +/*! + Models a joystick. + + A joystick is a collection of buttons, axes and hats, each of which holds a current + state. The holder must use @c update to cause this joystick to read a fresh copy + of its state. +*/ +@interface CSJoystick: NSObject +@property(nonatomic, readonly) NSArray *buttons; +@property(nonatomic, readonly) NSArray *axes; +@property(nonatomic, readonly) NSArray *hats; + +- (void)update; +@end + +/*! + The joystick manager watches for joystick connections and disconnections and + offers a list of joysticks currently attached. + + Be warned: this means using Apple's IOKit directly to watch for Bluetooth and + USB HID devices. So to use this code, make sure you have USB and Bluetooth + enabled for the app's sandbox. +*/ +@interface CSJoystickManager : NSObject +@property(nonatomic, readonly) NSArray *joysticks; + +/// Updates all joysticks. +- (void)update; @end diff --git a/OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.m b/OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.m index 6bc827e35..843edf9a3 100644 --- a/OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.m +++ b/OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.m @@ -11,6 +11,194 @@ @import IOKit; #include +#pragma mark - CSJoystickButton + +@implementation CSJoystickButton { + IOHIDElementRef _element; +} + +- (instancetype)initWithElement:(IOHIDElementRef)element index:(NSInteger)index { + self = [super init]; + if(self) { + _index = index; + _element = (IOHIDElementRef)CFRetain(element); + } + return self; +} + +- (void)dealloc { + CFRelease(_element); +} + +- (NSString *)description { + return [NSString stringWithFormat:@"; button %ld, %@", self, (long)self.index, self.isPressed ? @"pressed" : @"released"]; +} + +- (IOHIDElementRef)element { + return _element; +} + +- (void)setIsPressed:(bool)isPressed { + _isPressed = isPressed; +} + +@end + +#pragma mark - CSJoystickAxis + +@implementation CSJoystickAxis { + IOHIDElementRef _element; +} + +- (instancetype)initWithElement:(IOHIDElementRef)element type:(CSJoystickAxisType)type { + self = [super init]; + if(self) { + _element = (IOHIDElementRef)CFRetain(element); + _type = type; + _position = 0.5f; + } + return self; +} + +- (void)dealloc { + CFRelease(_element); +} + +- (NSString *)description { + return [NSString stringWithFormat:@"; type %d, value %0.2f", self, (int)self.type, self.position]; +} + +- (IOHIDElementRef)element { + return _element; +} + +- (void)setPosition:(float)position { + _position = position; +} + +@end + +#pragma mark - CSJoystickHat + +@implementation CSJoystickHat { + IOHIDElementRef _element; +} + +- (instancetype)initWithElement:(IOHIDElementRef)element { + self = [super init]; + if(self) { + _element = (IOHIDElementRef)CFRetain(element); + } + return self; +} + +- (void)dealloc { + CFRelease(_element); +} + +- (NSString *)description { + return [NSString stringWithFormat:@"; direction %ld", self, (long)self.direction]; +} + +- (IOHIDElementRef)element { + return _element; +} + +- (void)setDirection:(CSJoystickHatDirection)direction { + _direction = direction; +} + +@end + +#pragma mark - CSJoystick + +@implementation CSJoystick { + IOHIDDeviceRef _device; +} + +- (instancetype)initWithButtons:(NSArray *)buttons + axes:(NSArray *)axes + hats:(NSArray *)hats + device:(IOHIDDeviceRef)device { + self = [super init]; + if(self) { + // Sort buttons by index. + _buttons = [buttons sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"index" ascending:YES]]]; + + // Sort axes by enum value. + _axes = [axes sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"type" ascending:YES]]]; + + // Hats have no guaranteed ordering. + _hats = hats; + + // Keep hold of the device. + _device = (IOHIDDeviceRef)CFRetain(device); + } + return self; +} + +- (void)dealloc { + CFRelease(_device); +} + +- (NSString *)description { + return [NSString stringWithFormat:@"; buttons %@, axes %@, hats %@", self, self.buttons, self.axes, self.hats]; +} + +- (void)update { + // Update buttons. + for(CSJoystickButton *button in _buttons) { + IOHIDValueRef value; + if(IOHIDDeviceGetValue(_device, button.element, &value) == kIOReturnSuccess) { + // Some pressure-sensitive buttons return values greater than 1 for hard presses, + // but this class rationalised everything to Boolean. + button.isPressed = !!IOHIDValueGetIntegerValue(value); + } + } + + // Update hats. + for(CSJoystickHat *hat in _hats) { + IOHIDValueRef value; + if(IOHIDDeviceGetValue(_device, hat.element, &value) == kIOReturnSuccess) { + // Hats report a direction, which is either one of eight or one of four. + CFIndex integerValue = IOHIDValueGetIntegerValue(value) - IOHIDElementGetLogicalMin(hat.element); + const CFIndex range = 1 + IOHIDElementGetLogicalMax(hat.element) - IOHIDElementGetLogicalMin(hat.element); + integerValue *= 8 / range; + + // Map from the HID direction to the bit field. + switch(integerValue) { + default: hat.direction = 0; break; + case 0: hat.direction = CSJoystickHatDirectionUp; break; + case 1: hat.direction = CSJoystickHatDirectionUp | CSJoystickHatDirectionRight; break; + case 2: hat.direction = CSJoystickHatDirectionRight; break; + case 3: hat.direction = CSJoystickHatDirectionRight | CSJoystickHatDirectionDown; break; + case 4: hat.direction = CSJoystickHatDirectionDown; break; + case 5: hat.direction = CSJoystickHatDirectionDown | CSJoystickHatDirectionLeft; break; + case 6: hat.direction = CSJoystickHatDirectionLeft; break; + case 7: hat.direction = CSJoystickHatDirectionLeft | CSJoystickHatDirectionUp; break; + } + } + } + + // Update axes. + for(CSJoystickAxis *axis in _axes) { + IOHIDValueRef value; + if(IOHIDDeviceGetValue(_device, axis.element, &value) == kIOReturnSuccess) { + const CFIndex integerValue = IOHIDValueGetIntegerValue(value) - IOHIDElementGetLogicalMin(axis.element); + const CFIndex range = 1 + IOHIDElementGetLogicalMax(axis.element) - IOHIDElementGetLogicalMin(axis.element); + axis.position = (float)integerValue / (float)range; + } + } +} + +- (IOHIDDeviceRef)device { + return _device; +} + +@end + +#pragma mark - CSJoystickManager + @interface CSJoystickManager () - (void)deviceMatched:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender; - (void)deviceRemoved:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender; @@ -26,13 +214,13 @@ static void DeviceRemoved(void *context, IOReturn result, void *sender, IOHIDDev @implementation CSJoystickManager { IOHIDManagerRef _hidManager; - NSMutableSet *_activeDevices; + NSMutableArray *_joysticks; } - (instancetype)init { self = [super init]; if(self) { - _activeDevices = [[NSMutableSet alloc] init]; + _joysticks = [[NSMutableArray alloc] init]; _hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); if(!_hidManager) return nil; @@ -65,14 +253,17 @@ static void DeviceRemoved(void *context, IOReturn result, void *sender, IOHIDDev } - (void)deviceMatched:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender { - NSValue *const deviceKey = [NSValue valueWithPointer:device]; - if([_activeDevices containsObject:deviceKey]) { - return; + // Double check this joystick isn't already known. + for(CSJoystick *joystick in _joysticks) { + if(joystick.device == device) return; } - [_activeDevices addObject:deviceKey]; - NSLog(@"Matched"); + // Prepare to collate a list of buttons, axes and hats for the new device. + NSMutableArray *buttons = [[NSMutableArray alloc] init]; + NSMutableArray *axes = [[NSMutableArray alloc] init]; + NSMutableArray *hats = [[NSMutableArray alloc] init]; + // Inspect all elements for those that are comprehensible to this code. const CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone); for(CFIndex index = 0; index < CFArrayGetCount(elements); ++index) { const IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, index); @@ -81,40 +272,62 @@ static void DeviceRemoved(void *context, IOReturn result, void *sender, IOHIDDev const uint32_t usagePage = IOHIDElementGetUsagePage(element); if(usagePage != kHIDPage_GenericDesktop && usagePage != kHIDPage_Button) continue; - // Then inspect the usage and type. - const IOHIDElementType type = IOHIDElementGetType(element); - - // IOHIDElementGetCookie - - switch(type) { + // Then inspect the type. + switch(IOHIDElementGetType(element)) { default: break; - case kIOHIDElementTypeInput_Button: - // Add a buton - break; + + case kIOHIDElementTypeInput_Button: { + // Add a button; pretty easy stuff. 'Usage' provides a button index. + const uint32_t usage = IOHIDElementGetUsage(element); + [buttons addObject:[[CSJoystickButton alloc] initWithElement:element index:usage]]; + } break; case kIOHIDElementTypeInput_Misc: case kIOHIDElementTypeInput_Axis: { - const uint32_t usage = IOHIDElementGetUsage(element); - // Add something depending on usage... - } break; + CSJoystickAxisType axisType; + switch(IOHIDElementGetUsage(element)) { + default: continue; - case kIOHIDElementTypeCollection: - // TODO: recurse. - break; + // Three analogue axes are implemented here; there are another three sets + // of these that could be parsed in the future if interesting. + case kHIDUsage_GD_X: axisType = CSJoystickAxisTypeX; break; + case kHIDUsage_GD_Y: axisType = CSJoystickAxisTypeY; break; + case kHIDUsage_GD_Z: axisType = CSJoystickAxisTypeZ; break; + + // A hatswitch is a multi-directional control all of its own. + case kHIDUsage_GD_Hatswitch: + [hats addObject:[[CSJoystickHat alloc] initWithElement:element]]; + continue; + } + + // Add the axis; if it was a hat switch or unrecognised then the code doesn't + // reach here. + [axes addObject:[[CSJoystickAxis alloc] initWithElement:element type:axisType]]; + } break; } } - CFRelease(elements); + + // Add this joystick to the list. + [_joysticks addObject:[[CSJoystick alloc] initWithButtons:buttons axes:axes hats:hats device:device]]; } - (void)deviceRemoved:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender { - NSValue *const deviceKey = [NSValue valueWithPointer:device]; - if(![_activeDevices containsObject:deviceKey]) { - return; + // If this joystick was recorded, remove it. + for(CSJoystick *joystick in [_joysticks copy]) { + if(joystick.device == device) { + [_joysticks removeObject:joystick]; + return; + } } +} - [_activeDevices removeObject:deviceKey]; - NSLog(@"Removed"); +- (void)update { + [self.joysticks makeObjectsPerformSelector:@selector(update)]; +} + +- (NSArray *)joysticks { + return [_joysticks copy]; } @end