From e920dc7509f0fa42118945057510dfaa23691613 Mon Sep 17 00:00:00 2001 From: James Turner Date: Mon, 11 Dec 2017 17:10:29 +0000 Subject: [PATCH] Event input layer based on HID-Api Thus runs in parallel to the existing implementation on Linux and Mac, but can (soon) replace the Mac code and will run on Windows eventually. --- CMakeLists.txt | 7 + src/Include/config_cmake.h.in | 1 + src/Input/CMakeLists.txt | 13 + src/Input/FGDeviceConfigurationMap.cxx | 53 +- src/Input/FGEventInput.cxx | 101 ++-- src/Input/FGEventInput.hxx | 23 +- src/Input/FGHIDEventInput.cxx | 726 +++++++++++++++++++++++++ src/Input/FGHIDEventInput.hxx | 46 ++ src/Input/FGLinuxEventInput.cxx | 8 +- src/Input/FGMacOSXEventInput.cxx | 5 +- src/Input/input.cxx | 16 +- src/Input/test_hidinput.cxx | 80 +++ tests/testStubs.cxx | 5 + 13 files changed, 1007 insertions(+), 77 deletions(-) create mode 100644 src/Input/FGHIDEventInput.cxx create mode 100644 src/Input/FGHIDEventInput.hxx create mode 100644 src/Input/test_hidinput.cxx diff --git a/CMakeLists.txt b/CMakeLists.txt index 800932c34..34db2989f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,7 @@ option(ENABLE_QT "Set to ON to build the internal Qt launcher" ON) option(ENABLE_TRAFFIC "Set to ON to build the external traffic generator modules" ON) option(ENABLE_FGQCANVAS "Set to ON to build the Qt-based remote canvas application" OFF) option(ENABLE_DEMCONVERT "Set to ON to build the dem conversion tool (default)" ON) +option(ENABLE_HID_INPUT "Set to ON to build HID-based input code (default)" OFF) include (DetectArch) @@ -239,6 +240,8 @@ include( ConfigureMsvc3rdParty ) if(EVENT_INPUT) if(APPLE) add_definitions(-DWITH_EVENTINPUT) + find_library(IOKIT_FRAMEWORK IOKit) + list(APPEND EVENT_INPUT_LIBRARIES ${IOKIT_FRAMEWORK}) elseif(CMAKE_SYSTEM_NAME MATCHES "Linux|FreeBSD") if(NOT UDEV_FOUND) message(WARNING "UDev not found, event input is disabled!") @@ -252,6 +255,10 @@ if(EVENT_INPUT) message(WARNING "event-based input is not supported on this platform yet") endif() + if (ENABLE_HID_INPUT) + list(APPEND EVENT_INPUT_LIBRARIES hidapi) + endif() + # Keep PLIB INPUT enabled as long as EventInput does not replace current joystick configurations. set(ENABLE_PLIB_JOYSTICK 1) else(EVENT_INPUT) diff --git a/src/Include/config_cmake.h.in b/src/Include/config_cmake.h.in index e6fc32c24..514aa6f3c 100644 --- a/src/Include/config_cmake.h.in +++ b/src/Include/config_cmake.h.in @@ -50,6 +50,7 @@ #cmakedefine HAVE_CRASHRPT #cmakedefine ENABLE_FLITE +#cmakedefine ENABLE_HID_INPUT #cmakedefine HAVE_QT diff --git a/src/Input/CMakeLists.txt b/src/Input/CMakeLists.txt index 629fe66ed..0e0235391 100644 --- a/src/Input/CMakeLists.txt +++ b/src/Input/CMakeLists.txt @@ -10,6 +10,11 @@ else() set(EVENT_INPUT_HEADERS FGLinuxEventInput.hxx) endif() +if (ENABLE_HID_INPUT) + list(APPEND EVENT_INPUT_SOURCES FGHIDEventInput.cxx) + list(APPEND EVENT_INPUT_HEADERS FGHIDEventInput.hxx) +endif() + set(SOURCES FGButton.cxx @@ -65,4 +70,12 @@ if(ENABLE_JS_DEMO) install(TARGETS js_demo RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() +if (COMMAND flightgear_test) + set(HID_INPUT_TEST_SOURCES test_hidinput.cxx FGEventInput.cxx + FGCommonInput.cxx FGDEviceConfigurationMap.cxx) + + flightgear_test(hidinput "${HID_INPUT_TEST_SOURCES}") + target_link_libraries(hidinput ${EVENT_INPUT_LIBRARIES} hidapi) +endif() + flightgear_component(Input "${SOURCES}" "${HEADERS}") diff --git a/src/Input/FGDeviceConfigurationMap.cxx b/src/Input/FGDeviceConfigurationMap.cxx index 5e3c0603c..95d107e04 100644 --- a/src/Input/FGDeviceConfigurationMap.cxx +++ b/src/Input/FGDeviceConfigurationMap.cxx @@ -28,8 +28,6 @@ #include "FGDeviceConfigurationMap.hxx" -#include - #include #include #include @@ -45,9 +43,15 @@ FGDeviceConfigurationMap::FGDeviceConfigurationMap( const string& relative_path, const std::string& nodeName) { // scan for over-ride configurations, loaded via joysticks.xml, etc - BOOST_FOREACH(SGPropertyNode_ptr preloaded, nodePath->getChildren(nodeName)) { - BOOST_FOREACH(SGPropertyNode* nameProp, preloaded->getChildren("name")) { - overrideDict[nameProp->getStringValue()] = preloaded; + for (auto preloaded : nodePath->getChildren(nodeName)) { + // allow specifying a serial number in the override + std::string serial = preloaded->getStringValue("serial-number"); + if (!serial.empty()) { + serial = "::" + serial; + } + + for (auto nameProp : preloaded->getChildren("name")) { + overrideDict[nameProp->getStringValue() + serial] = preloaded; } // of names iteration } // of defined overrides iteration @@ -62,15 +66,15 @@ FGDeviceConfigurationMap::~FGDeviceConfigurationMap() SGPropertyNode_ptr FGDeviceConfigurationMap::configurationForDeviceName(const std::string& name) { - NameNodeMap::iterator j = overrideDict.find(name); + auto j = overrideDict.find(name); if (j != overrideDict.end()) { return j->second; } // no override, check out list of config files - NamePathMap::iterator it = namePathMap.find(name); + auto it = namePathMap.find(name); if (it == namePathMap.end()) { - return SGPropertyNode_ptr(); + return {}; } SGPropertyNode_ptr result(new SGPropertyNode); @@ -79,14 +83,15 @@ FGDeviceConfigurationMap::configurationForDeviceName(const std::string& name) result->setStringValue("source", it->second.utf8Str()); } catch (sg_exception&) { SG_LOG(SG_INPUT, SG_WARN, "parse failure reading:" << it->second); - return NULL; + return {}; } + return result; } bool FGDeviceConfigurationMap::hasConfiguration(const std::string& name) const { - NameNodeMap::const_iterator j = overrideDict.find(name); + auto j = overrideDict.find(name); if (j != overrideDict.end()) { return true; } @@ -107,7 +112,7 @@ void FGDeviceConfigurationMap::scan_dir(const SGPath & path) simgear::PathList children = dir.children(simgear::Dir::TYPE_FILE | simgear::Dir::TYPE_DIR | simgear::Dir::NO_DOT_OR_DOTDOT); - BOOST_FOREACH(SGPath path, children) { + for (SGPath path : children) { if (path.isDir()) { scan_dir(path); } else if (path.extension() == "xml") { @@ -124,14 +129,12 @@ void FGDeviceConfigurationMap::scan_dir(const SGPath & path) void FGDeviceConfigurationMap::readCachedData(const SGPath& path) { - flightgear::NavDataCache* cache = flightgear::NavDataCache::instance(); - NamePathMap::iterator it; - BOOST_FOREACH(string s, cache->readStringListProperty(path.utf8Str())) { + auto cache = flightgear::NavDataCache::instance(); + for (string s : cache->readStringListProperty(path.utf8Str())) { // important - only insert if not already present. This ensures // user configs can override those in the base package, since they are // searched first. - it = namePathMap.find(s); - if (it == namePathMap.end()) { + if (namePathMap.find(s) == namePathMap.end()) { namePathMap.insert(std::make_pair(s, path)); } } // of cached names iteration @@ -148,18 +151,22 @@ void FGDeviceConfigurationMap::refreshCacheForFile(const SGPath& path) return; } - NamePathMap::iterator it; + std::string serial = n->getStringValue("serial-number"); + if (!serial.empty()) { + serial = "::" + serial; + } + string_list names; - BOOST_FOREACH(SGPropertyNode* nameProp, n->getChildren("name")) { - names.push_back(nameProp->getStringValue()); + for (auto nameProp : n->getChildren("name")) { + const string name = nameProp->getStringValue() + serial; + names.push_back(name); // same comment as readCachedData: only insert if not already present - it = namePathMap.find(names.back()); - if (it == namePathMap.end()) { - namePathMap.insert(std::make_pair(names.back(), path)); + if (namePathMap.find(name) == namePathMap.end()) { + namePathMap.insert(std::make_pair(name, path)); } } - flightgear::NavDataCache* cache = flightgear::NavDataCache::instance(); + auto cache = flightgear::NavDataCache::instance(); cache->stampCacheFile(path); cache->writeStringListProperty(path.utf8Str(), names); } diff --git a/src/Input/FGEventInput.cxx b/src/Input/FGEventInput.cxx index 48bdc53b5..be13a9028 100644 --- a/src/Input/FGEventInput.cxx +++ b/src/Input/FGEventInput.cxx @@ -101,10 +101,9 @@ FGInputEvent::FGInputEvent( FGInputDevice * aDevice, SGPropertyNode_ptr node ) : intervalSec = node->getDoubleValue("interval-sec",0.0); read_bindings( node, bindings, KEYMOD_NONE, device->GetNasalModule() ); - - PropertyList settingNodes = node->getChildren("setting"); - for( PropertyList::iterator it = settingNodes.begin(); it != settingNodes.end(); ++it ) - settings.push_back( new FGEventSetting( *it ) ); + + for (auto node : node->getChildren("setting")) + settings.push_back( new FGEventSetting(node) ); } FGInputEvent::~FGInputEvent() @@ -113,11 +112,11 @@ FGInputEvent::~FGInputEvent() void FGInputEvent::update( double dt ) { - for( setting_list_t::iterator it = settings.begin(); it != settings.end(); ++it ) { - if( (*it)->Test() ) { - double value = (*it)->GetValue(); + for (auto setting : settings) { + if( setting->Test() ) { + const double value = setting->GetValue(); if( value != lastSettingValue ) { - device->Send( GetName(), (*it)->GetValue() ); + device->Send( GetName(), value ); lastSettingValue = value; } } @@ -275,6 +274,14 @@ void FGInputDevice::Configure( SGPropertyNode_ptr aDeviceNode ) } +void FGInputDevice::AddHandledEvent( FGInputEvent_ptr event ) +{ + auto it = handledEvents.find(event->GetName()); + if (it == handledEvents.end()) { + handledEvents.insert(it, std::make_pair(event->GetName(), event)); + } +} + void FGInputDevice::update( double dt ) { for( map::iterator it = handledEvents.begin(); it != handledEvents.end(); it++ ) @@ -292,10 +299,11 @@ void FGInputDevice::update( double dt ) void FGInputDevice::HandleEvent( FGEventData & eventData ) { string eventName = TranslateEventName( eventData ); - if( debugEvents ) - cout << GetName() << " has event " << - eventName << " modifiers=" << eventData.modifiers << " value=" << eventData.value << endl; - + if( debugEvents ) { + SG_LOG(SG_INPUT, SG_INFO, GetName() << " has event " << + eventName << " modifiers=" << eventData.modifiers << " value=" << eventData.value); + } + if( handledEvents.count( eventName ) > 0 ) { handledEvents[ eventName ]->fire( eventData ); } @@ -306,6 +314,11 @@ void FGInputDevice::SetName( string name ) this->name = name; } +void FGInputDevice::SetSerialNumber( std::string serial ) +{ + serialNumber = serial; +} + void FGInputDevice::SendFeatureReport(unsigned int reportId, const std::string& data) { SG_LOG(SG_INPUT, SG_WARN, "SendFeatureReport not implemented"); @@ -335,9 +348,6 @@ void FGEventInput::shutdown() void FGEventInput::init( ) { - SG_LOG(SG_INPUT, SG_DEBUG, "Initializing event bindings"); -// SGPropertyNode * base = fgGetNode("/input/event", true); - } void FGEventInput::postinit () @@ -354,37 +364,49 @@ void FGEventInput::update( double dt ) unsigned FGEventInput::AddDevice( FGInputDevice * inputDevice ) { SGPropertyNode_ptr baseNode = fgGetNode( PROPERTY_ROOT, true ); - SGPropertyNode_ptr deviceNode = NULL; + SGPropertyNode_ptr deviceNode; - // look for configuration in the device map - if ( configMap.hasConfiguration( inputDevice->GetName() ) ) { - // found - copy to /input/event/device[n] - - // find a free index - unsigned index; - for( index = 0; index < MAX_DEVICES; index++ ) - if( (deviceNode = baseNode->getNode( "device", index, false ) ) == NULL ) - break; - - if( index == MAX_DEVICES ) { - SG_LOG(SG_INPUT, SG_WARN, "To many event devices - ignoring " << inputDevice->GetName() ); + const string deviceName = inputDevice->GetName(); + SGPropertyNode_ptr configNode; + + if (!inputDevice->GetSerialNumber().empty()) { + const string nameWithSerial = deviceName + "::" + inputDevice->GetSerialNumber(); + if (configMap.hasConfiguration(nameWithSerial)) { + configNode = configMap.configurationForDeviceName(nameWithSerial); + } + } + + if (configNode == nullptr) { + if (!configMap.hasConfiguration(deviceName)) { + SG_LOG(SG_INPUT, SG_DEBUG, "No configuration found for device " << deviceName << + " (with serial: " << inputDevice->GetSerialNumber() << ")"); + delete inputDevice; return INVALID_DEVICE_INDEX; } + configNode = configMap.configurationForDeviceName(deviceName); + } + + // found - copy to /input/event/device[n] - // create this node - deviceNode = baseNode->getNode( "device", index, true ); - - // and copy the properties from the configuration tree - copyProperties( configMap.configurationForDeviceName(inputDevice->GetName()), deviceNode ); - + // find a free index + unsigned int index; + for ( index = 0; index < MAX_DEVICES; index++ ) { + if ( (deviceNode = baseNode->getNode( "device", index, false ) ) == nullptr ) + break; } - if( deviceNode == NULL ) { - SG_LOG(SG_INPUT, SG_DEBUG, "No configuration found for device " << inputDevice->GetName() ); - delete inputDevice; + if (index == MAX_DEVICES) { + SG_LOG(SG_INPUT, SG_WARN, "To many event devices - ignoring " << inputDevice->GetName() ); + delete inputDevice; return INVALID_DEVICE_INDEX; } + // create this node + deviceNode = baseNode->getNode( "device", index, true ); + + // and copy the properties from the configuration tree + copyProperties(configNode, deviceNode ); + inputDevice->Configure( deviceNode ); try { @@ -447,6 +469,11 @@ std::string FGReportSetting::reportBytes(const std::string& moduleName) const } naRef module = nas->getModule(moduleName.c_str()); + if (naIsNil(module)) { + SG_LOG(SG_IO, SG_WARN, "No such Nasal module:" << moduleName); + return {}; + } + naRef func = naHash_cget(module, (char*) nasalFunction.c_str()); if (!naIsFunc(func)) { return std::string(); diff --git a/src/Input/FGEventInput.hxx b/src/Input/FGEventInput.hxx index 4dbfbf1e7..9bd62d5da 100644 --- a/src/Input/FGEventInput.hxx +++ b/src/Input/FGEventInput.hxx @@ -217,8 +217,9 @@ typedef class SGSharedPtr FGInputEvent_ptr; */ class FGInputDevice : public SGReferenced { public: - FGInputDevice() : debugEvents(false), grab(false) {} - FGInputDevice( std::string aName ) : name(aName), debugEvents(false), grab(false) {} + FGInputDevice() {} + FGInputDevice( std::string aName, std::string aSerial = {} ) : + name(aName), serialNumber(aSerial) {} virtual ~FGInputDevice(); @@ -239,12 +240,12 @@ public: void SetName( std::string name ); std::string & GetName() { return name; } + void SetSerialNumber( std::string serial ); + std::string& GetSerialNumber() { return serialNumber; } + void HandleEvent( FGEventData & eventData ); - virtual void AddHandledEvent( FGInputEvent_ptr handledEvent ) { - if( handledEvents.count( handledEvent->GetName() ) == 0 ) - handledEvents[handledEvent->GetName()] = handledEvent; - } + virtual void AddHandledEvent( FGInputEvent_ptr handledEvent ); virtual void Configure( SGPropertyNode_ptr deviceNode ); @@ -256,20 +257,24 @@ public: const std::string & GetNasalModule() const { return nasalModule; } -private: +protected: // A map of events, this device handles std::map handledEvents; // the device has a name to be recognized std::string name; + // serial number string to disambiguate multiple instances + // of the same device + std::string serialNumber; + // print out events comming in from the device // if true - bool debugEvents; + bool debugEvents = false; // grab the device exclusively, if O/S supports this // so events are not sent to other applications - bool grab; + bool grab = false; SGPropertyNode_ptr deviceNode; std::string nasalModule; diff --git a/src/Input/FGHIDEventInput.cxx b/src/Input/FGHIDEventInput.cxx new file mode 100644 index 000000000..6935114a8 --- /dev/null +++ b/src/Input/FGHIDEventInput.cxx @@ -0,0 +1,726 @@ +// FGHIDEventInput.cxx -- handle event driven input devices via HIDAPI +// +// Written by James Turner +// +// Copyright (C) 2017, James Turner +// +// 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. +// + +#include "config.h" + +#include "FGHIDEventInput.hxx" + +#include +#include + +#include +#include + +#include +#include +#include +#include + + +namespace HID +{ + enum class UsagePage + { + Undefined = 0, + GenericDesktop, + Simulation, + VR, + Sport, + Game, + GenericDevice, + Keyboard, + LEDs, + Button, + Ordinal, + Telephony, + Consumer, + Digitizer, + // reserved 0x0E + // PID 0x0f + Unicode = 0x10, + AlphanumericDisplay = 0x14, + + VendorDefinedStart = 0xFF00 + }; + + enum GenericDesktopUsage + { + // generic desktop section + GD_Joystick = 0x04, + GD_GamePad = 0x05, + GD_Keyboard = 0x06, + GD_Keypad = 0x07, + GD_MultiAxisController = 0x08, + GD_X = 0x30, + GD_Y, + GD_Z, + GD_Rx, + GD_Ry, + GD_Rz, + GD_Slider, + GD_Dial, + GD_Wheel, + GD_Hatswitch, + GD_DpadUp = 0x90, + GD_DpadDown, + GD_DpadRight, + GD_DpadLeft + }; + + enum LEDUsage + { + LED_Undefined = 0, + LED_Play = 0x36, + LED_Pause = 0x37, + LED_GenericIndicator = 0x4B + }; + + enum AlphanumericUsage + { + AD_AlphanumericDisplay = 0x01, + AD_BitmappedDisplay = 0x2, + AD_DisplayControlReport = 0x24, + AD_ClearDisplay = 0x25, + AD_CharacterReport = 0x2B, + AD_DisplayData = 0x2C, + AD_DisplayStatus = 0x2D, + AD_DisplayBrightness = 0x46, + AD_DisplayContrast = 0x47 + }; + + enum class ReportType + { + In = 0x08, + Out = 0x09, + Feature = 0x0B + }; + + std::string nameForUsage(uint32_t usagePage, uint32_t usage) + { + const auto enumUsage = static_cast(usagePage); + if (enumUsage == UsagePage::GenericDesktop) { + switch (usage) { + case GD_Joystick: return "joystick"; + case GD_Wheel: return "wheel"; + case GD_Dial: return "dial"; + case GD_Hatswitch: return "hat"; + case GD_Slider: return "slider"; + case GD_Rx: return "x-rotate"; + case GD_Ry: return "y-rotate"; + case GD_Rz: return "z-rotate"; + case GD_X: return "x-translate"; + case GD_Y: return "y-translate"; + case GD_Z: return "z-translate"; + default: + SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID generic desktop usage:" << usage); + } + } else if (enumUsage == UsagePage::Simulation) { + switch (usage) { + default: + SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID simulation usage:" << usage); + } + } else if (enumUsage == UsagePage::Consumer) { + switch (usage) { + default: + SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID consumer usage:" << usage); + } + } else if (enumUsage == UsagePage::AlphanumericDisplay) { + switch (usage) { + case AD_AlphanumericDisplay: return "alphanumeric"; + case AD_CharacterReport: return "character-report"; + case AD_DisplayData: return "display-data"; + case AD_DisplayBrightness: return "display-brightness"; + + default: + SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID alphanumeric usage:" << usage); + } + } else if (enumUsage == UsagePage::LEDs) { + switch (usage) { + case LED_GenericIndicator: return "led-misc"; + case LED_Pause: return "led-pause"; + default: + SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID LED usage:" << usage); + + } + } else if (enumUsage == UsagePage::Button) { + std::stringstream os; + os << "button-" << usage; + return os.str(); + } else if (enumUsage >= UsagePage::VendorDefinedStart) { + return "vendor"; + } else { + SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID usage page:" << std::hex << usagePage + << " with usage " << std::hex << usage); + } + + return "unknown"; + } + + bool shouldPrefixWithAbs(uint32_t usagePage, uint32_t usage) + { + const auto enumUsage = static_cast(usagePage); + if (enumUsage == UsagePage::GenericDesktop) { + switch (usage) { + case GD_Wheel: + case GD_Dial: + case GD_Hatswitch: + case GD_Slider: + case GD_Rx: + case GD_Ry: + case GD_Rz: + case GD_X: + case GD_Y: + case GD_Z: + return true; + default: + break; + } + } + + return false; + } + +} // of namespace + +class FGHIDEventInput::FGHIDEventInputPrivate +{ +public: + FGHIDEventInput* p = nullptr; + + void evaluateDevice(hid_device_info* deviceInfo); +}; + +// anonymous namespace to define our device subclass +namespace +{ + +class FGHIDDevice : public FGInputDevice { +public: + FGHIDDevice(hid_device_info* devInfo, + FGHIDEventInput* subsys); + + virtual ~FGHIDDevice(); + + void Open() override; + void Close() override; + + void update(double dt) override; + const char *TranslateEventName(FGEventData &eventData) override; + void Send( const char * eventName, double value ) override; + void SendFeatureReport(unsigned int reportId, const std::string& data) override; + + class Item + { + public: + Item(const std::string& n, uint32_t offset, uint8_t size) : + name(n), + bitOffset(offset), + bitSize(size) + {} + + std::string name; + uint32_t bitOffset = 0; // form the start of the report + uint8_t bitSize = 1; + bool isRelative = false; + bool doSignExtend = false; + int lastValue = 0; + // int defaultValue = 0; + // range, units, etc not needed for now + // hopefully this doesn't need to be a list + FGInputEvent_ptr event; + }; +private: + class Report + { + public: + Report(HID::ReportType ty, uint8_t n = 0) : type(ty), number(n) {} + + HID::ReportType type; + uint8_t number = 0; + std::vector items; + + uint32_t currentBitSize() const + { + uint32_t size = 0; + for (auto i : items) { + size += i->bitSize; + } + return size; + } + }; + + void scanCollection(hid_item* collection); + void scanItem(hid_item* item); + + Report* getReport(HID::ReportType ty, uint8_t number, bool doCreate = false); + + void sendReport(Report* report) const; + + uint8_t countWithName(const std::string& name) const; + std::pair itemWithName(const std::string& name) const; + + void processInputReport(Report* report, unsigned char* data, size_t length, + double dt, int keyModifiers); + + int maybeSignExtend(Item* item, int inValue); + + std::vector _reports; + std::string _hidPath; + hid_device* _device = nullptr; + bool _haveNumberedReports = false; + + // all sets which will be send on the next update() call. + std::set _dirtyReports; +}; + +class HIDEventData : public FGEventData +{ +public: + // item, value, dt, keyModifiers + HIDEventData(FGHIDDevice::Item* it, int value, double dt, int keyMods) : + FGEventData(value, dt, keyMods), + item(it) + { + assert(item); + } + + FGHIDDevice::Item* item = nullptr; +}; + +FGHIDDevice::FGHIDDevice(hid_device_info *devInfo, FGHIDEventInput *) +{ + _hidPath = devInfo->path; + + std::wstring manufactuerName = std::wstring(devInfo->manufacturer_string), + productName = std::wstring(devInfo->product_string); + const auto serial = devInfo->serial_number; + + auto path = devInfo->path; + + SetName(simgear::strutils::convertWStringToUtf8(manufactuerName) + " " + + simgear::strutils::convertWStringToUtf8(productName)); + SetSerialNumber(simgear::strutils::convertWStringToUtf8(serial)); +} + +FGHIDDevice::~FGHIDDevice() +{ + +} + +void FGHIDDevice::Open() +{ + _device = hid_open_path(_hidPath.c_str()); + if (_device == 0) { + SG_LOG(SG_INPUT, SG_WARN, "Failed to open:" << _hidPath); + return; + } + + unsigned char reportDescriptor[1024]; + int descriptorSize = hid_get_descriptor(_device, reportDescriptor, 1024); + + hid_item* rootItem = nullptr; + hid_parse_reportdesc(reportDescriptor, descriptorSize, &rootItem); + + scanCollection(rootItem); + + hid_free_reportdesc(rootItem); + + for (auto& v : handledEvents) { + auto reportItem = itemWithName(v.first); + if (!reportItem.second) { + SG_LOG(SG_INPUT, SG_WARN, "HID device has no element for event:" << v.first); + continue; + } + + FGInputEvent_ptr event = v.second; + // SG_LOG(SG_INPUT, SG_INFO, "found item for event:" << v.first); + reportItem.second->event = event; + } +} + +void FGHIDDevice::scanCollection(hid_item* c) +{ + for (hid_item* child = c->collection; child != nullptr; child = child->next) { + if (child->collection) { + scanCollection(child); + } else { + // leaf item + scanItem(child); + } + } +} + +auto FGHIDDevice::getReport(HID::ReportType ty, uint8_t number, bool doCreate) -> Report* +{ + if (number > 0) { + _haveNumberedReports = true; + } + + for (auto report : _reports) { + if ((report->type == ty) && (report->number == number)) { + return report; + } + } + + if (doCreate) { + auto r = new Report{ty, number}; + _reports.push_back(r); + return r; + } else { + return nullptr; + } +} + +auto FGHIDDevice::itemWithName(const std::string& name) const -> std::pair +{ + for (auto report : _reports) { + for (auto item : report->items) { + if (item->name == name) { + return std::make_pair(report, item); + } + } + } + + return std::make_pair(static_cast(nullptr), static_cast(nullptr)); +} + +uint8_t FGHIDDevice::countWithName(const std::string& name) const +{ + uint8_t result = 0; + size_t nameLength = name.length(); + + for (auto report : _reports) { + for (auto item : report->items) { + if (strncmp(name.c_str(), item->name.c_str(), nameLength) == 0) { + result++; + } + } + } + + return result; +} + +void FGHIDDevice::scanItem(hid_item* item) +{ + std::string name = HID::nameForUsage(item->usage >> 16, item->usage & 0xffff); + if (hid_parse_is_relative(item)) { + name = "rel-" + name; // prefix relative names + } else if (HID::shouldPrefixWithAbs(item->usage >> 16, item->usage & 0xffff)) { + name = "abs-" + name; + } + + const auto ty = static_cast(item->type); + auto existingItem = itemWithName(name); + if (existingItem.second) { + // type fixup + const HID::ReportType existingItemType = existingItem.first->type; + if (existingItemType != ty) { + // might be an item named identically in input/output and feature reports + // -> prefix the feature one with 'feature' + if (ty == HID::ReportType::Feature) { + name = "feature-" + name; + } else if (existingItemType == HID::ReportType::Feature) { + // rename this existing item since it's a feature + existingItem.second->name = "feature-" + name; + } + } + } + + // do the count now, after we did any renaming, since we might have + // N > 1 for the new name + int existingCount = countWithName(name); + if (existingCount > 0) { + if (existingCount == 1) { + // rename existing item 0 to have the "-0" suffix + auto existingItem = itemWithName(name); + existingItem.second->name += "-0"; + } + + // define the new nae + std::stringstream os; + os << name << "-" << existingCount; + name = os.str(); + } + + auto report = getReport(ty, item->report_id, true /* create */); + uint32_t bitOffset = report->currentBitSize(); + + SG_LOG(SG_INPUT, SG_INFO, "adding item:" << name); + Item* itemObject = new Item{name, bitOffset, item->report_size}; + itemObject->isRelative = hid_parse_is_relative(item); + + SG_LOG(SG_INPUT, SG_INFO, "\t logical min-max:" << item->logical_min << " / " << item->logical_max); + + itemObject->doSignExtend = (item->logical_min < 0) || (item->logical_max < 0); + report->items.push_back(itemObject); +} + +void FGHIDDevice::Close() +{ + hid_close(_device); +} + +void FGHIDDevice::update(double dt) +{ + uint8_t reportBuf[65]; + int readCount = 0; + while (true) { + readCount = hid_read_timeout(_device, reportBuf, sizeof(reportBuf), 0); + + if (readCount <= 0) { + break; + } + + int modifiers = fgGetKeyModifiers(); + const uint8_t reportNumber = _haveNumberedReports ? reportBuf[0] : 0; + auto inputReport = getReport(HID::ReportType::In, reportNumber, false); + if (!inputReport) { + SG_LOG(SG_INPUT, SG_WARN, "FGHIDDevice: Unknown input report number"); + } else { + uint8_t* reportBytes = _haveNumberedReports ? reportBuf + 1 : reportBuf; + size_t reportSize = _haveNumberedReports ? readCount - 1 : readCount; + processInputReport(inputReport, reportBytes, reportSize, dt, modifiers); + } + } + + FGInputDevice::update(dt); + + for (auto rep : _dirtyReports) { + sendReport(rep); + } + + _dirtyReports.clear(); +} + +void writeBits(uint8_t* bytes, size_t bitOffset, size_t bitSize, int value) +{ + size_t wholeBytesToSkip = bitOffset >> 3; + uint8_t* dataByte = bytes + wholeBytesToSkip; + size_t offsetInByte = bitOffset & 0x7; + size_t bitsInByte = std::min(bitSize, 8 - offsetInByte); + uint8_t mask = 0xff >> (8 - bitsInByte); + + *dataByte |= ((value & mask) << offsetInByte); + + if (bitsInByte < bitSize) { + // if we have more bits to write, recurse + writeBits(bytes, bitOffset + bitsInByte, bitSize - bitsInByte, value >> bitsInByte); + } +} + +void FGHIDDevice::sendReport(Report* report) const +{ + uint8_t reportBytes[65]; + size_t reportLength = 0; + memset(reportBytes, 0, sizeof(reportBytes)); + reportBytes[0] = report->number; + +// fill in valid data + for (auto item : report->items) { + reportLength += item->bitSize; + if (item->lastValue == 0) { + continue; + } + + writeBits(reportBytes + 1, item->bitOffset, item->bitSize, item->lastValue); + } + + reportLength /= 8; +// send the data, based on th report type + if (report->type == HID::ReportType::Feature) { + hid_send_feature_report(_device, reportBytes, reportLength + 1); + } else { + assert(report->type == HID::ReportType::Out); + hid_write(_device, reportBytes, reportLength + 1); + } +} + +int extractBits(uint8_t* bytes, size_t lengthInBytes, size_t bitOffset, size_t bitSize) +{ + const size_t wholeBytesToSkip = bitOffset >> 3; + const size_t offsetInByte = bitOffset & 0x7; + + // work out how many whole bytes to copy + const size_t bytesToCopy = std::min(sizeof(uint32_t), (offsetInByte + bitSize + 7) / 8); + uint32_t v = 0; + // this goes from byte alignment to word alignment safely + memcpy((void*) &v, bytes + wholeBytesToSkip, bytesToCopy); + + // shift down so lowest bit is aligned + v = v >> offsetInByte; + + // mask off any extraneous top bits + const uint32_t mask = ~(0xffffffff << bitSize); + v &= mask; + + return v; +} + +int signExtend(int inValue, size_t bitSize) +{ + const int m = 1U << (bitSize - 1); + return (inValue ^ m) - m; +} + +int FGHIDDevice::maybeSignExtend(Item* item, int inValue) +{ + return item->doSignExtend ? signExtend(inValue, item->bitSize) : inValue; +} + +void FGHIDDevice::processInputReport(Report* report, unsigned char* data, + size_t length, + double dt, int keyModifiers) +{ + //SG_LOG(SG_INPUT, SG_INFO, "Report " << report->number); +#if 0 + { + std::ostringstream byteString; + byteString << std::hex; + for (int i=0; iitems) { + int value = extractBits(data, length, item->bitOffset, item->bitSize); + + value = maybeSignExtend(item, value); + + // suppress events for values that aren't changing + if (item->isRelative) { + // supress spurious 0-valued relative events + if (value == 0) { + continue; + } + } else { + // supress no-change events for absolute items + if (value == item->lastValue) { + continue; + } + } + + item->lastValue = value; + if (!item->event) + continue; + + SG_LOG(SG_INPUT, SG_INFO, "\titem:" << item->name << " = " << value); + + HIDEventData event{item, value, dt, keyModifiers}; + HandleEvent(event); + } +} + +void FGHIDDevice::SendFeatureReport(unsigned int reportId, const std::string& data) +{ + uint8_t buf[65]; + size_t len = std::min(data.length() + 1, sizeof(buf)); + buf[0] = reportId; + memcpy(buf + 1, data.data(), len - 1); + hid_send_feature_report(_device, buf, len); +} + +const char *FGHIDDevice::TranslateEventName(FGEventData &eventData) +{ + HIDEventData& hidEvent = static_cast(eventData); + return hidEvent.item->name.c_str(); +} + +void FGHIDDevice::Send(const char *eventName, double value) +{ + auto item = itemWithName(eventName); + if (item.second == nullptr) { + SG_LOG(SG_INPUT, SG_WARN, "FGHIDDevice: unknown item name:" << eventName); + return; + } + + int intValue = static_cast(value); + if (item.second->lastValue == intValue) { + return; // not actually changing + } + + // update the stored value prior to sending + item.second->lastValue = intValue; + _dirtyReports.insert(item.first); +} + +} // of anonymous namespace + +FGHIDEventInput::FGHIDEventInput() : + FGEventInput(), + d(new FGHIDEventInputPrivate) +{ + d->p = this; // store back pointer to outer object on pimpl +} + +FGHIDEventInput::~FGHIDEventInput() +{ +} + +void FGHIDEventInput::init() +{ + // have to wait until postinit since loading config files + // requires Nasal to be running +} + +void FGHIDEventInput::postinit() +{ + hid_init(); + + hid_device_info* devices = hid_enumerate(0 /* vendor ID */, 0 /* product ID */); + + for (hid_device_info* curDev = devices; curDev != nullptr; curDev = curDev->next) { + d->evaluateDevice(curDev); + } + + hid_free_enumeration(devices); +} + +void FGHIDEventInput::shutdown() +{ + FGEventInput::shutdown(); + + hid_exit(); +} + +// +// read all elements in each input device +// +void FGHIDEventInput::update(double dt) +{ + FGEventInput::update(dt); +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + +void FGHIDEventInput::FGHIDEventInputPrivate::evaluateDevice(hid_device_info* deviceInfo) +{ + SG_LOG(SG_INPUT, SG_DEBUG, "HID device:" << deviceInfo->product_string << " from " << deviceInfo->manufacturer_string); + + // allocate an input device, and add to the base class to see if we have + // a config + p->AddDevice(new FGHIDDevice(deviceInfo, p)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + + diff --git a/src/Input/FGHIDEventInput.hxx b/src/Input/FGHIDEventInput.hxx new file mode 100644 index 000000000..92d66fd1e --- /dev/null +++ b/src/Input/FGHIDEventInput.hxx @@ -0,0 +1,46 @@ +// FGHIDEventInput.hxx -- handle event driven input devices via HIDAPI +// +// Written by James Turner +// +// Copyright (C) 2017, James Turner +// +// 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. +// + + +#ifndef __FGHIDEVENTINPUT_HXX_ +#define __FGHIDEVENTINPUT_HXX_ + +#include + +#include "FGEventInput.hxx" + +class FGHIDEventInput : public FGEventInput { +public: + FGHIDEventInput(); + + virtual ~FGHIDEventInput(); + + void update(double dt) override; + void init() override; + void postinit(); + void shutdown() override; +private: + class FGHIDEventInputPrivate; + + std::unique_ptr d; +}; + +#endif diff --git a/src/Input/FGLinuxEventInput.cxx b/src/Input/FGLinuxEventInput.cxx index 39702cfc7..876be00b1 100644 --- a/src/Input/FGLinuxEventInput.cxx +++ b/src/Input/FGLinuxEventInput.cxx @@ -260,8 +260,8 @@ public: static EventTypeByName EVENT_TYPE_BY_NAME; -FGLinuxInputDevice::FGLinuxInputDevice( std::string aName, std::string aDevname ) : - FGInputDevice(aName), +FGLinuxInputDevice::FGLinuxInputDevice( std::string aName, std::string aDevname, std::string aSerial ) : + FGInputDevice(aName,aSerial), devname( aDevname ), fd(-1) { @@ -481,10 +481,10 @@ void FGLinuxEventInput::postinit() dev = udev_device_get_parent( dev ); const char * name = udev_device_get_sysattr_value(dev,"name"); - + const char * serial = udev_device_get_sysattr_value(dev, "serial"); SG_LOG(SG_INPUT,SG_DEBUG, "name=" << (name?name:"") << ", node=" << (node?node:"")); if( name && node ) - AddDevice( new FGLinuxInputDevice(name, node) ); + AddDevice( new FGLinuxInputDevice(name, node, serial) ); udev_device_unref(dev); } diff --git a/src/Input/FGMacOSXEventInput.cxx b/src/Input/FGMacOSXEventInput.cxx index b8d7168fe..58bec9d4a 100644 --- a/src/Input/FGMacOSXEventInput.cxx +++ b/src/Input/FGMacOSXEventInput.cxx @@ -250,7 +250,8 @@ 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_INFO, "matched device:" << productName << " from " << manufacturer); // allocate a Mac input device, and add to the base class to see if we have @@ -258,6 +259,8 @@ void FGMacOSXEventInputPrivate::matchedDevice(IOHIDDeviceRef device) FGMacOSXInputDevice* macInputDevice = new FGMacOSXInputDevice(device, this); macInputDevice->SetName(manufacturer + " " + productName); + macInputDevice->SetSerialNumber(serial); + p->AddDevice(macInputDevice); } diff --git a/src/Input/input.cxx b/src/Input/input.cxx index 55ea02d99..10419b6fc 100644 --- a/src/Input/input.cxx +++ b/src/Input/input.cxx @@ -22,9 +22,7 @@ // // $Id$ -#ifdef HAVE_CONFIG_H -# include -#endif +#include #include "input.hxx" @@ -48,6 +46,10 @@ #define INPUTEVENT_CLASS FGLinuxEventInput #endif +#if defined(ENABLE_HID_INPUT) +#include "FGHIDEventInput.hxx" +#endif + #endif //////////////////////////////////////////////////////////////////////// @@ -81,6 +83,14 @@ FGInput::FGInput () set_subsystem( "input-event", new INPUTEVENT_CLASS() ); } #endif + +#if defined(ENABLE_HID_INPUT) + if (fgGetBool("/sim/input/enable-hid", true)) { + set_subsystem( "input-event-hid", new FGHIDEventInput() ); + } else { + SG_LOG(SG_INPUT, SG_WARN, "HID-based event input disabled"); + } +#endif } FGInput::~FGInput () diff --git a/src/Input/test_hidinput.cxx b/src/Input/test_hidinput.cxx new file mode 100644 index 000000000..1f90d4d1a --- /dev/null +++ b/src/Input/test_hidinput.cxx @@ -0,0 +1,80 @@ +// Written by James Turner, started 2017. +// +// Copyright (C) 2017 James Turner +// +// 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. + +#include "config.h" + +#include "unitTestHelpers.hxx" + +#include + +#include "FGHIDEventInput.cxx" + +void testValueExtract() +{ + uint8_t testDataFromSpec[4] = {0, 0xf4, 0x1 | (0x7 << 2), 0x03}; + SG_VERIFY(extractBits(testDataFromSpec, 4, 8, 10) == 500); + SG_VERIFY(extractBits(testDataFromSpec, 4, 18, 10) == 199); + + uint8_t testData2[4] = {0x01 << 6 | 0x0f, + 0x17 | (1 << 6), + 0x3 | (0x11 << 2), + 0x3d | (1 << 6) }; + + SG_VERIFY(extractBits(testData2, 4, 0, 6) == 15); + SG_VERIFY(extractBits(testData2, 4, 6, 12) == 3421); + SG_VERIFY(extractBits(testData2, 4, 18, 12) == 3921); + SG_VERIFY(extractBits(testData2, 4, 30, 1) == 1); + SG_VERIFY(extractBits(testData2, 4, 31, 1) == 0); +} + +// void writeBits(uint8_t* bytes, size_t bitOffset, size_t bitSize, int value) + +void testValueInsert() +{ + uint8_t buf[8]; + memset(buf, 0, 8); + + int a = 3421; + int b = 3921; + writeBits(buf, 6, 12, a); + writeBits(buf, 18, 12, b); + + SG_VERIFY(buf[0] == 0x40); + SG_VERIFY(buf[1] == 0x57); + SG_VERIFY(buf[2] == (0x03 | 0x44)); + SG_VERIFY(buf[3] == 0x3d); +} + +void testSignExtension() +{ + SG_VERIFY(signExtend(0x80, 8) == -128); + SG_VERIFY(signExtend(0xff, 8) == -1); + SG_VERIFY(signExtend(0x7f, 8) == 127); + + SG_VERIFY(signExtend(0x831, 12) == -1999); + SG_VERIFY(signExtend(0x7dd, 12) == 2013); +} + +int main(int argc, char* argv[]) +{ + testValueExtract(); + testValueInsert(); + testSignExtension(); + + return EXIT_SUCCESS; +} diff --git a/tests/testStubs.cxx b/tests/testStubs.cxx index c646c9b44..c9f632173 100644 --- a/tests/testStubs.cxx +++ b/tests/testStubs.cxx @@ -81,6 +81,11 @@ void postinitNasalGUI(naRef globals, naContext c) } +int fgGetKeyModifiers() +{ + return 0; +} + void syncPausePopupState() {