#include "pihidevice.h" #include "piliterals_string.h" #include "piliterals_time.h" #ifndef WINDOWS # include "pidir.h" # include "pifile.h" # include "piiostream.h" # include # include # include # include # include # include #else // clang-format off # undef _WIN32_WINNT # define _WIN32_WINNT 0x0600 # include # include extern "C" { # include } // clang-format on #endif bool PIHIDeviceInfo::match(const PIString & str) const { if (product.toLowerCase().contains(str.toLowerCase())) return true; if (path.toLowerCase().contains(str.toLowerCase())) return true; return false; } int PIHIDeviceInfo::axesAbsoluteCount() const { int ret = 0; for (const auto & a: axes) if (!a.is_relative) ++ret; return ret; } int PIHIDeviceInfo::axesRelativeCount() const { int ret = 0; for (const auto & a: axes) if (a.is_relative) ++ret; return ret; } void PIHIDeviceInfo::prepare() { axis_by_dataindex.clear(); button_by_dataindex.clear(); for (const auto & i: axes) axis_by_dataindex[i.data_index] = i; for (const auto & i: buttons) button_by_dataindex[i.data_index] = i; } PICout operator<<(PICout s, const PIHIDeviceInfo & v) { s.saveAndSetControls(0); s << "PIHIDeviceInfo(" << v.product << " (" << v.manufacturer << "), " << v.VID << ":" << v.PID << ", " //<< "path \"" << v.path << "\", " << v.axesAbsoluteCount() << " abs axes, " << v.axesRelativeCount() << " rel axes, " << v.buttonsCount() << " buttons)"; s.restoreControls(); return s; } PRIVATE_DEFINITION_START(PIHIDevice) #ifndef WINDOWS PIFile file; bool is_js = false; #else PIByteArray buffer; HANDLE deviceHandle = nullptr; PHIDP_PREPARSED_DATA preparsed = nullptr; #endif PRIVATE_DEFINITION_END(PIHIDevice) PIHIDevice::~PIHIDevice() { close(); } bool PIHIDevice::isOpened() const { #ifndef WINDOWS return PRIVATE->file.isOpened(); #else return PRIVATE->deviceHandle; #endif } bool PIHIDevice::open(const PIHIDeviceInfo & device) { close(); cur_axes.clear(); cur_buttons.clear(); di = device; di.prepare(); if (device.isNull()) return false; #ifndef WINDOWS if (!PRIVATE->file.open(di.path, PIIODevice::ReadOnly)) { piCout << "PIHIDevice::open" << di.path << "error:" << errorString(); return false; } PRIVATE->is_js = PIFile::FileInfo(di.path).name().startsWith("js"_a); return true; #else PRIVATE->deviceHandle = CreateFileA(di.path.dataAscii(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); if (PRIVATE->deviceHandle == INVALID_HANDLE_VALUE) { piCoutObj << "PIHIDevice::open" << di.path << "error:" << errorString(); PRIVATE->deviceHandle = nullptr; return false; } HidD_GetPreparsedData(PRIVATE->deviceHandle, &PRIVATE->preparsed); return true; #endif } bool PIHIDevice::open() { return open(di); } void PIHIDevice::close() { stop(); #ifndef WINDOWS PRIVATE->file.close(); #else if (PRIVATE->deviceHandle) { CloseHandle(PRIVATE->deviceHandle); PRIVATE->deviceHandle = nullptr; } if (PRIVATE->preparsed) { HidD_FreePreparsedData(PRIVATE->preparsed); PRIVATE->preparsed = nullptr; } #endif } void PIHIDevice::start() { if (!isOpened()) return; PIThread::start(200_Hz); #ifndef WINDOWS #else #endif } void PIHIDevice::stop() { PIThread::stop(); #ifdef WINDOWS if (PRIVATE->deviceHandle) { CancelIoEx(PRIVATE->deviceHandle, nullptr); } #endif if (!waitForFinish(1000_ms)) terminate(); } void PIHIDevice::run() { Event e; #ifndef WINDOWS # pragma pack(push, 1) struct input_event { struct timeval time; ushort type; ushort code; uint value; }; struct js_event { uint time; /* event timestamp in milliseconds */ short value; /* value */ uchar type; /* event type */ uchar number; /* axis/button number */ }; # pragma pack(pop) if (PRIVATE->is_js) { js_event ie; while (PRIVATE->file.read(&ie, sizeof(ie)) == sizeof(ie)) { if (ie.type == 0) continue; bool ok = false; switch (ie.type) { case 2: { // piCout << ie.value; cur_axes[ie.number] = procDeadZone(ie.value / 32767. / 2. + 0.5); ok = true; } break; case 1: // piCout << ie.code; cur_buttons[ie.number] = ie.value; ok = true; break; default: break; } // piCout << ok << ie.type << ie.code << ie.value; if (!ok) continue; } } else { input_event ie; while (PRIVATE->file.read(&ie, sizeof(ie)) == sizeof(ie)) { if (ie.type == 0) continue; bool ok = false; switch (ie.type) { case 2: { // rel axis auto vi = di.axis_by_dataindex.value(ie.code); if (vi.isValid()) { e.type = Event::tAxisMove; e.axis = vi; e.value = static_cast(ie.value); event(e); } ok = true; } break; case 3: { // abs axis auto vi = di.axis_by_dataindex.value(ie.code); float fv = (ie.value - vi.min) / piMaxf(1.f, (float)(vi.max - vi.min)); cur_axes[ie.code] = procDeadZone(fv); ok = true; } break; case 1: // button // piCout << ie.code; cur_buttons[ie.code] = ie.value; ok = true; break; default: break; } // piCout << ok << ie.type << ie.code << ie.value; if (!ok) continue; } } #else PRIVATE->buffer.resize(di.input_report_size).fill(0); DWORD readed = 0; // piCout << "read" << PRIVATE->deviceHandle << PRIVATE->buffer.size(); HIDP_DATA gdd[256]; ULONG gdd_len = 256; if (ReadFile(PRIVATE->deviceHandle, PRIVATE->buffer.data(), PRIVATE->buffer.size_s(), &readed, nullptr) != TRUE) return; // piCout << readed << PRIVATE->buffer.size(); if (readed != PRIVATE->buffer.size()) return; auto gd = HidP_GetData(HidP_Input, gdd, &gdd_len, PRIVATE->preparsed, (PCHAR)PRIVATE->buffer.data(), PRIVATE->buffer.size_s()); NO_UNUSED(gd); // piCout << "readed" << PRIVATE->buffer << gdd_len; auto cbit = cur_buttons.makeIterator(); while (cbit.next()) cbit.value() = 0; for (ULONG i = 0; i < gdd_len; ++i) { const auto & cd(gdd[i]); // piCout << cd.DataIndex << cd.RawValue; auto vi = di.axis_by_dataindex.value(cd.DataIndex); if (vi.isValid()) { if (vi.is_relative) { e.type = Event::tAxisMove; e.axis = vi; e.value = static_cast(cd.RawValue); event(e); } else { // auto & axis(cur_axes[cd.DataIndex]); float fv = (cd.RawValue - vi.min) / piMaxf(1.f, (float)(vi.max - vi.min)); cur_axes[vi.data_index] = procDeadZone(fv); } // piCout << "axis" << vi.data_index << "->" << cur_axes[vi.data_index]; continue; } auto bi = di.button_by_dataindex.value(cd.DataIndex); if (bi.isValid()) { cur_buttons[bi.data_index] = 1; // piCout << "button" << bi.data_index << "-> 1"; continue; } } #endif auto ait = cur_axes.makeIterator(); e.type = Event::tAxisMove; while (ait.next()) { if (ait.value() != prev_axes.value(ait.key())) { e.axis = di.axis_by_dataindex.value(ait.key()); e.value = ait.value(); event(e); } } prev_axes = cur_axes; auto bit = cur_buttons.makeIterator(); e.type = Event::tButton; while (bit.next()) { if (bit.value() != prev_buttons.value(bit.key())) { e.button = di.button_by_dataindex.value(bit.key()); e.value = bit.value(); event(e); } } prev_buttons = cur_buttons; } double PIHIDevice::procDeadZone(double in) { double cv = (in - 0.5) * 2.; if (piAbsd(cv) < dead_zone) return 0.; if (cv < 0) return (cv + dead_zone) / (1. - dead_zone); else return (cv - dead_zone) / (1. - dead_zone); return cv; } PIVector PIHIDevice::allDevices(bool try_open) { PIVector ret; #ifndef WINDOWS auto readFile = [](const PIString & path) { auto ba = PIFile::readAll(path); PIString ret; for (const auto & b: ba) { if (!PIChar(b).isAscii()) break; ret += PIChar(b); } return ret.trim(); }; auto isDir = [](const PIFile::FileInfo & fi) { return fi.isDir() && !fi.flags[PIFile::FileInfo::Dot] && !fi.flags[PIFile::FileInfo::DotDot]; }; auto checkBit = [](const ullong & flags, ullong bit, const PIString & name) { return (flags & (1ULL << bit)) > 0; }; PIDir hid_dir("/sys/bus/hid/devices"_a); auto hid_devs = hid_dir.entries(); for (auto hd: hid_devs) { // piCout << d.path; if (!isDir(hd)) continue; PIDir dir_input(hd.path + "/input"_a); auto hid_inputs = dir_input.entries(); for (auto hd_i: hid_inputs) { if (!isDir(hd_i)) continue; // now in /sys/bus/hid/devices//input/input // piCout << hd_i.path; PIHIDeviceInfo dev; dev.product = readFile(hd_i.path + "/name"_a); // piCout << readFile(hd_i.path + "/name"_a); dev.VID = readFile(hd_i.path + "/id/vendor"_a); dev.PID = readFile(hd_i.path + "/id/product"_a); dev.version = readFile(hd_i.path + "/id/version"_a); dev.manufacturer = readFile(hd_i.path + "/id/manufacturer"_a); // piCout << dev.product; dev.input_report_size = 24; PIDir dir_e(hd_i.path); PIStringList devs; auto dl_e = dir_e.entries(); for (auto d_e: dl_e) { if (!d_e.isDir() || d_e.flags[PIFile::FileInfo::Dot] || d_e.flags[PIFile::FileInfo::DotDot]) continue; devs << d_e.name(); } /*bool dev_found = false; for (const auto & d: devs) { if (d.startsWith("js"_a)) { dev.path = "/dev/input/"_a + d; dev_found = true; break; } } if (!dev_found) {*/ // search for event dir for (const auto & d: devs) { if (d.startsWith("event"_a)) { dev.path = "/dev/input/"_a + d; break; } } if (dev.path.isEmpty()) continue; if (try_open) { PIFile test_f(dev.path, PIIODevice::ReadOnly); if (test_f.isClosed()) continue; } ullong ev = readFile(hd_i.path + "/capabilities/ev"_a).toULLong(16); auto readAxes = [readFile, checkBit, &hd_i, &dev](const PIString & file, bool is_relative) { PIVector ret; ullong bits = readFile(hd_i.path + file).toULLong(16); // piCout<< PICoutManipulators::Bin << abs; if (bits > 0) { int fd = ::open(dev.path.dataAscii(), O_RDONLY); if (fd < 0) { // piCout << "Warning: can`t open" << dev.path << errorString(); } PIHIDeviceInfo::AxisInfo ai; ai.is_relative = is_relative; for (int bit = 0; bit < 64; ++bit) { if (checkBit(bits, bit, PIString::fromNumber(bit))) { ai.data_index = bit; if (fd >= 0) { struct input_absinfo abs_info; if (ioctl(fd, EVIOCGABS(bit), &abs_info) != -1) { ai.min = abs_info.minimum; ai.max = abs_info.maximum; // piCout << "axis" << bit << abs_info.minimum << abs_info.maximum << abs_info.flat << abs_info.fuzz; } else { ai.min = 0; ai.max = 1024; } } ret << ai; } } if (fd >= 0) ::close(fd); } return ret; }; dev.axes << readAxes("/capabilities/abs"_a, false) << readAxes("/capabilities/rel"_a, true); for (int i = 0; i < dev.axes.size_s(); ++i) dev.axes[i].index = i; PIString key_str = readFile(hd_i.path + "/capabilities/key"_a); PIVector key_words; while (key_str.isNotEmpty()) { PIString w = key_str.takeWord().trimmed(); if (w.isEmpty()) break; key_words.prepend(w.toULong(16)); } PIBitArray key_bits((const uchar *)key_words.data(), key_words.size_s() * sizeof(ulong)); PIHIDeviceInfo::ButtonInfo bi; for (uint b = 0; b < key_bits.bitSize(); ++b) { if (!key_bits[b]) continue; bi.index = dev.buttons.size_s(); bi.code = b; bi.data_index = b; dev.buttons << bi; } if (dev.manufacturer.isEmpty()) { for (const auto * hwp: {"/usr/share/hwdata/usb.ids", "/var/lib/usbutils/usb.ids"}) { PIFile hwf(hwp, PIIODevice::ReadOnly); if (hwf.isClosed()) continue; // piCout << "search" << dev.VID << "in" << hwp; PIString line; PIIOTextStream ts(&hwf); while (!hwf.isEnd()) { line = ts.readLine(); if (line.startsWith(dev.VID)) { line.takeWord(); line.trim(); dev.manufacturer = line; break; } } break; } } if (dev.isNotNull() && (dev.buttonsCount() > 0 || dev.axesCount() > 0)) ret << dev; } } #else GUID guid; HidD_GetHidGuid(&guid); HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&guid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (deviceInfoSet == INVALID_HANDLE_VALUE) { piCout << "SetupDiGetClassDevs error:" << errorString(); return ret; } SP_DEVICE_INTERFACE_DATA deviceInterfaceData; deviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); for (DWORD i = 0; SetupDiEnumDeviceInterfaces(deviceInfoSet, nullptr, &guid, i, &deviceInterfaceData); ++i) { PIHIDeviceInfo dev; DWORD requiredSize = 0; SetupDiGetDeviceInterfaceDetail(deviceInfoSet, &deviceInterfaceData, nullptr, 0, &requiredSize, nullptr); PSP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData = reinterpret_cast(new BYTE[requiredSize]); PIScopeExitCall exit_call([&deviceInterfaceDetailData]() { delete[] reinterpret_cast(deviceInterfaceDetailData); }); deviceInterfaceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); if (!SetupDiGetDeviceInterfaceDetail(deviceInfoSet, &deviceInterfaceData, deviceInterfaceDetailData, requiredSize, nullptr, nullptr)) { piCout << "SetupDiGetDeviceInterfaceDetail error:" << errorString(); continue; } if (try_open) { auto test_f = CreateFileA(deviceInterfaceDetailData->DevicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); if (test_f == INVALID_HANDLE_VALUE) continue; CloseHandle(test_f); } HANDLE deviceHandle = CreateFileA(deviceInterfaceDetailData->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); if (deviceHandle == INVALID_HANDLE_VALUE) { piCout << "CreateFileA error:" << errorString(); continue; } // piCout << i << deviceHandle; PHIDP_PREPARSED_DATA preparsed = nullptr; if (HidD_GetPreparsedData(deviceHandle, &preparsed) == FALSE) { piCout << "HidD_GetPreparsedData error:" << errorString(); continue; } // auto pp = PIByteArray(preparsed, 64); // piCout << piChangedEndian(pp.dataAs(7)) << piChangedEndian(pp.dataAs(8)) << // piChangedEndian(pp.dataAs(9)); // piCout << pp; dev.path = deviceInterfaceDetailData->DevicePath; HIDD_ATTRIBUTES attr; PIByteArray str_buff(1024); HidD_GetAttributes(deviceHandle, &attr); dev.VID.setNumber(attr.VendorID, 16).expandLeftTo(4, '0').toLowerCase(); dev.PID.setNumber(attr.ProductID, 16).expandLeftTo(4, '0').toLowerCase(); dev.version.setNumber(attr.VersionNumber); HIDP_CAPS caps; HidP_GetCaps(preparsed, &caps); dev.input_report_size = caps.InputReportByteLength; str_buff.fill(0); HidD_GetManufacturerString(deviceHandle, str_buff.data(), str_buff.size_s()); dev.manufacturer = PIString(reinterpret_cast(str_buff.data())); str_buff.fill(0); HidD_GetProductString(deviceHandle, str_buff.data(), str_buff.size_s()); dev.product = PIString(reinterpret_cast(str_buff.data())); str_buff.fill(0); HidD_GetSerialNumberString(deviceHandle, str_buff.data(), str_buff.size_s()); dev.serial = PIString(reinterpret_cast(str_buff.data())); PIVector axes_abs, axes_rel; HIDP_VALUE_CAPS value_caps[1024]; USHORT _cnt = caps.NumberInputValueCaps; memset(value_caps, 0, sizeof(value_caps[0]) * 1024); HidP_GetValueCaps(HidP_Input, value_caps, &_cnt, preparsed); for (int i = 0; i < caps.NumberInputValueCaps; ++i) { const auto & vc(value_caps[i]); PIHIDeviceInfo::AxisInfo vi; vi.bits = vc.BitSize; vi.min = vc.LogicalMin; vi.max = vc.LogicalMax; vi.is_relative = vc.IsAbsolute == 0; if (vi.max == vi.min) ++vi.max; if (vc.IsRange == 1) { int count = vc.Range.UsageMax - vc.Range.UsageMin + 1; int cur_index = vc.Range.DataIndexMin; for (int v = 0; v < count; ++v) { vi.data_index = cur_index; if (vi.is_relative) axes_rel << vi; else axes_abs << vi; ++cur_index; } } else { vi.data_index = vc.NotRange.DataIndex; if (vi.is_relative) axes_rel << vi; else axes_abs << vi; } // piCout << vc.LinkCollection << vc.LinkUsage << vc.LinkUsagePage; } dev.axes << axes_abs << axes_rel; for (int i = 0; i < dev.axes.size_s(); ++i) dev.axes[i].index = i; HIDP_BUTTON_CAPS button_caps[1024]; _cnt = caps.NumberInputButtonCaps; memset(button_caps, 0, sizeof(button_caps[0]) * 1024); HidP_GetButtonCaps(HidP_Input, button_caps, &_cnt, preparsed); for (int i = 0; i < _cnt; ++i) { const auto & bc(button_caps[i]); PIHIDeviceInfo::ButtonInfo bi; // dev.values.append(PIHIDeviceInfo::ValueInfo{value_caps[i].BitSize, value_caps[i].LogicalMin, value_caps[i].LogicalMax}); if (bc.IsRange == 1) { int count = bc.Range.UsageMax - bc.Range.UsageMin + 1; int cur_index = bc.Range.DataIndexMin; for (int b = 0; b < count; ++b) { bi.index = dev.buttons.size_s(); bi.data_index = cur_index; dev.buttons << bi; ++cur_index; // piCout << b << (start_bit / 8) << (start_bit % 8); } } else { bi.index = dev.buttons.size_s(); bi.data_index = bc.NotRange.DataIndex; dev.buttons << bi; // piCout << (bi.bit / 8) << (bi.bit % 8); } // piCout << bc.IsRange << bc.Range.UsageMin << bc.Range.UsageMax // << bc.Range.DataIndexMin << bc.Range.DataIndexMax; } HidD_FreePreparsedData(preparsed); CloseHandle(deviceHandle); if (dev.buttonsCount() > 0 || dev.axesCount() > 0) ret << dev; } SetupDiDestroyDeviceInfoList(deviceInfoSet); #endif return ret; } PIHIDeviceInfo PIHIDevice::findDevice(const PIString & name) { if (name.isEmpty()) return PIHIDeviceInfo(); auto devices = PIHIDevice::allDevices(); for (auto d: devices) { if (d.match(name)) return d; } return PIHIDeviceInfo(); }