// gKeyMapper daemon - remaps G1..G9 on the Logitech G910. // // It reads the raw HID reports of the keyboard's vendor interface, looks up the // 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), M4 (D-Bus GUI channel) and // M5 (hot-plug, repeat/chord decoding, error reporting to the GUI) are done. #include #include #include #include #include #include #include "action.h" #include "config_watch.h" #include "device_tables.h" #include "g910_device.h" #include "ipc_dbus.h" #include "mapping.h" namespace { volatile std::sig_atomic_t g_running = 1; volatile std::sig_atomic_t g_reload = 0; void handle_stop(int /*signum*/) { g_running = 0; } 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) { std::signal(SIGINT, handle_stop); std::signal(SIGTERM, handle_stop); std::signal(SIGHUP, handle_reload); std::signal(SIGUSR1, handle_reload); std::signal(SIGCHLD, SIG_IGN); // auto-reap launched commands std::string configPath = gkeymapper::default_config_path(); if (argc > 1) { configPath = argv[1]; } gkeymapper::MappingSet mappings = gkeymapper::parse_mappings(""); // Reads the config; on failure the previous mappings stay in place, so a // transient read error never silently disables every key. auto load = [&]() -> bool { const bool ok = gkeymapper::load_mappings_file(configPath, mappings); std::fprintf(stderr, "gkeymapper-daemon: %s %s\n", ok ? "loaded" : "cannot read", configPath.c_str()); return ok; }; load(); // The GUI is the writer; a save is picked up through inotify, SIGHUP/SIGUSR1 // stay as the fallback for scripts and for systems without inotify. gkeymapper::daemon::ConfigWatcher watcher; watcher.init(configPath); gkeymapper::daemon::ActionRunner actions; actions.init(); // Declared before the IPC service so its status callback can report the // device. It is opened only after the name check below: a second instance // must not detach the kernel driver from the keyboard the running one uses. gkeymapper::daemon::GKeyDevice device; const gkeymapper::daemon::DeviceProfile* profile = nullptr; // Runs the mapping of G (0-based). A real keypress and the D-Bus // TestMapping call both go through here, so a test fires exactly the same // path. `hold` is set for a real press, where a "keyhold" mapping keeps its // keys down until the G key is released; a test always taps. Returns whether // there was an action to run. auto run_mapping = [&](int index, bool test, bool hold) -> bool { if (index < 0 || index >= static_cast(mappings.keys.size())) { return false; } const gkeymapper::Mapping& mapping = mappings.keys[index]; const bool usable = mapping.type != gkeymapper::ActionType::Disabled && !mapping.target.empty(); std::printf("G%d -> %s:%s%s\n", index + 1, gkeymapper::action_type_token(mapping.type), mapping.target.c_str(), test ? " (test)" : ""); std::fflush(stdout); if (usable) { if (hold && mapping.type == gkeymapper::ActionType::KeyHold) { actions.begin_hold(index, mapping); } else { actions.run(mapping); } } return usable; }; gkeymapper::daemon::DaemonService service; auto reload = [&]() -> bool { const bool ok = load(); service.notify_config_reloaded(ok); return ok; }; service.set_reload(reload); service.set_device([&]() { return profile != nullptr ? std::string(profile->name) : std::string(); }); service.set_test([&](int index, std::string& action) { if (!run_mapping(index, true, false)) { return false; } const gkeymapper::Mapping& mapping = mappings.keys[index]; action = std::string(gkeymapper::action_type_token(mapping.type)) + ":" + mapping.target; return true; }); switch (service.start(configPath)) { case gkeymapper::daemon::StartResult::AlreadyRunning: // Two instances would fight over the device and create a second // virtual keyboard, so the newcomer gives way. std::fprintf(stderr, "gkeymapper-daemon: another instance is already running; " "exiting\n"); return 0; case gkeymapper::daemon::StartResult::NoBus: case gkeymapper::daemon::StartResult::Ok: break; } // 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 // bounds how long a config change takes to be applied. service.pump(); watcher.poll_events(); if (g_reload || watcher.take_change()) { g_reload = 0; 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) { // 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); // A held key would stay down forever without its release report. actions.release_held(); service.notify_device_changed(false, profile != nullptr ? profile->name : ""); device.close(); profile = nullptr; pressed = 0; next_open = 0; continue; } if (profile == nullptr) { continue; } uint32_t state = 0; if (gkeymapper::daemon::decode_gkey_state(*profile, buffer, n, state)) { // Only a *change* of a bit acts: 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. The falling edge matters for "keyhold", // which lets its keys go when the G key comes up. const size_t count = std::min(mappings.keys.size(), static_cast(gkeymapper::kNumKeys)); for (size_t i = 0; i < count; ++i) { const uint32_t bit = 1u << i; const bool down = (state & bit) != 0; const bool was_down = (pressed & bit) != 0; if (down == was_down) { continue; } if (!down) { // No-op unless this G key holds a "keyhold" mapping. actions.end_hold(static_cast(i)); 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(i), false, true); service.notify_key_pressed(static_cast(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); } } // Shutting down: a "keyhold" key that is still down belongs to the session, // not to the daemon, so let it go before the virtual keyboard disappears. actions.release_held(); service.notify_device_changed(false, profile != nullptr ? profile->name : ""); service.pump(); device.close(); return 0; }