4 Commits

Author SHA1 Message Date
c750fe0449 Fix OpenRGB compatibility: hidraw instead of libusb, add preinst for upgrade path 2026-10-27 19:05:46 +01:00
5410ed2ae5 0.1.2 2026-10-27 18:57:14 +01:00
f45596a7e3 Tools and Licence 2026-10-27 03:41:12 +01:00
da60aef44e README: use relative paths only 2026-10-18 23:03:47 +02:00
9 changed files with 315 additions and 159 deletions

6
.vscode/tasks.json vendored
View File

@@ -44,6 +44,12 @@
"type": "shell", "type": "shell",
"command": "make clean && make", "command": "make clean && make",
"problemMatcher": "$gcc" "problemMatcher": "$gcc"
},
{
"label": "make: deb",
"type": "shell",
"command": "make deb",
"problemMatcher": []
} }
] ]
} }

View File

@@ -10,10 +10,10 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
## Build / run / verify ## Build / run / verify
- `make` — builds the `gkeymapper` GUI, the `gkeymapper-daemon` and the `.mo` - `make` — builds the `gkeymapper` GUI, the `gkeymapper-daemon` and the `.mo`
catalogs. Needs `g++` (C++17), `make`, `gettext`, `libgtk-4-dev`, catalogs. Needs `g++` (C++17), `make`, `gettext`, `libgtk-4-dev`,
`libusb-1.0-0-dev`, `libxkbcommon-dev` and `libglib2.0-dev` (see README for `libxkbcommon-dev` and `libglib2.0-dev` (see README for
`apt install` on a fresh machine). `libglib2.0-dev` also provides `apt install` on a fresh machine). `libglib2.0-dev` also provides
`glib-compile-resources`, which generates the icons embedded into the GUI. `glib-compile-resources`, which generates the icons embedded into the GUI.
- `make daemon` — builds only `gkeymapper-daemon` (needs libusb, xkbcommon and - `make daemon` — builds only `gkeymapper-daemon` (needs xkbcommon and
gio-2.0, not GTK). gio-2.0, not GTK).
- `make translations` — only runs `msgfmt` on the existing `.po` files. - `make translations` — only runs `msgfmt` on the existing `.po` files.
- `make run` — builds, then runs with `GKEYMAPPER_LOCALEDIR=$PWD/locale` and - `make run` — builds, then runs with `GKEYMAPPER_LOCALEDIR=$PWD/locale` and
@@ -66,11 +66,9 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
`optionsdialog` and `popovers` (the popup input grab, see below). `optionsdialog` and `popovers` (the popup input grab, see below).
- The Makefile globs sources: `make` builds the GUI (`src/*.cpp` + `shared/*.cpp` - The Makefile globs sources: `make` builds the GUI (`src/*.cpp` + `shared/*.cpp`
+ the generated `build/gresources.c`, see Assets) **and** the daemon + the generated `build/gresources.c`, see Assets) **and** the daemon
(`daemon/*.cpp` + `shared/*.cpp`, needs `libusb-1.0-0-dev`, (`daemon/*.cpp` + `shared/*.cpp`, needs `libxkbcommon-dev` and `libglib2.0-dev`). `make daemon` builds only the daemon.
`libxkbcommon-dev` and `libglib2.0-dev`). `make daemon` builds only the daemon.
- The daemon is split into: `device_tables` (raw HID report tables per model plus - The daemon is split into: `device_tables` (raw HID report tables per model plus
`decode_gkey_state`, the report → pressed-bitmap decoder), `g910_device` (libusb `decode_gkey_state`, the report → pressed-bitmap decoder), `g910_device` (hidraw open/read/close), `uinput_keyboard` (virtual keyboard), `action`
open/detach/claim/read), `uinput_keyboard` (virtual keyboard), `action`
(dispatch `key`/`command`/`disabled`/`keyhold`), `config_watch` (inotify auto-reload of (dispatch `key`/`command`/`disabled`/`keyhold`), `config_watch` (inotify auto-reload of
the mappings file) and `ipc_dbus` (the GDBus service). `shared/keymap` does the the mappings file) and `ipc_dbus` (the GDBus service). `shared/keymap` does the
layout lookups for both sides. The hot path never touches the GUI: D-Bus is layout lookups for both sides. The hot path never touches the GUI: D-Bus is
@@ -100,7 +98,7 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
2 s while D-Bus and the config watch keep running. Never make it exit on a 2 s while D-Bus and the config watch keep running. Never make it exit on a
missing device — that would drop the bus name and make the GUI look like the missing device — that would drop the bus name and make the GUI look like the
daemon died. `GKeyDevice::open(quiet)` exists so those retries stay out of the daemon died. `GKeyDevice::open(quiet)` exists so those retries stay out of the
journal (libusb-level errors always log). journal (hidraw-level errors always log).
- `make install` (PREFIX default `/usr/local`) also installs the packaging - `make install` (PREFIX default `/usr/local`) also installs the packaging
files from `data/`: the udev rule, the `modules-load.d` fragment, the files from `data/`: the udev rule, the `modules-load.d` fragment, the
systemd **user** unit and the D-Bus activation file. Both contain an systemd **user** unit and the D-Bus activation file. Both contain an
@@ -389,9 +387,9 @@ over any installed file; the file lookup remains only as a safety net.
families, which must *not* decode as media keys (see the bit table above); families, which must *not* decode as media keys (see the bit table above);
- build the daemon with a **fake** `GKeyDevice`: keep the header, script - build the daemon with a **fake** `GKeyDevice`: keep the header, script
`open()`/`read_report()`, link `daemon/*.cpp` **without** `g910_device.cpp` `open()`/`read_report()`, link `daemon/*.cpp` **without** `g910_device.cpp`
and without libusb. That runs the real loop, so it covers "no keyboard at and without g910_device.cpp. That runs the real loop, so it covers "no keyboard at
start", "device lost" and the reopen, emits real D-Bus signals and never start", "device lost" and the reopen, emits real D-Bus signals and never
claims the real device — nothing touches the keyboard and no key is injected opens the real device — nothing touches the keyboard and no key is injected
into the focused window. Use a config with `command:` mappings only (a helper into the focused window. Use a config with `command:` mappings only (a helper
script that dumps its environment is also how you check that a `command` script that dumps its environment is also how you check that a `command`
action sees `DISPLAY`); with `key:` mappings the fake would inject keys into action sees `DISPLAY`); with `key:` mappings the fake would inject keys into
@@ -401,7 +399,7 @@ over any installed file; the file lookup remains only as a safety net.
the fake `g910_device.h` in an include directory. `#include "g910_device.h"` the fake `g910_device.h` in an include directory. `#include "g910_device.h"`
searches the *including file's* directory first, so compiling the real searches the *including file's* directory first, so compiling the real
`daemon/main.cpp` would always pick up the real header (its inline `daemon/main.cpp` would always pick up the real header (its inline
`is_open()` needs libusb). `is_open()` needs hidraw).
- **Give the fake its own session bus** (`dbus-daemon --session --address= - **Give the fake its own session bus** (`dbus-daemon --session --address=
unix:path=/tmp/…/fake_bus --fork --print-pid=1`, then unix:path=/tmp/…/fake_bus --fork --print-pid=1`, then
`DBUS_SESSION_BUS_ADDRESS=unix:path=…` for the fake). Otherwise it refuses `DBUS_SESSION_BUS_ADDRESS=unix:path=…` for the fake). Otherwise it refuses

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Thomas Rager
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,7 +1,7 @@
# gKeyMapper - Build # gKeyMapper - Build
# Dependencies: g++, pkg-config, gettext (msgfmt, for the translations), # Dependencies: g++, pkg-config, gettext (msgfmt, for the translations),
# libgtk-4-dev, libusb-1.0-0-dev, libxkbcommon-dev # libgtk-4-dev, libxkbcommon-dev
# Installation: sudo apt install build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev # Installation: sudo apt install build-essential gettext libgtk-4-dev libxkbcommon-dev
CXX ?= g++ CXX ?= g++
CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra -Wpedantic CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra -Wpedantic
@@ -11,8 +11,6 @@ GLIB_COMPILE_RESOURCES ?= glib-compile-resources
GTK_CFLAGS := $(shell $(PKG_CONFIG) --cflags gtk4) GTK_CFLAGS := $(shell $(PKG_CONFIG) --cflags gtk4)
GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4) GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4)
USB_CFLAGS := $(shell $(PKG_CONFIG) --cflags libusb-1.0)
USB_LIBS := $(shell $(PKG_CONFIG) --libs libusb-1.0)
XKB_CFLAGS := $(shell $(PKG_CONFIG) --cflags xkbcommon) XKB_CFLAGS := $(shell $(PKG_CONFIG) --cflags xkbcommon)
XKB_LIBS := $(shell $(PKG_CONFIG) --libs xkbcommon) XKB_LIBS := $(shell $(PKG_CONFIG) --libs xkbcommon)
GIO_CFLAGS := $(shell $(PKG_CONFIG) --cflags gio-2.0) GIO_CFLAGS := $(shell $(PKG_CONFIG) --cflags gio-2.0)
@@ -32,7 +30,7 @@ DAEMON_HEADERS := $(wildcard $(DAEMON_DIR)/*.h) $(wildcard $(SHARED_DIR)/*.h)
GUI_RES := build/gresources.c GUI_RES := build/gresources.c
GUI_RES_SRC := data/icons.gresource.xml GUI_RES_SRC := data/icons.gresource.xml
PREFIX ?= /usr/local PREFIX ?= /usr/local
DEB_VERSION ?= 0.1.1 DEB_VERSION ?= 0.1.2
DEB_ARCH := $(shell dpkg --print-architecture 2>/dev/null || echo amd64) DEB_ARCH := $(shell dpkg --print-architecture 2>/dev/null || echo amd64)
BINDIR := $(PREFIX)/bin BINDIR := $(PREFIX)/bin
LOCALEDIR := $(PREFIX)/share/locale LOCALEDIR := $(PREFIX)/share/locale
@@ -70,7 +68,7 @@ $(TARGET): $(GUI_SOURCES) $(GUI_HEADERS) $(GUI_RES)
daemon: $(DAEMON) daemon: $(DAEMON)
$(DAEMON): $(DAEMON_SOURCES) $(DAEMON_HEADERS) $(DAEMON): $(DAEMON_SOURCES) $(DAEMON_HEADERS)
$(CXX) $(CXXFLAGS) $(USB_CFLAGS) $(XKB_CFLAGS) $(GIO_CFLAGS) -o $@ $(DAEMON_SOURCES) $(USB_LIBS) $(XKB_LIBS) $(GIO_LIBS) $(CXX) $(CXXFLAGS) $(XKB_CFLAGS) $(GIO_CFLAGS) -o $@ $(DAEMON_SOURCES) $(XKB_LIBS) $(GIO_LIBS)
translations: $(MO_FILES) translations: $(MO_FILES)

View File

@@ -3,7 +3,6 @@
![C++17](https://img.shields.io/badge/C%2B%2B-17-00599C?logo=cplusplus&logoColor=white) ![C++17](https://img.shields.io/badge/C%2B%2B-17-00599C?logo=cplusplus&logoColor=white)
![GTK 4](https://img.shields.io/badge/GTK-4-7FE719?logo=gtk&logoColor=black) ![GTK 4](https://img.shields.io/badge/GTK-4-7FE719?logo=gtk&logoColor=black)
![GLib/GIO](https://img.shields.io/badge/GLib%2FGIO-D--Bus-4A86CF) ![GLib/GIO](https://img.shields.io/badge/GLib%2FGIO-D--Bus-4A86CF)
![libusb](https://img.shields.io/badge/libusb-1.0-c8a200)
![xkbcommon](https://img.shields.io/badge/xkbcommon-layout--aware-6E4C13) ![xkbcommon](https://img.shields.io/badge/xkbcommon-layout--aware-6E4C13)
![i18n](https://img.shields.io/badge/i18n-de%20%7C%20en%20%7C%20es%20%7C%20fr-blueviolet) ![i18n](https://img.shields.io/badge/i18n-de%20%7C%20en%20%7C%20es%20%7C%20fr-blueviolet)
![systemd](https://img.shields.io/badge/systemd-user%20service-30D475?logo=systemd&logoColor=white) ![systemd](https://img.shields.io/badge/systemd-user%20service-30D475?logo=systemd&logoColor=white)
@@ -32,7 +31,6 @@ Documentation and source comments are written in English.
- pkg-config - pkg-config
- gettext (for translations, `msgfmt`) - gettext (for translations, `msgfmt`)
- GTK 4 development files - GTK 4 development files
- libusb 1.0 development files (for the daemon)
- libxkbcommon development files (for the daemon's layout handling) - libxkbcommon development files (for the daemon's layout handling)
- GLib/GIO development files (for the GUI ↔ daemon D-Bus channel; `libgtk-4-dev` - GLib/GIO development files (for the GUI ↔ daemon D-Bus channel; `libgtk-4-dev`
already pulls them in, but they are needed for `make daemon` on its own) already pulls them in, but they are needed for `make daemon` on its own)
@@ -41,21 +39,21 @@ Documentation and source comments are written in English.
```bash ```bash
sudo apt update sudo apt update
sudo apt install -y build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev libglib2.0-dev sudo apt install -y build-essential gettext libgtk-4-dev libxkbcommon-dev libglib2.0-dev
``` ```
Afterwards `pkg-config --modversion gtk4`, `pkg-config --modversion libusb-1.0` Afterwards `pkg-config --modversion gtk4` and `pkg-config --modversion xkbcommon`
and `pkg-config --modversion gio-2.0` should print versions. should print versions.
## Setting up the project ## Setting up the project
Change to the project directory: Change to the project directory:
```bash ```bash
cd /home/trager/Nextcloud/Projekte/C++/gKeyMapper cd gKeyMapper
``` ```
(Alternatively clone the repository or copy the project folder to your own location.) (Use the path where you cloned or copied the repository.)
## Build ## Build
@@ -181,7 +179,8 @@ The daemon reads G1–G9 from the keyboard and replays the configured action
./gkeymapper-daemon /path/to.conf # or an explicit file ./gkeymapper-daemon /path/to.conf # or an explicit file
``` ```
- It reaches the keyboard through libusb (raw USB HID, vendor interface 1) and - It reads the keyboard's vendor interface through the kernel's hidraw
interface (interface 1) and
creates a virtual keyboard via `/dev/uinput`. Both need access for your user; creates a virtual keyboard via `/dev/uinput`. Both need access for your user;
`make install` ships a udev rule (`TAG+="uaccess"`) for that. On Linux Mint the `make install` ships a udev rule (`TAG+="uaccess"`) for that. On Linux Mint the
device already has such an ACL, otherwise install the rule or add your user to device already has such an ACL, otherwise install the rule or add your user to
@@ -391,7 +390,7 @@ shared/keymap.{h,cpp} # layout-aware key lookups (xkbcommon), GUI + daemon
shared/ipc_ids.h # app id + D-Bus names shared by GUI and daemon shared/ipc_ids.h # app id + D-Bus names shared by GUI and daemon
daemon/main.cpp # event loop: read G-keys, run the action daemon/main.cpp # event loop: read G-keys, run the action
daemon/device_tables.{h,cpp} # per-model raw HID report tables (G910) daemon/device_tables.{h,cpp} # per-model raw HID report tables (G910)
daemon/g910_device.{h,cpp} # libusb access to the vendor interface daemon/g910_device.{h,cpp} # hidraw access to the vendor interface
daemon/uinput_keyboard.{h,cpp}# virtual keyboard for "key" mappings daemon/uinput_keyboard.{h,cpp}# virtual keyboard for "key" mappings
daemon/action.{h,cpp} # dispatch "key" / "command" / "disabled" / "keyhold" daemon/action.{h,cpp} # dispatch "key" / "command" / "disabled" / "keyhold"
daemon/config_watch.{h,cpp} # inotify watch on mappings.conf (auto-reload) daemon/config_watch.{h,cpp} # inotify watch on mappings.conf (auto-reload)

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 "g910_device.h"
#include <cstdio> #include <cstdio>
#include <cstdlib>
// libusb.h uses zero-length arrays, which -Wpedantic flags. #include <cstring>
#pragma GCC diagnostic push #include <cerrno>
#pragma GCC diagnostic ignored "-Wpedantic" #include <fcntl.h>
#include <libusb.h> #include <unistd.h>
#pragma GCC diagnostic pop #include <poll.h>
#include <sys/ioctl.h>
#include <linux/hidraw.h>
namespace gkeymapper::daemon { namespace gkeymapper::daemon {
@@ -16,108 +23,106 @@ GKeyDevice::~GKeyDevice() {
close(); 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) { bool GKeyDevice::open(bool quiet) {
if (handle_) { if (fd_ >= 0) {
return true; return true;
} }
if (libusb_init(&context_) != 0) { // Find a supported profile first (by vendor/product).
std::fprintf(stderr, "gkeymapper-daemon: libusb_init failed\n"); // We iterate all profiles and try to find a matching hidraw node.
return false; const auto& profs = profiles();
} const DeviceProfile* matched = nullptr;
int node = -1;
libusb_device** devices = nullptr; for (const DeviceProfile& p : profs) {
ssize_t count = libusb_get_device_list(context_, &devices); node = find_hidraw_node(p.vendor, p.product);
if (count < 0) { if (node >= 0) {
std::fprintf(stderr, "gkeymapper-daemon: no USB devices\n"); matched = &p;
close(); break;
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);
} }
} }
if (!target) { if (!matched || node < 0) {
if (!quiet) { if (!quiet) {
std::fprintf(stderr, "gkeymapper-daemon: no supported keyboard found\n"); std::fprintf(stderr, "gkeymapper-daemon: no supported keyboard found\n");
} }
libusb_free_device_list(devices, 1);
close();
return false; 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) { if (!quiet) {
std::fprintf(stderr, "gkeymapper-daemon: could not open %s (permissions?)\n", std::fprintf(stderr, "gkeymapper-daemon: could not open %s (%s)\n",
profile_->name); devpath, std::strerror(errno));
} }
libusb_free_device_list(devices, 1); profile_ = nullptr;
close();
return false;
}
libusb_free_device_list(devices, 1);
if (!find_endpoint()) {
close();
return false; return false;
} }
// Detach and claim the vendor interface, like the reference driver. // Disable the default G-key binding (G-keys mapped to F-keys) if needed.
if (libusb_kernel_driver_active(handle_, profile_->interfaceNumber) == 1) { // Done after open so disable_binding() can use the fd.
if (libusb_detach_kernel_driver(handle_, profile_->interfaceNumber) == 0) { // (disable_binding is called separately by main.cpp)
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;
std::fprintf(stderr, "gkeymapper-daemon: %s found (endpoint 0x%02x)\n", std::fprintf(stderr, "gkeymapper-daemon: %s found (hidraw%d)\n",
profile_->name, endpoint_); profile_->name, node);
return true; 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() { bool GKeyDevice::disable_binding() {
if (!handle_ || !profile_) { if (fd_ < 0 || !profile_) {
return false; return false;
} }
if (!profile_->disableBinding) { if (!profile_->disableBinding) {
@@ -125,19 +130,20 @@ bool GKeyDevice::disable_binding() {
} }
for (const Report& command : profile_->disableCommands) { for (const Report& command : profile_->disableCommands) {
int written = libusb_control_transfer(handle_, 0x21, 0x09, 0x0212, // HID SET_REPORT (feature report). The first byte of the command
profile_->interfaceNumber, // is the report ID.
const_cast<unsigned char*>(command.data()), int len = static_cast<int>(command.size());
static_cast<uint16_t>(command.size()), // HIDIOCSFEATURE takes a buffer where byte 0 is the report ID.
1000); std::vector<uint8_t> buf(command.begin(), command.end());
if (written < 0) { int rc = ioctl(fd_, HIDIOCSFEATURE(len), buf.data());
std::fprintf(stderr, "gkeymapper-daemon: disable-binding write failed (%d)\n", if (rc < 0) {
written); std::fprintf(stderr, "gkeymapper-daemon: disable-binding ioctl failed (%s)\n",
std::strerror(errno));
return false; return false;
} }
} }
// Drain the confirmation reports so they do not show up as key events. // Drain confirmation reports.
uint8_t scratch[64]; uint8_t scratch[64];
for (int i = 0; i < 6; ++i) { for (int i = 0; i < 6; ++i) {
if (read_report(scratch, sizeof(scratch), 200) <= 0) { 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) { int GKeyDevice::read_report(uint8_t* buffer, int length, int timeoutMs) {
if (!handle_) { if (fd_ < 0) {
return -1; return -1;
} }
int transferred = 0;
int rc = libusb_interrupt_transfer(handle_, endpoint_, buffer, length, struct pollfd pfd;
&transferred, timeoutMs); pfd.fd = fd_;
if (rc == 0) { pfd.events = POLLIN;
return transferred; pfd.revents = 0;
}
if (rc == LIBUSB_ERROR_TIMEOUT) { int rc = poll(&pfd, 1, timeoutMs);
if (rc < 0) {
if (errno == EINTR) {
return 0; return 0;
} }
return rc; // negative 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() { void GKeyDevice::close() {
if (handle_) { if (fd_ >= 0) {
if (claimed_interface_ >= 0) { ::close(fd_);
libusb_release_interface(handle_, claimed_interface_);
} }
if (detached_kernel_driver_) { fd_ = -1;
libusb_attach_kernel_driver(handle_, profile_->interfaceNumber);
}
libusb_close(handle_);
}
handle_ = nullptr;
if (context_) {
libusb_exit(context_);
context_ = nullptr;
}
claimed_interface_ = -1;
detached_kernel_driver_ = false;
profile_ = nullptr; 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 #ifndef GKEYMAPPER_DAEMON_GKEY_DEVICE_H
#define GKEYMAPPER_DAEMON_GKEY_DEVICE_H #define GKEYMAPPER_DAEMON_GKEY_DEVICE_H
@@ -6,9 +10,6 @@
#include "device_tables.h" #include "device_tables.h"
struct libusb_context;
struct libusb_device_handle;
namespace gkeymapper::daemon { namespace gkeymapper::daemon {
class GKeyDevice { class GKeyDevice {
@@ -19,15 +20,15 @@ public:
GKeyDevice(const GKeyDevice&) = delete; GKeyDevice(const GKeyDevice&) = delete;
GKeyDevice& operator=(const GKeyDevice&) = delete; GKeyDevice& operator=(const GKeyDevice&) = delete;
// Finds a supported keyboard, opens it and detaches the kernel driver from // Finds a supported keyboard and opens its hidraw node. Returns false (and
// the vendor interface. Returns false (and logs) if none is found. // logs) if none is found.
// //
// `quiet` silences the "cannot use the keyboard" lines only, which the // `quiet` silences the "cannot use the keyboard" lines only, which the
// hot-plug retry uses so an unplugged keyboard does not flood the journal // 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); bool open(bool quiet = false);
void close(); 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. // Sends the "stop mapping G-keys to F-keys" command if the model needs it.
bool disable_binding(); bool disable_binding();
@@ -39,14 +40,10 @@ public:
const DeviceProfile* profile() const { return profile_; } const DeviceProfile* profile() const { return profile_; }
private: private:
bool find_endpoint(); int find_hidraw_node(uint16_t vendor, uint16_t product);
libusb_context* context_ = nullptr; int fd_ = -1;
libusb_device_handle* handle_ = nullptr;
const DeviceProfile* profile_ = nullptr; const DeviceProfile* profile_ = nullptr;
uint8_t endpoint_ = 0;
int claimed_interface_ = -1;
bool detached_kernel_driver_ = false;
}; };
} // namespace gkeymapper::daemon } // namespace gkeymapper::daemon

View File

@@ -3,7 +3,7 @@
# Usage: scripts/make-deb.sh [version] [arch] # Usage: scripts/make-deb.sh [version] [arch]
set -e set -e
VERSION="${1:-0.1.1}" VERSION="${1:-0.1.2}"
ARCH="${2:-$(dpkg --print-architecture 2>/dev/null || echo amd64)}" ARCH="${2:-$(dpkg --print-architecture 2>/dev/null || echo amd64)}"
PACKAGE="gkeymapper" PACKAGE="gkeymapper"
STAGING="build/deb" STAGING="build/deb"
@@ -29,13 +29,58 @@ Section: utils
Installed-Size: $INSTALLED_SIZE Installed-Size: $INSTALLED_SIZE
Priority: optional Priority: optional
Architecture: $ARCH Architecture: $ARCH
Depends: libc6, libgtk-4-1, libusb-1.0-0, libxkbcommon0, libglib2.0-0, systemd, udev Depends: libc6, libgtk-4-1, libxkbcommon0, libglib2.0-0, systemd, udev
Maintainer: gKeyMapper <gkeymapper@rager.dev> Maintainer: gKeyMapper <gkeymapper@rager.dev>
Description: Remap G1-G9 keys of the Logitech G910 Description: Remap G1-G9 keys of the Logitech G910
A GTK4 GUI plus a background daemon for remapping the G1-G9 keys A GTK4 GUI plus a background daemon for remapping the G1-G9 keys
of a Logitech G910 keyboard to keys or commands. of a Logitech G910 keyboard to keys or commands.
EOF EOF
# preinst: stop any running daemon and re-attach the kernel driver BEFORE
# upgrading. The old (libusb-based) daemon detached the kernel driver from
# interface 1; if it was not stopped gracefully, the driver stays detached
# and the new (hidraw-based) daemon cannot find the device.
cat > "$STAGING/DEBIAN/preinst" <<'EOF'
#!/bin/sh
set -e
case "$1" in
install|upgrade)
# Stop any running daemon (old or new).
for pid in $(pgrep -f gkeymapper-daemon 2>/dev/null); do
kill "$pid" 2>/dev/null || true
done
sleep 1
# Re-attach the kernel driver to interface 1 if it was detached by
# an older (libusb-based) daemon.
for _dev in /sys/bus/usb/devices/*; do
_vendor=$(cat "$_dev/idVendor" 2>/dev/null) || continue
_product=$(cat "$_dev/idProduct" 2>/dev/null) || continue
case "$_vendor" in
046d)
case "$_product" in
c32b|c335)
for _iface in "$_dev"/*:*; do
case "$_iface" in
*:1.*)
_name=$(basename "$_iface")
if [ -w /sys/bus/usb/drivers/usbhid/bind ]; then
echo "$_name" > /sys/bus/usb/drivers/usbhid/bind 2>/dev/null || true
fi
;;
esac
done
;;
esac
;;
esac
done
;;
esac
exit 0
EOF
chmod 755 "$STAGING/DEBIAN/preinst"
# postinst: reload udev/systemd and enable the user daemon. # postinst: reload udev/systemd and enable the user daemon.
cat > "$STAGING/DEBIAN/postinst" <<'EOF' cat > "$STAGING/DEBIAN/postinst" <<'EOF'
#!/bin/sh #!/bin/sh
@@ -48,6 +93,36 @@ case "$1" in
udevadm trigger --subsystem-match=input 2>/dev/null || true udevadm trigger --subsystem-match=input 2>/dev/null || true
fi fi
# Remove stale service file from old "make install" (PREFIX=/usr/local)
# so the new one from /usr takes precedence.
rm -f /usr/local/lib/systemd/user/gkeymapper-daemon.service
# Re-attach the kernel driver to interface 1 if it was detached by an
# older (libusb-based) daemon. Without this the new (hidraw-based)
# daemon cannot find /dev/hidraw for interface 1.
for _dev in /sys/bus/usb/devices/*; do
_vendor=$(cat "$_dev/idVendor" 2>/dev/null) || continue
_product=$(cat "$_dev/idProduct" 2>/dev/null) || continue
case "$_vendor" in
046d)
case "$_product" in
c32b|c335)
for _iface in "$_dev"/*:*; do
case "$_iface" in
*:1.*)
_name=$(basename "$_iface")
if [ -w /sys/bus/usb/drivers/usbhid/bind ]; then
echo "$_name" > /sys/bus/usb/drivers/usbhid/bind 2>/dev/null || true
fi
;;
esac
done
;;
esac
;;
esac
done
# Helper: run a command as a given user with the D-Bus session bus # Helper: run a command as a given user with the D-Bus session bus
# available. "systemctl --user" needs DBUS_SESSION_BUS_ADDRESS, which # available. "systemctl --user" needs DBUS_SESSION_BUS_ADDRESS, which
# is not set in a plain "su -" login shell. # is not set in a plain "su -" login shell.
@@ -95,6 +170,56 @@ EOF
chmod 755 "$STAGING/DEBIAN/postinst" chmod 755 "$STAGING/DEBIAN/postinst"
# prerm: stop the daemon and re-attach the kernel driver before upgrading.
# The old (libusb-based) daemon detached the kernel driver from interface 1;
# if it was not stopped gracefully, the driver stays detached and the new
# (hidraw-based) daemon cannot find the device.
cat > "$STAGING/DEBIAN/prerm" <<'EOF'
#!/bin/sh
set -e
case "$1" in
remove|upgrade|deconfigure)
# Stop the daemon if running.
if [ -n "$SUDO_USER" ]; then
runuser -u "$SUDO_USER" -- systemctl --user stop gkeymapper-daemon >/dev/null 2>&1 || true
runuser -u "$SUDO_USER" -- systemctl --user daemon-reload >/dev/null 2>&1 || true
else
for _user in $(loginctl list-users --no-legend 2>/dev/null | awk '{print $2}'); do
runuser -u "$_user" -- systemctl --user stop gkeymapper-daemon >/dev/null 2>&1 || true
done
fi
# Re-attach the kernel driver to interface 1 if it was detached by
# an older (libusb-based) daemon. Without this, the new daemon
# (hidraw-based) cannot find /dev/hidraw for interface 1.
for _dev in /sys/bus/usb/devices/*; do
_vendor=$(cat "$_dev/idVendor" 2>/dev/null) || continue
_product=$(cat "$_dev/idProduct" 2>/dev/null) || continue
case "$_vendor" in
046d)
case "$_product" in
c32b|c335)
for _iface in "$_dev"/*:*; do
case "$_iface" in
*:1.*)
_name=$(basename "$_iface")
if [ -w /sys/bus/usb/drivers/usbhid/bind ]; then
echo "$_name" > /sys/bus/usb/drivers/usbhid/bind 2>/dev/null || true
fi
;;
esac
done
;;
esac
;;
esac
done
;;
esac
exit 0
EOF
chmod 755 "$STAGING/DEBIAN/prerm"
# postrm: disable the user daemon on removal. # postrm: disable the user daemon on removal.
cat > "$STAGING/DEBIAN/postrm" <<'EOF' cat > "$STAGING/DEBIAN/postrm" <<'EOF'
#!/bin/sh #!/bin/sh

View File

@@ -13,7 +13,7 @@ namespace gkeymapper::ipc {
inline constexpr const char* kAppId = "dev.rager.gkeymapper"; inline constexpr const char* kAppId = "dev.rager.gkeymapper";
// Reported by GetStatus and shown in the About dialog. // Reported by GetStatus and shown in the About dialog.
inline constexpr const char* kVersion = "0.1.1"; inline constexpr const char* kVersion = "0.1.2";
// The daemon owns this name; the GUI talks to it via the object below. // The daemon owns this name; the GUI talks to it via the object below.
// //