971 lines
29 KiB
C++
971 lines
29 KiB
C++
// FGHIDEventInput.cxx -- handle event driven input devices via HIDAPI
|
|
//
|
|
// Written by James Turner
|
|
//
|
|
// Copyright (C) 2017, James Turner <zakalawe@mac.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.
|
|
//
|
|
|
|
#include "config.h"
|
|
|
|
#include "FGHIDEventInput.hxx"
|
|
|
|
#include <cstdlib>
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
|
|
#include <hidapi/hidapi.h>
|
|
#include <hidapi/hidparse.h>
|
|
|
|
#include <simgear/structure/exception.hxx>
|
|
#include <simgear/sg_inlines.h>
|
|
#include <simgear/misc/strutils.hxx>
|
|
#include <simgear/io/lowlevel.hxx>
|
|
|
|
const char* hexTable = "0123456789ABCDEF";
|
|
|
|
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_Rows = 0x35,
|
|
AD_Columns = 0x36,
|
|
AD_7SegmentDirectMap = 0x43,
|
|
AD_14SegmentDirectMap = 0x45,
|
|
AD_DisplayBrightness = 0x46,
|
|
AD_DisplayContrast = 0x47
|
|
};
|
|
|
|
enum class ReportType
|
|
{
|
|
Invalid = 0,
|
|
In = 0x08,
|
|
Out = 0x09,
|
|
Feature = 0x0B
|
|
};
|
|
|
|
std::string nameForUsage(uint32_t usagePage, uint32_t usage)
|
|
{
|
|
const auto enumUsage = static_cast<UsagePage>(usagePage);
|
|
if (enumUsage == UsagePage::Undefined) {
|
|
std::stringstream os;
|
|
os << "undefined-" << usage;
|
|
return os.str();
|
|
}
|
|
|
|
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";
|
|
case AD_7SegmentDirectMap: return "seven-segment-direct";
|
|
case AD_14SegmentDirectMap: return "fourteen-segment-direct";
|
|
|
|
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>(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;
|
|
}
|
|
|
|
ReportType reportTypeFromString(const std::string& s)
|
|
{
|
|
if (s == "input") return ReportType::In;
|
|
if (s == "output") return ReportType::Out;
|
|
if (s == "feature") return ReportType::Feature;
|
|
return ReportType::Invalid;
|
|
}
|
|
} // 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();
|
|
|
|
bool Open() override;
|
|
void Close() override;
|
|
void Configure(SGPropertyNode_ptr node) 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<Item*> items;
|
|
|
|
uint32_t currentBitSize() const
|
|
{
|
|
uint32_t size = 0;
|
|
for (auto i : items) {
|
|
size += i->bitSize;
|
|
}
|
|
return size;
|
|
}
|
|
};
|
|
|
|
bool parseUSBHIDDescriptor();
|
|
void parseCollection(hid_item* collection);
|
|
void parseItem(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<Report*, Item*> 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);
|
|
|
|
void defineReport(SGPropertyNode_ptr reportNode);
|
|
|
|
std::vector<Report*> _reports;
|
|
std::string _hidPath;
|
|
hid_device* _device = nullptr;
|
|
bool _haveNumberedReports = false;
|
|
bool _debugRaw = false;
|
|
|
|
/// set if we parsed the device description our XML
|
|
/// instead of from the USB data. Useful on Windows where the data
|
|
/// is inaccessible, or devices with broken descriptors
|
|
bool _haveLocalDescriptor = false;
|
|
|
|
/// allow specifying the descriptor as hex bytes in XML
|
|
std::vector<uint8_t>_rawXMLDescriptor;
|
|
|
|
// all sets which will be send on the next update() call.
|
|
std::set<Report*> _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 manufacturerName, productName;
|
|
productName = devInfo->product_string ? std::wstring(devInfo->product_string)
|
|
: L"unknown HID device";
|
|
|
|
if (devInfo->manufacturer_string) {
|
|
manufacturerName = std::wstring(devInfo->manufacturer_string);
|
|
SetName(simgear::strutils::convertWStringToUtf8(manufacturerName) + " " +
|
|
simgear::strutils::convertWStringToUtf8(productName));
|
|
} else {
|
|
SetName(simgear::strutils::convertWStringToUtf8(productName));
|
|
}
|
|
|
|
const auto serial = devInfo->serial_number;
|
|
std::string path(devInfo->path);
|
|
// most devices return an empty serial number, unfortunately
|
|
if ((serial != nullptr) && std::wcslen(serial) > 0) {
|
|
SetSerialNumber(simgear::strutils::convertWStringToUtf8(serial));
|
|
}
|
|
|
|
SG_LOG(SG_INPUT, SG_DEBUG, "HID device:" << GetName() << " at path " << _hidPath);
|
|
}
|
|
|
|
FGHIDDevice::~FGHIDDevice()
|
|
{
|
|
if (_device) {
|
|
hid_close(_device);
|
|
}
|
|
}
|
|
|
|
void FGHIDDevice::Configure(SGPropertyNode_ptr node)
|
|
{
|
|
// base class first
|
|
FGInputDevice::Configure(node);
|
|
|
|
if (node->hasChild("hid-descriptor")) {
|
|
_haveLocalDescriptor = true;
|
|
if (debugEvents) {
|
|
SG_LOG(SG_INPUT, SG_INFO, GetUniqueName() << " will configure using local HID descriptor");
|
|
}
|
|
|
|
for (auto report : node->getChild("hid-descriptor")->getChildren("report")) {
|
|
defineReport(report);
|
|
}
|
|
}
|
|
|
|
if (node->hasChild("hid-raw-descriptor")) {
|
|
_rawXMLDescriptor = simgear::strutils::decodeHex(node->getStringValue("hid-raw-descriptor"));
|
|
if (debugEvents) {
|
|
SG_LOG(SG_INPUT, SG_INFO, GetUniqueName() << " will configure using XML-defined raw HID descriptor");
|
|
}
|
|
}
|
|
|
|
if (node->getBoolValue("hid-debug-raw")) {
|
|
_debugRaw = true;
|
|
}
|
|
}
|
|
|
|
bool FGHIDDevice::Open()
|
|
{
|
|
_device = hid_open_path(_hidPath.c_str());
|
|
if (_device == nullptr) {
|
|
SG_LOG(SG_INPUT, SG_WARN, GetUniqueName() << ": HID: Failed to open:" << _hidPath);
|
|
SG_LOG(SG_INPUT, SG_WARN, "\tnote on Linux you may need to adjust permissions of the device using UDev rules.");
|
|
return false;
|
|
}
|
|
|
|
#if !defined(SG_WINDOWS)
|
|
if (_rawXMLDescriptor.empty()) {
|
|
_rawXMLDescriptor.resize(2048);
|
|
int descriptorSize = hid_get_descriptor(_device, _rawXMLDescriptor.data(), _rawXMLDescriptor.size());
|
|
if (descriptorSize <= 0) {
|
|
SG_LOG(SG_INPUT, SG_WARN, "HID: " << GetUniqueName() << " failed to read HID descriptor");
|
|
return false;
|
|
}
|
|
|
|
_rawXMLDescriptor.resize(descriptorSize);
|
|
}
|
|
#endif
|
|
|
|
if (!_haveLocalDescriptor) {
|
|
bool ok = parseUSBHIDDescriptor();
|
|
if (!ok)
|
|
return false;
|
|
}
|
|
|
|
for (auto& v : handledEvents) {
|
|
auto reportItem = itemWithName(v.first);
|
|
if (!reportItem.second) {
|
|
SG_LOG(SG_INPUT, SG_WARN, "HID device:" << GetUniqueName() << " has no element for event:" << v.first);
|
|
continue;
|
|
}
|
|
|
|
FGInputEvent_ptr event = v.second;
|
|
if (debugEvents) {
|
|
SG_LOG(SG_INPUT, SG_INFO, "\tfound item for event:" << v.first);
|
|
}
|
|
|
|
reportItem.second->event = event;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FGHIDDevice::parseUSBHIDDescriptor()
|
|
{
|
|
#if defined(SG_WINDOWS)
|
|
if (_rawXMLDescriptor.empty()) {
|
|
SG_LOG(SG_INPUT, SG_ALERT, GetUniqueName() << ": on Windows, there is no way to extract the UDB-HID report descriptor. "
|
|
<< "\nPlease supply the report descriptor in the device XML configuration.");
|
|
SG_LOG(SG_INPUT, SG_ALERT, "See this page:<> for information on extracting the report descriptor on Windows");
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
if (_debugRaw) {
|
|
SG_LOG(SG_INPUT, SG_INFO, "\nHID: descriptor for:" << GetUniqueName());
|
|
{
|
|
std::ostringstream byteString;
|
|
|
|
for (unsigned i=0; i<_rawXMLDescriptor.size(); ++i) {
|
|
byteString << hexTable[_rawXMLDescriptor[i] >> 4];
|
|
byteString << hexTable[_rawXMLDescriptor[i] & 0x0f];
|
|
byteString << " ";
|
|
}
|
|
SG_LOG(SG_INPUT, SG_INFO, "\tbytes: " << byteString.str());
|
|
}
|
|
}
|
|
|
|
hid_item* rootItem = nullptr;
|
|
hid_parse_reportdesc(_rawXMLDescriptor.data(), _rawXMLDescriptor.size(), &rootItem);
|
|
if (debugEvents) {
|
|
SG_LOG(SG_INPUT, SG_INFO, "\nHID: scan for:" << GetUniqueName());
|
|
}
|
|
|
|
parseCollection(rootItem);
|
|
|
|
hid_free_reportdesc(rootItem);
|
|
return true;
|
|
}
|
|
|
|
void FGHIDDevice::parseCollection(hid_item* c)
|
|
{
|
|
for (hid_item* child = c->collection; child != nullptr; child = child->next) {
|
|
if (child->collection) {
|
|
parseCollection(child);
|
|
} else {
|
|
// leaf item
|
|
parseItem(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<Report*, Item*>
|
|
{
|
|
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<Report*>(nullptr), static_cast<Item*>(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::parseItem(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<HID::ReportType>(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();
|
|
|
|
if (debugEvents) {
|
|
SG_LOG(SG_INPUT, SG_INFO, GetUniqueName() << ": add:" << name << ", bits: " << bitOffset << ":" << (int) item->report_size
|
|
<< ", report=" << (int) item->report_id);
|
|
}
|
|
|
|
Item* itemObject = new Item{name, bitOffset, item->report_size};
|
|
itemObject->isRelative = hid_parse_is_relative(item);
|
|
itemObject->doSignExtend = (item->logical_min < 0) || (item->logical_max < 0);
|
|
report->items.push_back(itemObject);
|
|
}
|
|
|
|
void FGHIDDevice::Close()
|
|
{
|
|
if (_device) {
|
|
hid_close(_device);
|
|
_device = nullptr;
|
|
}
|
|
}
|
|
|
|
void FGHIDDevice::update(double dt)
|
|
{
|
|
if (!_device) {
|
|
return;
|
|
}
|
|
|
|
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, GetName() << ": FGHIDDevice: Unknown input report number:" <<
|
|
static_cast<int>(reportNumber));
|
|
} 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 FGHIDDevice::sendReport(Report* report) const
|
|
{
|
|
if (!_device) {
|
|
return;
|
|
}
|
|
|
|
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;
|
|
|
|
if (_debugRaw) {
|
|
std::ostringstream byteString;
|
|
for (size_t i=0; i<reportLength; ++i) {
|
|
byteString << hexTable[reportBytes[i] >> 4];
|
|
byteString << hexTable[reportBytes[i] & 0x0f];
|
|
byteString << " ";
|
|
}
|
|
SG_LOG(SG_INPUT, SG_INFO, "sending bytes: " << byteString.str());
|
|
}
|
|
|
|
|
|
// send the data, based on the 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 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)
|
|
{
|
|
if (_debugRaw) {
|
|
SG_LOG(SG_INPUT, SG_INFO, GetName() << " FGHIDDeivce received input report:" << (int) report->number << ", len=" << length);
|
|
{
|
|
std::ostringstream byteString;
|
|
for (size_t i=0; i<length; ++i) {
|
|
byteString << hexTable[data[i] >> 4];
|
|
byteString << hexTable[data[i] & 0x0f];
|
|
byteString << " ";
|
|
}
|
|
SG_LOG(SG_INPUT, SG_INFO, "\tbytes: " << byteString.str());
|
|
}
|
|
}
|
|
|
|
for (auto item : report->items) {
|
|
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;
|
|
|
|
if (_debugRaw) {
|
|
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)
|
|
{
|
|
if (!_device) {
|
|
return;
|
|
}
|
|
|
|
if (_debugRaw) {
|
|
SG_LOG(SG_INPUT, SG_INFO, GetName() << ": FGHIDDevice: Sending feature report:" << (int) reportId << ", len=" << data.size());
|
|
{
|
|
std::ostringstream byteString;
|
|
|
|
for (unsigned int i=0; i<data.size(); ++i) {
|
|
byteString << hexTable[data[i] >> 4];
|
|
byteString << hexTable[data[i] & 0x0f];
|
|
byteString << " ";
|
|
}
|
|
SG_LOG(SG_INPUT, SG_INFO, "\tbytes: " << byteString.str());
|
|
}
|
|
}
|
|
|
|
uint8_t buf[65];
|
|
size_t len = std::min(data.length() + 1, sizeof(buf));
|
|
buf[0] = reportId;
|
|
memcpy(buf + 1, data.data(), len - 1);
|
|
int r = hid_send_feature_report(_device, buf, len);
|
|
if (r < 0) {
|
|
SG_LOG(SG_INPUT, SG_WARN, GetName() << ": FGHIDDevice: Sending feature report failed, error-string is:\n"
|
|
<< simgear::strutils::error_string(errno));
|
|
}
|
|
}
|
|
|
|
const char *FGHIDDevice::TranslateEventName(FGEventData &eventData)
|
|
{
|
|
HIDEventData& hidEvent = static_cast<HIDEventData&>(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, GetName() << ": FGHIDDevice:unknown item name:" << eventName);
|
|
return;
|
|
}
|
|
|
|
int intValue = static_cast<int>(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);
|
|
}
|
|
|
|
void FGHIDDevice::defineReport(SGPropertyNode_ptr reportNode)
|
|
{
|
|
const int nChildren = reportNode->nChildren();
|
|
uint32_t bitCount = 0;
|
|
const auto rty = HID::reportTypeFromString(reportNode->getStringValue("type"));
|
|
if (rty == HID::ReportType::Invalid) {
|
|
SG_LOG(SG_INPUT, SG_WARN, GetName() << ": FGHIDDevice: invalid report type:" <<
|
|
reportNode->getStringValue("type"));
|
|
return;
|
|
}
|
|
|
|
const auto id = reportNode->getIntValue("id");
|
|
if (id > 0) {
|
|
_haveNumberedReports = true;
|
|
}
|
|
|
|
auto report = new Report(rty, id);
|
|
_reports.push_back(report);
|
|
|
|
for (int c=0; c < nChildren; ++c) {
|
|
const auto nd = reportNode->getChild(c);
|
|
const int size = nd->getIntValue("size", 1); // default to a single bit
|
|
if (!strcmp(nd->getName(), "unused-bits")) {
|
|
bitCount += size;
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(nd->getName(), "type") || !strcmp(nd->getName(), "id")) {
|
|
continue; // already handled above
|
|
}
|
|
|
|
// allow repeating items
|
|
uint8_t count = nd->getIntValue("count", 1);
|
|
std::string name = nd->getNameString();
|
|
const auto lastHypen = name.rfind("-");
|
|
std::string baseName = name.substr(0, lastHypen + 1);
|
|
int baseIndex = std::stoi(name.substr(lastHypen + 1));
|
|
|
|
const bool isRelative = (name.find("rel-") == 0);
|
|
const bool isSigned = nd->getBoolValue("is-signed", false);
|
|
|
|
for (uint8_t i=0; i < count; ++i) {
|
|
std::ostringstream oss;
|
|
oss << baseName << (baseIndex + i);
|
|
Item* itemObject = new Item{oss.str(), bitCount, static_cast<uint8_t>(size)};
|
|
itemObject->isRelative = isRelative;
|
|
itemObject->doSignExtend = isSigned;
|
|
report->items.push_back(itemObject);
|
|
bitCount += size;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
} // of anonymous namespace
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
FGHIDEventInput::FGHIDEventInput() :
|
|
FGEventInput(),
|
|
d(new FGHIDEventInputPrivate)
|
|
{
|
|
d->p = this; // store back pointer to outer object on pimpl
|
|
}
|
|
|
|
FGHIDEventInput::~FGHIDEventInput()
|
|
{
|
|
}
|
|
|
|
void FGHIDEventInput::init()
|
|
{
|
|
FGEventInput::init();
|
|
// have to wait until postinit since loading config files
|
|
// requires Nasal to be running
|
|
}
|
|
|
|
void FGHIDEventInput::reinit()
|
|
{
|
|
SG_LOG(SG_INPUT, SG_INFO, "Re-Initializing HID input bindings");
|
|
FGHIDEventInput::shutdown();
|
|
FGHIDEventInput::init();
|
|
FGHIDEventInput::postinit();
|
|
}
|
|
|
|
void FGHIDEventInput::postinit()
|
|
{
|
|
SG_LOG(SG_INPUT, SG_INFO, "HID event input starting up");
|
|
|
|
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()
|
|
{
|
|
SG_LOG(SG_INPUT, SG_INFO, "HID event input shutting down");
|
|
FGEventInput::shutdown();
|
|
|
|
hid_exit();
|
|
}
|
|
|
|
//
|
|
// read all elements in each input device
|
|
//
|
|
void FGHIDEventInput::update(double dt)
|
|
{
|
|
FGEventInput::update(dt);
|
|
}
|
|
|
|
|
|
// Register the subsystem.
|
|
SGSubsystemMgr::Registrant<FGHIDEventInput> registrantFGHIDEventInput;
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void FGHIDEventInput::FGHIDEventInputPrivate::evaluateDevice(hid_device_info* deviceInfo)
|
|
{
|
|
// allocate an input device, and add to the base class to see if we have
|
|
// a config
|
|
p->AddDevice(new FGHIDDevice(deviceInfo, p));
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|