199 lines
5.6 KiB
C++
199 lines
5.6 KiB
C++
// gKeyMapper daemon - raw HID access to the keyboard's vendor interface.
|
|
//
|
|
// Uses the kernel's hidraw interface instead of libusb. hidraw provides
|
|
// non-exclusive access: the kernel driver (usbhid) stays attached, and other
|
|
// tools (OpenRGB, etc.) can open the same device at the same time. This
|
|
// avoids the exclusive claim that blocked OpenRGB from detecting the keyboard.
|
|
|
|
#include "g910_device.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <cerrno>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <poll.h>
|
|
#include <sys/ioctl.h>
|
|
#include <linux/hidraw.h>
|
|
|
|
namespace gkeymapper::daemon {
|
|
|
|
GKeyDevice::~GKeyDevice() {
|
|
close();
|
|
}
|
|
|
|
// Scan /sys/class/hidraw for a node whose HID_ID matches vendor:product and
|
|
// whose sysfs path points at interface 1 (the vendor-specific one). Returns
|
|
// the minor number (>= 0) or -1.
|
|
int GKeyDevice::find_hidraw_node(uint16_t vendor, uint16_t product) {
|
|
char id_str[64];
|
|
std::snprintf(id_str, sizeof(id_str), "0003:%08X:%08X", (unsigned)vendor, (unsigned)product);
|
|
|
|
for (int i = 0; i < 64; ++i) {
|
|
char path[512];
|
|
std::snprintf(path, sizeof(path), "/sys/class/hidraw/hidraw%d/device/uevent", i);
|
|
FILE* f = std::fopen(path, "r");
|
|
if (!f) {
|
|
continue;
|
|
}
|
|
char buf[2048];
|
|
size_t n = std::fread(buf, 1, sizeof(buf) - 1, f);
|
|
std::fclose(f);
|
|
buf[n] = '\0';
|
|
|
|
// Check HID_ID matches our vendor:product.
|
|
if (std::strstr(buf, id_str) == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
// Check the sysfs path contains ":1." (interface 1, the vendor one).
|
|
// The readlink gives us the full device path.
|
|
char linkpath[512];
|
|
std::snprintf(linkpath, sizeof(linkpath), "/sys/class/hidraw/hidraw%d", i);
|
|
char resolved[1024];
|
|
ssize_t rlen = readlink(linkpath, resolved, sizeof(resolved) - 1);
|
|
if (rlen < 0) {
|
|
continue;
|
|
}
|
|
resolved[rlen] = '\0';
|
|
|
|
// The resolved path contains the interface number in the form
|
|
// <device>:<config>.<interface>. Interface 1 (vendor-specific) has
|
|
// config 1, so the path contains ":1.1" (altsetting 1) or ":1.0".
|
|
// We check the uevent's HID_PHYS instead, which contains "input1"
|
|
// for interface 1 and "input0" for interface 0.
|
|
if (std::strstr(buf, "input1") == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
bool GKeyDevice::open(bool quiet) {
|
|
if (fd_ >= 0) {
|
|
return true;
|
|
}
|
|
|
|
// Find a supported profile first (by vendor/product).
|
|
// We iterate all profiles and try to find a matching hidraw node.
|
|
const auto& profs = profiles();
|
|
const DeviceProfile* matched = nullptr;
|
|
int node = -1;
|
|
|
|
for (const DeviceProfile& p : profs) {
|
|
node = find_hidraw_node(p.vendor, p.product);
|
|
if (node >= 0) {
|
|
matched = &p;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!matched || node < 0) {
|
|
if (!quiet) {
|
|
std::fprintf(stderr, "gkeymapper-daemon: no supported keyboard found\n");
|
|
}
|
|
return false;
|
|
}
|
|
profile_ = matched;
|
|
|
|
char devpath[64];
|
|
std::snprintf(devpath, sizeof(devpath), "/dev/hidraw%d", node);
|
|
|
|
fd_ = ::open(devpath, O_RDWR | O_NONBLOCK);
|
|
if (fd_ < 0) {
|
|
if (!quiet) {
|
|
std::fprintf(stderr, "gkeymapper-daemon: could not open %s (%s)\n",
|
|
devpath, std::strerror(errno));
|
|
}
|
|
profile_ = nullptr;
|
|
return false;
|
|
}
|
|
|
|
// Disable the default G-key binding (G-keys mapped to F-keys) if needed.
|
|
// Done after open so disable_binding() can use the fd.
|
|
// (disable_binding is called separately by main.cpp)
|
|
|
|
std::fprintf(stderr, "gkeymapper-daemon: %s found (hidraw%d)\n",
|
|
profile_->name, node);
|
|
return true;
|
|
}
|
|
|
|
bool GKeyDevice::disable_binding() {
|
|
if (fd_ < 0 || !profile_) {
|
|
return false;
|
|
}
|
|
if (!profile_->disableBinding) {
|
|
return true;
|
|
}
|
|
|
|
for (const Report& command : profile_->disableCommands) {
|
|
// HID SET_REPORT (feature report). The first byte of the command
|
|
// is the report ID.
|
|
int len = static_cast<int>(command.size());
|
|
// HIDIOCSFEATURE takes a buffer where byte 0 is the report ID.
|
|
std::vector<uint8_t> buf(command.begin(), command.end());
|
|
int rc = ioctl(fd_, HIDIOCSFEATURE(len), buf.data());
|
|
if (rc < 0) {
|
|
std::fprintf(stderr, "gkeymapper-daemon: disable-binding ioctl failed (%s)\n",
|
|
std::strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Drain confirmation reports.
|
|
uint8_t scratch[64];
|
|
for (int i = 0; i < 6; ++i) {
|
|
if (read_report(scratch, sizeof(scratch), 200) <= 0) {
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int GKeyDevice::read_report(uint8_t* buffer, int length, int timeoutMs) {
|
|
if (fd_ < 0) {
|
|
return -1;
|
|
}
|
|
|
|
struct pollfd pfd;
|
|
pfd.fd = fd_;
|
|
pfd.events = POLLIN;
|
|
pfd.revents = 0;
|
|
|
|
int rc = poll(&pfd, 1, timeoutMs);
|
|
if (rc < 0) {
|
|
if (errno == EINTR) {
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
if (rc == 0) {
|
|
return 0; // timeout
|
|
}
|
|
if (!(pfd.revents & POLLIN)) {
|
|
return 0;
|
|
}
|
|
|
|
ssize_t n = ::read(fd_, buffer, length);
|
|
if (n < 0) {
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
return static_cast<int>(n);
|
|
}
|
|
|
|
void GKeyDevice::close() {
|
|
if (fd_ >= 0) {
|
|
::close(fd_);
|
|
}
|
|
fd_ = -1;
|
|
profile_ = nullptr;
|
|
}
|
|
|
|
} // namespace gkeymapper::daemon
|