This commit is contained in:
2026-10-27 18:57:14 +01:00
parent f45596a7e3
commit 5410ed2ae5
9 changed files with 258 additions and 178 deletions

View File

@@ -1,14 +1,21 @@
// gKeyMapper daemon - raw USB HID access to the keyboard's vendor interface.
// 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>
// libusb.h uses zero-length arrays, which -Wpedantic flags.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#include <libusb.h>
#pragma GCC diagnostic pop
#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 {
@@ -16,108 +23,106 @@ 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 (handle_) {
if (fd_ >= 0) {
return true;
}
if (libusb_init(&context_) != 0) {
std::fprintf(stderr, "gkeymapper-daemon: libusb_init failed\n");
return false;
}
// 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;
libusb_device** devices = nullptr;
ssize_t count = libusb_get_device_list(context_, &devices);
if (count < 0) {
std::fprintf(stderr, "gkeymapper-daemon: no USB devices\n");
close();
return false;
}
libusb_device* target = nullptr;
for (ssize_t i = 0; i < count && !target; ++i) {
libusb_device_descriptor desc{};
if (libusb_get_device_descriptor(devices[i], &desc) != 0) {
continue;
}
if (find_profile(desc.idVendor, desc.idProduct) != nullptr) {
target = devices[i];
profile_ = find_profile(desc.idVendor, desc.idProduct);
for (const DeviceProfile& p : profs) {
node = find_hidraw_node(p.vendor, p.product);
if (node >= 0) {
matched = &p;
break;
}
}
if (!target) {
if (!matched || node < 0) {
if (!quiet) {
std::fprintf(stderr, "gkeymapper-daemon: no supported keyboard found\n");
}
libusb_free_device_list(devices, 1);
close();
return false;
}
profile_ = matched;
if (libusb_open(target, &handle_) != 0) {
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 (permissions?)\n",
profile_->name);
std::fprintf(stderr, "gkeymapper-daemon: could not open %s (%s)\n",
devpath, std::strerror(errno));
}
libusb_free_device_list(devices, 1);
close();
return false;
}
libusb_free_device_list(devices, 1);
if (!find_endpoint()) {
close();
profile_ = nullptr;
return false;
}
// Detach and claim the vendor interface, like the reference driver.
if (libusb_kernel_driver_active(handle_, profile_->interfaceNumber) == 1) {
if (libusb_detach_kernel_driver(handle_, profile_->interfaceNumber) == 0) {
detached_kernel_driver_ = true;
}
}
if (libusb_claim_interface(handle_, profile_->interfaceNumber) != 0) {
if (!quiet) {
std::fprintf(stderr, "gkeymapper-daemon: could not claim interface %d\n",
profile_->interfaceNumber);
}
close();
return false;
}
claimed_interface_ = profile_->interfaceNumber;
// 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 (endpoint 0x%02x)\n",
profile_->name, endpoint_);
std::fprintf(stderr, "gkeymapper-daemon: %s found (hidraw%d)\n",
profile_->name, node);
return true;
}
bool GKeyDevice::find_endpoint() {
libusb_device* device = libusb_get_device(handle_);
libusb_config_descriptor* config = nullptr;
if (libusb_get_config_descriptor(device, profile_->configuration, &config) != 0) {
return false;
}
bool found = false;
for (int i = 0; i < config->bNumInterfaces && !found; ++i) {
const libusb_interface& iface = config->interface[i];
for (int a = 0; a < iface.num_altsetting && !found; ++a) {
const libusb_interface_descriptor& desc = iface.altsetting[a];
if (desc.bInterfaceNumber == profile_->interfaceNumber &&
desc.bAlternateSetting == profile_->altSetting &&
desc.bNumEndpoints > 0) {
endpoint_ = desc.endpoint[0].bEndpointAddress;
found = true;
}
}
}
libusb_free_config_descriptor(config);
return found;
}
bool GKeyDevice::disable_binding() {
if (!handle_ || !profile_) {
if (fd_ < 0 || !profile_) {
return false;
}
if (!profile_->disableBinding) {
@@ -125,19 +130,20 @@ bool GKeyDevice::disable_binding() {
}
for (const Report& command : profile_->disableCommands) {
int written = libusb_control_transfer(handle_, 0x21, 0x09, 0x0212,
profile_->interfaceNumber,
const_cast<unsigned char*>(command.data()),
static_cast<uint16_t>(command.size()),
1000);
if (written < 0) {
std::fprintf(stderr, "gkeymapper-daemon: disable-binding write failed (%d)\n",
written);
// 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 the confirmation reports so they do not show up as key events.
// Drain confirmation reports.
uint8_t scratch[64];
for (int i = 0; i < 6; ++i) {
if (read_report(scratch, sizeof(scratch), 200) <= 0) {
@@ -148,38 +154,44 @@ bool GKeyDevice::disable_binding() {
}
int GKeyDevice::read_report(uint8_t* buffer, int length, int timeoutMs) {
if (!handle_) {
if (fd_ < 0) {
return -1;
}
int transferred = 0;
int rc = libusb_interrupt_transfer(handle_, endpoint_, buffer, length,
&transferred, timeoutMs);
if (rc == 0) {
return transferred;
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 == LIBUSB_ERROR_TIMEOUT) {
if (rc == 0) {
return 0; // timeout
}
if (!(pfd.revents & POLLIN)) {
return 0;
}
return rc; // negative
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 (handle_) {
if (claimed_interface_ >= 0) {
libusb_release_interface(handle_, claimed_interface_);
}
if (detached_kernel_driver_) {
libusb_attach_kernel_driver(handle_, profile_->interfaceNumber);
}
libusb_close(handle_);
if (fd_ >= 0) {
::close(fd_);
}
handle_ = nullptr;
if (context_) {
libusb_exit(context_);
context_ = nullptr;
}
claimed_interface_ = -1;
detached_kernel_driver_ = false;
fd_ = -1;
profile_ = nullptr;
}

View File

@@ -1,4 +1,8 @@
// gKeyMapper daemon - raw USB HID access to the keyboard's vendor interface.
// 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, so other tools (OpenRGB, etc.) can talk to the same
// device at the same time. The kernel driver (usbhid) stays attached.
#ifndef GKEYMAPPER_DAEMON_GKEY_DEVICE_H
#define GKEYMAPPER_DAEMON_GKEY_DEVICE_H
@@ -6,9 +10,6 @@
#include "device_tables.h"
struct libusb_context;
struct libusb_device_handle;
namespace gkeymapper::daemon {
class GKeyDevice {
@@ -19,15 +20,15 @@ public:
GKeyDevice(const GKeyDevice&) = delete;
GKeyDevice& operator=(const GKeyDevice&) = delete;
// Finds a supported keyboard, opens it and detaches the kernel driver from
// the vendor interface. Returns false (and logs) if none is found.
// Finds a supported keyboard and opens its hidraw node. Returns false (and
// logs) if none is found.
//
// `quiet` silences the "cannot use the keyboard" lines only, which the
// hot-plug retry uses so an unplugged keyboard does not flood the journal
// with one line every retry; libusb-level failures always log.
// with one line every retry.
bool open(bool quiet = false);
void close();
bool is_open() const { return handle_ != nullptr; }
bool is_open() const { return fd_ >= 0; }
// Sends the "stop mapping G-keys to F-keys" command if the model needs it.
bool disable_binding();
@@ -39,14 +40,10 @@ public:
const DeviceProfile* profile() const { return profile_; }
private:
bool find_endpoint();
int find_hidraw_node(uint16_t vendor, uint16_t product);
libusb_context* context_ = nullptr;
libusb_device_handle* handle_ = nullptr;
int fd_ = -1;
const DeviceProfile* profile_ = nullptr;
uint8_t endpoint_ = 0;
int claimed_interface_ = -1;
bool detached_kernel_driver_ = false;
};
} // namespace gkeymapper::daemon