163 lines
5.6 KiB
C++
163 lines
5.6 KiB
C++
// gKeyMapper daemon - per-model raw HID report tables.
|
|
|
|
#include "device_tables.h"
|
|
|
|
#include <algorithm>
|
|
#include <initializer_list>
|
|
|
|
#include <linux/input-event-codes.h>
|
|
|
|
namespace gkeymapper::daemon {
|
|
|
|
namespace {
|
|
|
|
// The G910 reports are 20 bytes; set the given prefix and zero the rest.
|
|
Report report20(std::initializer_list<uint8_t> prefix) {
|
|
Report report(20, 0);
|
|
std::copy(prefix.begin(), prefix.end(), report.begin());
|
|
return report;
|
|
}
|
|
|
|
// G1..G9 press reports, ported from LogitechG910EventInterface.macroKeys.
|
|
std::vector<Report> g910GKeys() {
|
|
return {
|
|
report20({0x11, 0xff, 0x08, 0x00, 0x01}), // G1
|
|
report20({0x11, 0xff, 0x08, 0x00, 0x02}), // G2
|
|
report20({0x11, 0xff, 0x08, 0x00, 0x04}), // G3
|
|
report20({0x11, 0xff, 0x08, 0x00, 0x08}), // G4
|
|
report20({0x11, 0xff, 0x08, 0x00, 0x10}), // G5
|
|
report20({0x11, 0xff, 0x08, 0x00, 0x20}), // G6
|
|
report20({0x11, 0xff, 0x08, 0x00, 0x40}), // G7
|
|
report20({0x11, 0xff, 0x08, 0x00, 0x80}), // G8
|
|
report20({0x11, 0xff, 0x08, 0x00, 0x00, 0x01}), // G9
|
|
};
|
|
}
|
|
|
|
// "disable F-key to G-key binding" command (disableGKeys[0]).
|
|
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;
|
|
profile.vendor = 0x046d;
|
|
profile.product = product;
|
|
profile.configuration = 0;
|
|
profile.interfaceNumber = 1;
|
|
profile.altSetting = 0;
|
|
profile.gKeyReports = g910GKeys();
|
|
profile.mediaPrefix = 0x02;
|
|
profile.mediaKeys = g910MediaKeys();
|
|
profile.disableBinding = disableBinding;
|
|
if (disableBinding) {
|
|
profile.disableCommands = g910DisableCommands();
|
|
}
|
|
return profile;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const std::vector<DeviceProfile>& profiles() {
|
|
// Orion Spectrum (c335) needs the disable command; the Orion Spark (c32b)
|
|
// does not (mirrors disableGKeysInterface == 0 in the reference).
|
|
static const std::vector<DeviceProfile> list = {
|
|
make_g910("Logitech G910 Orion Spectrum", 0xc335, true),
|
|
make_g910("Logitech G910 Orion Spark", 0xc32b, false),
|
|
};
|
|
return list;
|
|
}
|
|
|
|
const DeviceProfile* find_profile(uint16_t vendor, uint16_t product) {
|
|
for (const DeviceProfile& profile : profiles()) {
|
|
if (profile.vendor == vendor && profile.product == product) {
|
|
return &profile;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
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 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
|