// FGMacOSXEventInput.cxx -- handle event driven input devices for Mac OS X // // Written by Tatsuhiro Nishioka, started Aug. 2009. // // Copyright (C) 2009 Tasuhiro Nishioka, tat fgmacosx gmail com // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 2 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. // // $Id$ #include "FGMacOSXEventInput.hxx" #include #include #include #include #include #include static std::string nameForUsage(uint32_t usagePage, uint32_t usage) { if (usagePage == kHIDPage_GenericDesktop) { switch (usage) { case kHIDUsage_GD_Joystick: return "joystick"; case kHIDUsage_GD_Wheel: return "wheel"; case kHIDUsage_GD_Dial: return "dial"; case kHIDUsage_GD_Hatswitch: return "hat"; case kHIDUsage_GD_Slider: return "slider"; case kHIDUsage_GD_Rx: return "x-rotate"; case kHIDUsage_GD_Ry: return "y-rotate"; case kHIDUsage_GD_Rz: return "z-rotate"; case kHIDUsage_GD_X: return "x-translate"; case kHIDUsage_GD_Y: return "y-translate"; case kHIDUsage_GD_Z: return "z-translate"; default: SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID generic desktop usage:" << usage); } } else if (usagePage == kHIDPage_Simulation) { switch (usage) { default: SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID simulation usage:" << usage); } } else if (usagePage == kHIDPage_Consumer) { switch (usage) { default: SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID consumer usage:" << usage); } } else if (usagePage == kHIDPage_AlphanumericDisplay) { switch (usage) { case kHIDUsage_AD_AlphanumericDisplay: return "alphanumeric"; case kHIDUsage_AD_CharacterReport: return "character-report"; case kHIDUsage_AD_DisplayData: return "display-data"; case 0x46: return "display-brightness"; default: SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID alphanumeric usage:" << usage); } } else if (usagePage == kHIDPage_LEDs) { switch (usage) { case kHIDUsage_LED_GenericIndicator: return "led-misc"; case kHIDUsage_LED_Pause: return "led-pause"; default: SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID LED usage:" << usage); } } else if (usagePage == kHIDPage_Button) { std::stringstream os; os << "button-" << usage; return os.str(); } else if (usagePage >= kHIDPage_VendorDefinedStart) { return "vendor"; } else { SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID usage page:" << usagePage); } return "unknown"; } class FGMacOSXEventInputPrivate { public: IOHIDManagerRef hidManager; FGMacOSXEventInput* p; double currentDt; int currentModifiers; void matchedDevice(IOHIDDeviceRef device); void removedDevice(IOHIDDeviceRef device); void iterateDevices(CFSetRef matchingSet); std::string getDeviceStringProperty(IOHIDDeviceRef device, CFStringRef hidProp); bool getDeviceIntProperty(IOHIDDeviceRef device, CFStringRef hidProp, int& value); }; static void deviceMatchingCallback( void * inContext, // context from IOHIDManagerRegisterDeviceMatchingCallback IOReturn inResult, // the result of the matching operation void * inSender, // the IOHIDManagerRef for the new device IOHIDDeviceRef inIOHIDDeviceRef // the new HID device ) { // printf("%s(context: %p, result: %p, sender: %p, device: %p).\n", // __PRETTY_FUNCTION__, inContext, (void *) inResult, inSender, (void*) inIOHIDDeviceRef); FGMacOSXEventInputPrivate* p = static_cast(inContext); p->matchedDevice(inIOHIDDeviceRef); } // Handle_DeviceMatchingCallback static void deviceRemovalCallback( void * inContext, // context from IOHIDManagerRegisterDeviceMatchingCallback IOReturn inResult, // the result of the matching operation void * inSender, // the IOHIDManagerRef for the new device IOHIDDeviceRef inIOHIDDeviceRef // the new HID device ) { // printf("%s(context: %p, result: %p, sender: %p, device: %p).\n", // __PRETTY_FUNCTION__, inContext, (void *) inResult, inSender, (void*) inIOHIDDeviceRef); FGMacOSXEventInputPrivate* p = static_cast(inContext); p->removedDevice(inIOHIDDeviceRef); } // Handle_DeviceMatchingCallback // // FGMacOSXInputDevice implementation // // // FGMacOSXInputDevice // Mac OS X specific FGInputDevice // class FGMacOSXInputDevice : public FGInputDevice { public: FGMacOSXInputDevice(IOHIDDeviceRef hidRef, FGMacOSXEventInputPrivate* subsys); virtual ~FGMacOSXInputDevice(); bool Open() override; void Close() override; virtual void update(double dt); virtual const char *TranslateEventName(FGEventData &eventData); virtual void Send( const char * eventName, double value ); virtual void SendFeatureReport(unsigned int reportId, const std::string& data); virtual void AddHandledEvent( FGInputEvent_ptr handledEvent ); void drainQueue(); void handleValue(IOHIDValueRef value); private: void buildElementNameDictionary(); std::string nameForHIDElement(IOHIDElementRef element) const; IOHIDDeviceRef _hid; IOHIDQueueRef _queue; FGMacOSXEventInputPrivate* _subsystem; typedef std::map NameElementDict; NameElementDict namedElements; }; #if 0 static void valueAvailableCallback(void * inContext, // context from IOHIDQueueRegisterValueAvailableCallback IOReturn inResult, // the inResult void * inSender // IOHIDQueueRef of the queue ) { FGMacOSXInputDevice* dev = static_cast(inContext); dev->drainQueue(); } // Handle_ValueAvailableCallback #endif // // FGMacOSXEventInput implementation // FGMacOSXEventInput::FGMacOSXEventInput() : FGEventInput(), d(new FGMacOSXEventInputPrivate) { d->p = this; // store back pointer to outer object on pimpl SG_LOG(SG_INPUT, SG_DEBUG, "FGMacOSXEventInput created"); } FGMacOSXEventInput::~FGMacOSXEventInput() { } void FGMacOSXEventInput::init() { // everything is deffered until postinit since we need Nasal } void FGMacOSXEventInput::postinit() { d->hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); // set the HID device matching dictionary IOHIDManagerSetDeviceMatching( d->hidManager, NULL /* all devices */); IOHIDManagerRegisterDeviceMatchingCallback(d->hidManager, deviceMatchingCallback, d.get()); IOHIDManagerRegisterDeviceRemovalCallback(d->hidManager, deviceRemovalCallback, d.get()); IOHIDManagerOpen(d->hidManager, kIOHIDOptionsTypeNone); IOHIDManagerScheduleWithRunLoop(d->hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); } void FGMacOSXEventInput::shutdown() { FGEventInput::shutdown(); if (d->hidManager) { IOHIDManagerClose(d->hidManager, kIOHIDOptionsTypeNone); IOHIDManagerUnscheduleFromRunLoop(d->hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); CFRelease(d->hidManager); } } // // read all elements in each input device // void FGMacOSXEventInput::update(double dt) { d->currentDt = dt; d->currentModifiers = fgGetKeyModifiers(); FGEventInput::update(dt); } void FGMacOSXEventInputPrivate::matchedDevice(IOHIDDeviceRef device) { std::string productName = getDeviceStringProperty(device, CFSTR(kIOHIDProductKey)); std::string manufacturer = getDeviceStringProperty(device, CFSTR(kIOHIDManufacturerKey)); std::string serial = getDeviceStringProperty(device, CFSTR(kIOHIDSerialNumberKey)); SG_LOG(SG_INPUT, SG_DEBUG, "MacOSX-EventInput: matched device:" << productName << "( from " << manufacturer << ")"); // allocate a Mac input device, and add to the base class to see if we have // a config FGMacOSXInputDevice* macInputDevice = new FGMacOSXInputDevice(device, this); macInputDevice->SetName(manufacturer + " " + productName); macInputDevice->SetSerialNumber(serial); p->AddDevice(macInputDevice); } void FGMacOSXEventInputPrivate::removedDevice(IOHIDDeviceRef device) { std::string productName = getDeviceStringProperty(device, CFSTR(kIOHIDProductKey)); std::string manufacturer = getDeviceStringProperty(device, CFSTR(kIOHIDManufacturerKey)); // see if we have an entry for the device } std::string FGMacOSXEventInputPrivate::getDeviceStringProperty(IOHIDDeviceRef device, CFStringRef hidProp) { CFStringRef prop = (CFStringRef) IOHIDDeviceGetProperty(device, hidProp); if ((prop == nullptr)|| (CFGetTypeID(prop) != CFStringGetTypeID())) { return std::string(); } size_t len = CFStringGetLength(prop); if (len == 0) { return std::string(); } char* buffer = static_cast(malloc(len + 1)); // + 1 for the null byte Boolean ok = CFStringGetCString(prop, buffer, len + 1, kCFStringEncodingUTF8); if (!ok) { SG_LOG(SG_INPUT, SG_WARN, "string conversion failed"); } std::string result(buffer, len); free(buffer); return result; } bool FGMacOSXEventInputPrivate::getDeviceIntProperty(IOHIDDeviceRef device, CFStringRef hidProp, int& value) { CFTypeRef prop = IOHIDDeviceGetProperty(device, hidProp); if (CFGetTypeID(prop) != CFNumberGetTypeID()) { return false; } int32_t v; Boolean result = CFNumberGetValue((CFNumberRef) prop, kCFNumberSInt32Type, &v); value = v; return result; } void FGMacOSXEventInputPrivate::iterateDevices(CFSetRef matchingSet) { size_t numDevices = CFSetGetCount(matchingSet); IOHIDDeviceRef* devs = static_cast(::malloc(numDevices * sizeof(IOHIDDeviceRef))); CFSetGetValues(matchingSet, (const void **) devs); for (size_t i=0; i < numDevices; ++i) { matchedDevice(devs[i]); } free(devs); } FGMacOSXInputDevice::FGMacOSXInputDevice(IOHIDDeviceRef hidRef, FGMacOSXEventInputPrivate* subsys) { _hid = hidRef; CFRetain(_hid); _subsystem = subsys; CFIndex maxDepth = 128; _queue = IOHIDQueueCreate(kCFAllocatorDefault, _hid, maxDepth, kIOHIDOptionsTypeNone); } FGMacOSXInputDevice::~FGMacOSXInputDevice() { CFRelease(_queue); CFRelease(_hid); } bool FGMacOSXInputDevice::Open() { IOReturn result = IOHIDDeviceOpen(_hid, kIOHIDOptionsTypeNone); if (result != kIOReturnSuccess) { return false; } IOHIDDeviceScheduleWithRunLoop(_hid, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); // IOHIDQueueRegisterValueAvailableCallback(_queue, valueAvailableCallback, this); IOHIDQueueScheduleWithRunLoop(_queue, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); IOHIDQueueStart(_queue); return true; } void FGMacOSXInputDevice::buildElementNameDictionary() { // copy all elements from the device CFArrayRef elements = IOHIDDeviceCopyMatchingElements(_hid, NULL, kIOHIDOptionsTypeNone); CFIndex count = CFArrayGetCount(elements); typedef std::map NameCountMap; NameCountMap nameCounts; std::set seenCookies; for (int i = 0; i < count; ++i) { IOHIDElementRef element = (IOHIDElementRef) CFArrayGetValueAtIndex(elements, i); IOHIDElementCookie cookie = IOHIDElementGetCookie(element); if (seenCookies.find(cookie) != seenCookies.end()) { // skip duplicate match of this element; continue; } seenCookies.insert(cookie); // bool isWrapping = IOHIDElementIsWrapping(element); IOHIDElementType ty = IOHIDElementGetType(element); if (ty == kIOHIDElementTypeCollection) { continue; } uint32_t page = IOHIDElementGetUsagePage(element); uint32_t usage = IOHIDElementGetUsage(element); bool isRelative = IOHIDElementIsRelative(element); // compute the name for the element std::string name = nameForUsage(page, usage); if (isRelative) { name = "rel-" + name; // prefix relative elements } NameCountMap::iterator it = nameCounts.find(name); unsigned int nameCount; std::string finalName = name; if (it == nameCounts.end()) { nameCounts[name] = nameCount = 1; } else { // check if we have a collison but different element types, eg // input & feature. In which case, prefix the feature one since // we assume it's the input one which is more interesting. IOHIDElementRef other = namedElements[name]; IOHIDElementType otherTy = IOHIDElementGetType(other); if (otherTy != ty) { // types mismatch if (otherTy == kIOHIDElementTypeFeature) { namedElements[name] = element; element = other; finalName = "feature-" + name; } else if (ty == kIOHIDElementTypeFeature) { finalName = "feature-" + name; } nameCount = 1; } else { // duplicate element, append ordinal suffix std::stringstream os; os << name << "-" << it->second; finalName = os.str(); nameCount = ++(it->second); } } CFRetain(element); namedElements[finalName] = element; if (nameCount == 2) { // we have more than one entry for this name, so ensures // the first item is availabe with the -0 suffix std::stringstream os; os << name << "-0"; namedElements[os.str()] = namedElements[name]; CFRetain(namedElements[os.str()]); } } // HID debugging code #if 0 NameElementDict::const_iterator it; for (it = namedElements.begin(); it != namedElements.end(); ++it) { int report = IOHIDElementGetReportID(it->second); int reportCount = IOHIDElementGetReportCount(it->second); int reportSize = IOHIDElementGetReportSize(it->second); CFTypeRef sizeProp = IOHIDElementGetProperty(it->second, CFSTR(kIOHIDElementDuplicateIndexKey)); if (sizeProp) { CFShow(sizeProp); } bool isArray = IOHIDElementIsArray(it->second); if (isArray) { SG_LOG(SG_INPUT, SG_INFO, "YYYYYYYYYYYYYYYYYY"); } SG_LOG(SG_INPUT, SG_INFO, "\t" << it->first << " report ID " << report << " /count=" << reportCount << " ;sz=" << reportSize); } #endif CFRelease(elements); } void FGMacOSXInputDevice::Close() { // leaking these otherwise we get a crash shutting down the HID-manager // object. Don't understand why that should be the case #if 0 for (auto it : namedElements) { CFRelease(it.second); } #endif namedElements.clear(); IOHIDQueueStop(_queue); IOHIDQueueUnscheduleFromRunLoop(_queue, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); IOHIDDeviceUnscheduleFromRunLoop(_hid, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); IOHIDDeviceClose(_hid, kIOHIDOptionsTypeNone); } void FGMacOSXInputDevice::AddHandledEvent( FGInputEvent_ptr handledEvent ) { SG_LOG(SG_INPUT, SG_DEBUG, "adding event:" << handledEvent->GetName()); if (namedElements.empty()) { buildElementNameDictionary(); } NameElementDict::iterator it = namedElements.find(handledEvent->GetName()); if (it == namedElements.end()) { SG_LOG(SG_INPUT, SG_WARN, "device does not have any element with name:" << handledEvent->GetName()); return; } IOHIDQueueAddElement(_queue, it->second); FGInputDevice::AddHandledEvent(handledEvent); } void FGMacOSXInputDevice::update(double dt) { SG_UNUSED(dt); drainQueue(); FGInputDevice::update(dt); } void FGMacOSXInputDevice::drainQueue() { do { IOHIDValueRef valueRef = IOHIDQueueCopyNextValueWithTimeout( _queue, 0. ); if ( !valueRef ) break; handleValue(valueRef); CFRelease( valueRef ); } while ( 1 ) ; } void FGMacOSXInputDevice::handleValue(IOHIDValueRef value) { IOHIDElementRef element = IOHIDValueGetElement(value); CFIndex val; // need report count to know if we have to handle this specially int reportCount = IOHIDElementGetReportCount(element); if (reportCount > 1) { // for a repeated element, we need to read the final value of // the data bytes (even though this will be the lowest numbered name // for this element. int bitSize = IOHIDElementGetReportSize(element); const uint8_t* bytes= IOHIDValueGetBytePtr(value); size_t byteLen = IOHIDValueGetLength(value); uint8_t finalByte = bytes[byteLen - 1]; if (bitSize == 8) { val = (int8_t) finalByte; // force sign extension } else if (bitSize == 4) { int8_t b = finalByte >> 4; // high nibble if (b & 0x08) { // manually sign extend b |= 0xf0; // set all bits except the low nibble } val = b; } else { throw sg_io_exception("Unhandled bit size in MacoSXHID"); } } else { val = IOHIDValueGetIntegerValue(value); } // supress spurious 0-valued relative events std::string name = nameForHIDElement(element); if ((name.find("rel-") == 0) && (val == 0)) { return; } FGMacOSXEventData eventData(name, val, _subsystem->currentDt, _subsystem->currentModifiers); HandleEvent(eventData); } std::string FGMacOSXInputDevice::nameForHIDElement(IOHIDElementRef element) const { NameElementDict::const_iterator it; for (it = namedElements.begin(); it != namedElements.end(); ++it) { if (it->second == element) { return it->first; } } throw sg_exception("Unknown HID element"); } const char *FGMacOSXInputDevice::TranslateEventName(FGEventData &eventData) { FGMacOSXEventData &macEvent = (FGMacOSXEventData &)eventData; return macEvent.name.c_str(); } // // Outputs value to an writable element (like LEDElement) // void FGMacOSXInputDevice::Send(const char *eventName, double value) { NameElementDict::const_iterator it = namedElements.find(eventName); if (it == namedElements.end()) { SG_LOG(SG_INPUT, SG_WARN, "FGMacOSXInputDevice::Send: unknown element:" << eventName); return; } CFIndex cfVal = value; uint64_t timestamp = 0; IOHIDValueRef valueRef = IOHIDValueCreateWithIntegerValue(kCFAllocatorDefault, it->second, timestamp, cfVal); IOHIDDeviceSetValue(_hid, it->second, valueRef); CFRelease(valueRef); } void FGMacOSXInputDevice::SendFeatureReport(unsigned int reportId, const std::string& data) { string d(data); if (reportId > 0) { // prefix with report number d.insert(d.begin(), static_cast(reportId)); } size_t len = d.size(); const uint8_t* bytes = (const uint8_t*) d.data(); std::stringstream ss; for (int i=0; i(bytes[i]); ss << ":"; } SG_LOG(SG_INPUT, SG_INFO, "report " << reportId << " sending " << ss.str()); IOReturn res = IOHIDDeviceSetReport(_hid, kIOHIDReportTypeFeature, reportId, /* Report ID*/ bytes, len); if (res != kIOReturnSuccess) { SG_LOG(SG_INPUT, SG_WARN, "failed to send feature report" << reportId); } }