// gKeyMapper daemon - per-model raw HID report tables. #include "device_tables.h" #include #include #include namespace gkeymapper::daemon { namespace { // The G910 reports are 20 bytes; set the given prefix and zero the rest. Report report20(std::initializer_list 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 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 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 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& profiles() { // Orion Spectrum (c335) needs the disable command; the Orion Spark (c32b) // does not (mirrors disableGKeysInterface == 0 in the reference). static const std::vector 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(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(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