1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-07 23:29:06 +00:00

Attempts a full implementation of the joystick manager.

So it currently vends a list of existing joysticks plus their states. More work will be required for a UI — e.g. there is no way to identify one joystick from another — but this'll do for now.
This commit is contained in:
Thomas Harte 2018-07-22 15:23:26 -04:00
parent 8d18808efe
commit c05b6397b0
2 changed files with 309 additions and 32 deletions

View File

@ -8,8 +8,72 @@
#import <Foundation/Foundation.h>
@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<CSJoystickButton *> *buttons;
@property(nonatomic, readonly) NSArray<CSJoystickAxis *> *axes;
@property(nonatomic, readonly) NSArray<CSJoystickHat *> *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<CSJoystick *> *joysticks;
/// Updates all joysticks.
- (void)update;
@end

View File

@ -11,6 +11,194 @@
@import IOKit;
#include <IOKit/hid/IOHIDLib.h>
#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:@"<CSJoystickButton: %p>; 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:@"<CSJoystickAxis: %p>; 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:@"<CSJoystickHat: %p>; 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<CSJoystickButton *> *)buttons
axes:(NSArray<CSJoystickAxis *> *)axes
hats:(NSArray<CSJoystickHat *> *)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:@"<CSJoystick: %p>; 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<NSValue *> *_activeDevices;
NSMutableArray<CSJoystick *> *_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<CSJoystickButton *> *buttons = [[NSMutableArray alloc] init];
NSMutableArray<CSJoystickAxis *> *axes = [[NSMutableArray alloc] init];
NSMutableArray<CSJoystickHat *> *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<CSJoystick *> *)joysticks {
return [_joysticks copy];
}
@end