Media keys working again
This commit is contained in:
@@ -44,6 +44,10 @@ void ActionRunner::run(const gkeymapper::Mapping& mapping) {
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRunner::tap_key(unsigned short code) {
|
||||
keyboard_.tap(code);
|
||||
}
|
||||
|
||||
void ActionRunner::run_command(const std::string& command) {
|
||||
if (command.empty()) {
|
||||
return;
|
||||
|
||||
@@ -18,6 +18,11 @@ public:
|
||||
|
||||
void run(const gkeymapper::Mapping& mapping);
|
||||
|
||||
// Replays one key press + release. Used by the media-key passthrough, which
|
||||
// is not a mapping: the keyboard reports mute/volume/play only on the
|
||||
// interface the daemon holds, so they are forwarded verbatim.
|
||||
void tap_key(unsigned short code);
|
||||
|
||||
private:
|
||||
void run_command(const std::string& command);
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
namespace gkeymapper::daemon {
|
||||
|
||||
namespace {
|
||||
@@ -36,6 +38,25 @@ std::vector<Report> g910DisableCommands() {
|
||||
return {report20({0x11, 0xff, 0x08, 0x2e, 0x01})};
|
||||
}
|
||||
|
||||
// Media keys: a 2-byte report, "02" followed by one bit per key ("02 00" is
|
||||
// "all released"). The bit assignment and the Linux codes they map to were
|
||||
// measured on a G910 Orion Spark (2026-10-18) by reading the hidraw node of the
|
||||
// vendor interface while the kernel driver owned it, and cross-checked against
|
||||
// the key codes the kernel emits for that interface (input/event* with
|
||||
// Phys …/input1). The keyboard reports these keys *only* there, so the daemon
|
||||
// has to replay them once it holds the interface (see README).
|
||||
std::vector<MediaKey> g910MediaKeys() {
|
||||
return {
|
||||
{0x01, KEY_NEXTSONG, "next track"},
|
||||
{0x02, KEY_PREVIOUSSONG, "previous track"},
|
||||
{0x04, KEY_STOPCD, "stop"},
|
||||
{0x08, KEY_PLAYPAUSE, "play/pause"},
|
||||
{0x10, KEY_VOLUMEUP, "volume up"},
|
||||
{0x20, KEY_VOLUMEDOWN, "volume down"},
|
||||
{0x40, KEY_MUTE, "mute"},
|
||||
};
|
||||
}
|
||||
|
||||
DeviceProfile make_g910(const char* name, uint16_t product, bool disableBinding) {
|
||||
DeviceProfile profile;
|
||||
profile.name = name;
|
||||
@@ -45,7 +66,8 @@ DeviceProfile make_g910(const char* name, uint16_t product, bool disableBinding)
|
||||
profile.interfaceNumber = 1;
|
||||
profile.altSetting = 0;
|
||||
profile.gKeyReports = g910GKeys();
|
||||
profile.releaseReport = report20({0x11, 0xff, 0x08, 0x00});
|
||||
profile.mediaPrefix = 0x02;
|
||||
profile.mediaKeys = g910MediaKeys();
|
||||
profile.disableBinding = disableBinding;
|
||||
if (disableBinding) {
|
||||
profile.disableCommands = g910DisableCommands();
|
||||
@@ -74,23 +96,67 @@ const DeviceProfile* find_profile(uint16_t vendor, uint16_t product) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int match_gkey(const DeviceProfile& profile, const uint8_t* data, int length) {
|
||||
if (length <= 0) {
|
||||
return -1;
|
||||
bool decode_gkey_state(const DeviceProfile& profile, const uint8_t* data, int length,
|
||||
uint32_t& pressed) {
|
||||
pressed = 0;
|
||||
if (data == nullptr || length <= 0 || profile.gKeyReports.empty()) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < profile.gKeyReports.size(); ++i) {
|
||||
const Report& report = profile.gKeyReports[i];
|
||||
if (static_cast<int>(report.size()) == length &&
|
||||
std::equal(report.begin(), report.end(), data)) {
|
||||
return static_cast<int>(i);
|
||||
|
||||
const Report& first = profile.gKeyReports.front();
|
||||
if (static_cast<int>(first.size()) != length) {
|
||||
return false; // a different report family, e.g. the 2-byte media keys
|
||||
}
|
||||
|
||||
// Bytes that are the same in every key report are that model's fixed header;
|
||||
// the bytes after it are the key bitmap (G1..G8 in byte 4, G9 in byte 5).
|
||||
size_t header = 0;
|
||||
for (; header < first.size(); ++header) {
|
||||
bool shared = true;
|
||||
for (const Report& report : profile.gKeyReports) {
|
||||
if (report.size() != first.size() || report[header] != first[header]) {
|
||||
shared = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!shared) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
if (header >= first.size()) {
|
||||
return false; // no bit to read: pressed and released cannot be told apart
|
||||
}
|
||||
for (size_t i = 0; i < header; ++i) {
|
||||
if (data[i] != first[i]) {
|
||||
return false; // disable/enable G-key command, memory key, LED answer
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t key = 0; key < profile.gKeyReports.size() && key < 32; ++key) {
|
||||
const Report& press = profile.gKeyReports[key];
|
||||
for (size_t i = header; i < press.size() && i < static_cast<size_t>(length); ++i) {
|
||||
if (press[i] != 0 && (data[i] & press[i]) == press[i]) {
|
||||
pressed |= 1u << key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_release_report(const DeviceProfile& profile, const uint8_t* data, int length) {
|
||||
return static_cast<int>(profile.releaseReport.size()) == length &&
|
||||
std::equal(profile.releaseReport.begin(), profile.releaseReport.end(), data);
|
||||
bool decode_media_key(const DeviceProfile& profile, const uint8_t* data, int length,
|
||||
uint32_t& bits) {
|
||||
bits = 0;
|
||||
if (data == nullptr || profile.mediaPrefix == 0 || length < 2) {
|
||||
return false;
|
||||
}
|
||||
if (data[0] != profile.mediaPrefix) {
|
||||
return false; // a macro-key report, a memory key, a command or LED answer
|
||||
}
|
||||
// The report is two bytes; a longer read would be the same report padded by
|
||||
// the endpoint, so only the bit byte is of interest.
|
||||
bits = data[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gkeymapper::daemon
|
||||
|
||||
@@ -14,6 +14,14 @@ namespace gkeymapper::daemon {
|
||||
// A raw HID report from the vendor interface.
|
||||
using Report = std::vector<uint8_t>;
|
||||
|
||||
// One media key: the bit it has in the media report and the Linux input code it
|
||||
// is replayed as.
|
||||
struct MediaKey {
|
||||
uint8_t bit = 0;
|
||||
uint16_t key = 0; // Linux KEY_* code
|
||||
const char* name = ""; // for the log line
|
||||
};
|
||||
|
||||
struct DeviceProfile {
|
||||
const char* name = "";
|
||||
uint16_t vendor = 0;
|
||||
@@ -22,9 +30,18 @@ struct DeviceProfile {
|
||||
int interfaceNumber = 1;
|
||||
int altSetting = 0;
|
||||
|
||||
// gKeyReports[i] is the press report for G(i+1).
|
||||
// Media keys (mute / volume / play) arrive as a short report: this prefix
|
||||
// byte followed by one bit per key, 0 when they are all released. 0 means
|
||||
// the model reports none. This is the only path the keyboard offers for
|
||||
// those keys, so while the daemon holds the interface the kernel does not see
|
||||
// them at all — the daemon replays them through its virtual keyboard.
|
||||
uint8_t mediaPrefix = 0;
|
||||
std::vector<MediaKey> mediaKeys;
|
||||
|
||||
// gKeyReports[i] is the report that has the bit of G(i+1) set. Both the
|
||||
// shared header bytes and the bit of each key are derived from these, so a
|
||||
// new model only needs its table (see decode_gkey_state).
|
||||
std::vector<Report> gKeyReports;
|
||||
Report releaseReport;
|
||||
|
||||
// Some models must be told to stop mapping G-keys to F-keys first.
|
||||
bool disableBinding = false;
|
||||
@@ -36,11 +53,24 @@ const std::vector<DeviceProfile>& profiles();
|
||||
// Matching profile for vendor/product, or nullptr.
|
||||
const DeviceProfile* find_profile(uint16_t vendor, uint16_t product);
|
||||
|
||||
// Index of the G key (0..N-1) for a report, or -1 if it is not a known press.
|
||||
int match_gkey(const DeviceProfile& profile, const uint8_t* data, int length);
|
||||
// Decodes a macro-key report into the keys it reports as pressed: bit 0 = G1 ..
|
||||
// bit 8 = G9. Returns false for every other report — the media keys have their
|
||||
// own decoder below, and memory keys, LED and command acknowledgements are
|
||||
// ignored on purpose.
|
||||
//
|
||||
// The report carries the state of all G keys, not a single key: one report with
|
||||
// several bits set means a chord, and "no bit set" is the release. The caller
|
||||
// compares consecutive states, which makes a held key (repeating report) fire
|
||||
// once and a chord fire every key it contains.
|
||||
bool decode_gkey_state(const DeviceProfile& profile, const uint8_t* data, int length,
|
||||
uint32_t& pressed);
|
||||
|
||||
// True if the report is the "all macro keys released" report.
|
||||
bool is_release_report(const DeviceProfile& profile, const uint8_t* data, int length);
|
||||
// Decodes a media-key report into its bits (bit set = key held), using the bit
|
||||
// table of the profile. `bits` is 0 for the release report, which is a valid
|
||||
// media report. Returns false for every other report, in particular for the
|
||||
// macro-key reports.
|
||||
bool decode_media_key(const DeviceProfile& profile, const uint8_t* data, int length,
|
||||
uint32_t& bits);
|
||||
|
||||
} // namespace gkeymapper::daemon
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ GKeyDevice::~GKeyDevice() {
|
||||
close();
|
||||
}
|
||||
|
||||
bool GKeyDevice::open() {
|
||||
bool GKeyDevice::open(bool quiet) {
|
||||
if (handle_) {
|
||||
return true;
|
||||
}
|
||||
@@ -47,15 +47,19 @@ bool GKeyDevice::open() {
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
std::fprintf(stderr, "gkeymapper-daemon: no supported keyboard found\n");
|
||||
if (!quiet) {
|
||||
std::fprintf(stderr, "gkeymapper-daemon: no supported keyboard found\n");
|
||||
}
|
||||
libusb_free_device_list(devices, 1);
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (libusb_open(target, &handle_) != 0) {
|
||||
std::fprintf(stderr, "gkeymapper-daemon: could not open %s (permissions?)\n",
|
||||
profile_->name);
|
||||
if (!quiet) {
|
||||
std::fprintf(stderr, "gkeymapper-daemon: could not open %s (permissions?)\n",
|
||||
profile_->name);
|
||||
}
|
||||
libusb_free_device_list(devices, 1);
|
||||
close();
|
||||
return false;
|
||||
@@ -74,8 +78,10 @@ bool GKeyDevice::open() {
|
||||
}
|
||||
}
|
||||
if (libusb_claim_interface(handle_, profile_->interfaceNumber) != 0) {
|
||||
std::fprintf(stderr, "gkeymapper-daemon: could not claim interface %d\n",
|
||||
profile_->interfaceNumber);
|
||||
if (!quiet) {
|
||||
std::fprintf(stderr, "gkeymapper-daemon: could not claim interface %d\n",
|
||||
profile_->interfaceNumber);
|
||||
}
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ public:
|
||||
|
||||
// Finds a supported keyboard, opens it and detaches the kernel driver from
|
||||
// the vendor interface. Returns false (and logs) if none is found.
|
||||
bool open();
|
||||
//
|
||||
// `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.
|
||||
bool open(bool quiet = false);
|
||||
void close();
|
||||
bool is_open() const { return handle_ != nullptr; }
|
||||
|
||||
|
||||
122
daemon/main.cpp
122
daemon/main.cpp
@@ -4,8 +4,8 @@
|
||||
// configured action and replays it ("key" via uinput, "command" via exec).
|
||||
//
|
||||
// Milestones: M0 (shared core), M1 (device bring-up/decoding), M2 (action
|
||||
// execution), M3 (config watching + lifecycle) and M4 (D-Bus GUI channel) are
|
||||
// done.
|
||||
// execution), M3 (config watching + lifecycle), M4 (D-Bus GUI channel) and
|
||||
// M5 (hot-plug, repeat/chord decoding, error reporting to the GUI) are done.
|
||||
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
@@ -33,6 +33,12 @@ void handle_reload(int /*signum*/) {
|
||||
g_reload = 1;
|
||||
}
|
||||
|
||||
// How often the daemon looks for the keyboard while it has none. The loop keeps
|
||||
// pumping D-Bus and the config watch in between, so the control plane stays
|
||||
// responsive and GetStatus can report that there is no device.
|
||||
constexpr gint64 kReopenIntervalUs = 2 * 1000 * 1000;
|
||||
constexpr int kReopenSleepMs = 200;
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -126,14 +132,26 @@ int main(int argc, char** argv) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!device.open()) {
|
||||
return 1;
|
||||
}
|
||||
device.disable_binding();
|
||||
profile = device.profile();
|
||||
service.notify_device_changed(true, profile != nullptr ? profile->name : "");
|
||||
// Opens the keyboard and tells the GUI. Returns false when there is none,
|
||||
// which is not an error: the session can start before the keyboard is
|
||||
// plugged, and a lost or re-plugged keyboard must not cost the bus name —
|
||||
// the GUI would see the daemon as gone and D-Bus activation would start a
|
||||
// second one for nothing.
|
||||
bool missingReported = false;
|
||||
auto open_device = [&](bool quiet) -> bool {
|
||||
if (!device.open(quiet)) {
|
||||
return false;
|
||||
}
|
||||
device.disable_binding();
|
||||
profile = device.profile();
|
||||
service.notify_device_changed(true, profile != nullptr ? profile->name : "");
|
||||
missingReported = false;
|
||||
return true;
|
||||
};
|
||||
|
||||
uint8_t buffer[64];
|
||||
uint32_t pressed = 0; // G keys reported as pressed by the previous report
|
||||
gint64 next_open = 0; // earliest time to look for the keyboard again
|
||||
while (g_running) {
|
||||
// Control plane first (bounded by the read timeout below), then the
|
||||
// keyboard. The loop wakes up at least every read timeout, which also
|
||||
@@ -145,34 +163,88 @@ int main(int argc, char** argv) {
|
||||
reload();
|
||||
}
|
||||
|
||||
if (!device.is_open()) {
|
||||
const gint64 now = g_get_monotonic_time();
|
||||
if (now >= next_open) {
|
||||
next_open = now + kReopenIntervalUs;
|
||||
// The first attempt of an absence explains itself, the retries
|
||||
// stay quiet: one line per unplug instead of one per retry.
|
||||
if (open_device(missingReported)) {
|
||||
continue; // read from it right away
|
||||
}
|
||||
if (!missingReported) {
|
||||
missingReported = true;
|
||||
std::fprintf(stderr,
|
||||
"gkeymapper-daemon: no usable keyboard, will keep "
|
||||
"looking\n");
|
||||
}
|
||||
}
|
||||
// Sleep briefly instead of the 500 ms read timeout, so a keyboard
|
||||
// that appears is picked up sooner and the loop still services the
|
||||
// control plane.
|
||||
g_usleep(kReopenSleepMs * 1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
int n = device.read_report(buffer, sizeof(buffer), 500);
|
||||
if (n == 0) {
|
||||
continue; // no report within the timeout
|
||||
}
|
||||
if (n < 0) {
|
||||
std::fprintf(stderr, "gkeymapper-daemon: read error (%d), reopening\n", n);
|
||||
// Lost the keyboard (unplugged, or the kernel took the interface
|
||||
// back). Report it and keep looking; the daemon stays up.
|
||||
std::fprintf(stderr, "gkeymapper-daemon: read error (%d), device lost\n", n);
|
||||
service.notify_device_changed(false, profile != nullptr ? profile->name : "");
|
||||
device.close();
|
||||
if (!device.open()) {
|
||||
return 1;
|
||||
}
|
||||
device.disable_binding();
|
||||
profile = device.profile();
|
||||
service.notify_device_changed(true, profile != nullptr ? profile->name : "");
|
||||
profile = nullptr;
|
||||
pressed = 0;
|
||||
next_open = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
int index = gkeymapper::daemon::match_gkey(*profile, buffer, n);
|
||||
if (index >= 0 && index < static_cast<int>(mappings.keys.size())) {
|
||||
run_mapping(index, false);
|
||||
const gkeymapper::Mapping& mapping = mappings.keys[index];
|
||||
service.notify_key_pressed(
|
||||
index, std::string(gkeymapper::action_type_token(mapping.type)) + ":" +
|
||||
mapping.target);
|
||||
} else if (gkeymapper::daemon::is_release_report(*profile, buffer, n)) {
|
||||
// Key released; nothing to do.
|
||||
if (profile == nullptr) {
|
||||
continue;
|
||||
}
|
||||
uint32_t state = 0;
|
||||
if (gkeymapper::daemon::decode_gkey_state(*profile, buffer, n, state)) {
|
||||
// Only a key that was not pressed before fires: a held key repeats its
|
||||
// report, and a chord arrives as one report with several bits set, so
|
||||
// every key of it fires exactly once.
|
||||
for (size_t i = 0; i < mappings.keys.size() && i < gkeymapper::kNumKeys; ++i) {
|
||||
const uint32_t bit = 1u << i;
|
||||
if ((state & bit) == 0 || (pressed & bit) != 0) {
|
||||
continue;
|
||||
}
|
||||
const gkeymapper::Mapping& mapping = mappings.keys[i];
|
||||
const std::string action =
|
||||
std::string(gkeymapper::action_type_token(mapping.type)) + ":" +
|
||||
mapping.target;
|
||||
run_mapping(static_cast<int>(i), false);
|
||||
service.notify_key_pressed(static_cast<int>(i), action);
|
||||
}
|
||||
pressed = state;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Media keys (mute / volume / play) are forwarded unchanged: they are
|
||||
// reported only on the interface the daemon holds, so without this they
|
||||
// would do nothing at all while it runs (see README). Replaying every
|
||||
// report the keyboard sends keeps the behaviour identical to a keyboard
|
||||
// the kernel drives itself — including its release report, which carries
|
||||
// no bit and is therefore dropped below.
|
||||
uint32_t media = 0;
|
||||
if (!gkeymapper::daemon::decode_media_key(*profile, buffer, n, media) ||
|
||||
media == 0) {
|
||||
continue; // memory key, LED or command acknowledgement, or a release
|
||||
}
|
||||
for (const gkeymapper::daemon::MediaKey& key : profile->mediaKeys) {
|
||||
if ((media & key.bit) == 0) {
|
||||
continue;
|
||||
}
|
||||
std::printf("media -> %s\n", key.name);
|
||||
std::fflush(stdout);
|
||||
actions.tap_key(key.key);
|
||||
}
|
||||
// Media keys and LED acknowledgements are ignored.
|
||||
}
|
||||
|
||||
service.notify_device_changed(false, profile != nullptr ? profile->name : "");
|
||||
|
||||
@@ -99,6 +99,17 @@ void UinputKeyboard::key_event(unsigned short code, bool down) {
|
||||
emit(EV_KEY, code, down ? 1 : 0);
|
||||
}
|
||||
|
||||
void UinputKeyboard::tap(unsigned short code) {
|
||||
if (fd_ < 0) {
|
||||
return;
|
||||
}
|
||||
key_event(code, true);
|
||||
sync();
|
||||
usleep(2000);
|
||||
key_event(code, false);
|
||||
sync();
|
||||
}
|
||||
|
||||
void UinputKeyboard::play(const std::vector<gkeymapper::ShortcutStep>& steps) {
|
||||
if (fd_ < 0) {
|
||||
return;
|
||||
|
||||
@@ -25,6 +25,11 @@ public:
|
||||
// Presses and releases every step of the shortcut in order.
|
||||
void play(const std::vector<gkeymapper::ShortcutStep>& steps);
|
||||
|
||||
// Presses and releases a single Linux key code. The media keys are replayed
|
||||
// with this: the keyboard reports them on its own (see README), so the daemon
|
||||
// passes them on unchanged instead of turning them into a mapping.
|
||||
void tap(unsigned short code);
|
||||
|
||||
private:
|
||||
void emit(unsigned short type, unsigned short code, int value);
|
||||
void sync();
|
||||
|
||||
Reference in New Issue
Block a user