diff --git a/daemon/Makefile.am b/daemon/Makefile.am index 98e6244..dc889df 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -44,7 +44,10 @@ x52d_LDADD = \ if HAVE_EVDEV x52d_SOURCES += \ daemon/x52d_io.c \ - daemon/x52d_mouse_evdev.c + daemon/x52d_mouse_evdev.c \ + daemon/x52d_keyboard_evdev.c \ + daemon/x52d_profile.c \ + daemon/x52d_clutch.c x52d_CFLAGS += -DHAVE_EVDEV @EVDEV_CFLAGS@ x52d_LDFLAGS += @EVDEV_LIBS@ @@ -84,6 +87,7 @@ install-exec-hook: EXTRA_DIST += \ daemon/daemon.dox \ + daemon/default.conf.example \ daemon/protocol.dox \ daemon/x52d.service.in \ daemon/x52d_client.h \ @@ -93,7 +97,10 @@ EXTRA_DIST += \ daemon/x52d_const.h \ daemon/x52d_device.h \ daemon/x52d_io.h \ + daemon/x52d_clutch.h \ + daemon/x52d_keyboard.h \ daemon/x52d_mouse.h \ + daemon/x52d_profile.h \ daemon/x52d_notify.h \ daemon/x52d_command.h \ daemon/x52dcomm.h \ @@ -103,6 +110,9 @@ EXTRA_DIST += \ # Test cases EXTRA_DIST += \ daemon/test_daemon_comm.py \ + daemon/x52d_config_apply_stubs.c \ + daemon/x52d_profile_test.c \ + daemon/x52d_clutch_test.c \ daemon/tests/config/args.tc \ daemon/tests/config/clock.tc \ daemon/tests/config/led.tc \ @@ -135,6 +145,65 @@ x52d_mouse_test_LDADD = \ TESTS += x52d-mouse-test endif +if HAVE_CMOCKA +if HAVE_EVDEV +check_PROGRAMS += x52d-profile-test x52d-clutch-test + +x52d_profile_test_SOURCES = \ + daemon/x52d_profile_test.c \ + daemon/x52d_profile.c \ + daemon/x52d_config.c \ + daemon/x52d_config_parser.c \ + daemon/x52d_config_dump.c \ + daemon/x52d_config_apply_stubs.c +x52d_profile_test_CFLAGS = \ + -DHAVE_EVDEV \ + -DSYSCONFDIR=\"$(sysconfdir)\" \ + -DLOCALEDIR=\"$(localedir)\" \ + -DLOGDIR=\"$(localstatedir)/log\" \ + -DRUNDIR=\"$(localstatedir)/run\" \ + -I $(top_srcdir) \ + -I $(top_srcdir)/daemon \ + -I $(top_srcdir)/libx52 \ + -I $(top_srcdir)/libx52io \ + -I $(top_srcdir)/libx52util \ + -I $(top_srcdir)/lib/pinelog \ + -I $(top_srcdir)/lib/inih \ + $(WARN_CFLAGS) @CMOCKA_CFLAGS@ @EVDEV_CFLAGS@ +x52d_profile_test_LDFLAGS = $(WARN_LDFLAGS) +x52d_profile_test_LDADD = \ + lib/pinelog/libpinelog.la \ + lib/inih/libinih.la \ + libx52io.la \ + libx52.la \ + @EVDEV_LIBS@ \ + @CMOCKA_LIBS@ \ + @LTLIBINTL@ + +x52d_clutch_test_SOURCES = \ + daemon/x52d_clutch_test.c \ + daemon/x52d_clutch.c +x52d_clutch_test_CFLAGS = \ + -DHAVE_EVDEV \ + -DLOCALEDIR=\"$(localedir)\" \ + -I $(top_srcdir) \ + -I $(top_srcdir)/daemon \ + -I $(top_srcdir)/libx52 \ + -I $(top_srcdir)/libx52io \ + -I $(top_srcdir)/lib/pinelog \ + $(WARN_CFLAGS) @CMOCKA_CFLAGS@ +x52d_clutch_test_LDFLAGS = $(WARN_LDFLAGS) +x52d_clutch_test_LDADD = \ + lib/pinelog/libpinelog.la \ + libx52.la \ + libx52io.la \ + @CMOCKA_LIBS@ \ + @LTLIBINTL@ + +TESTS += x52d-profile-test x52d-clutch-test +endif +endif + if HAVE_SYSTEMD if !IS_MAKE_DISTCHECK SED_ARGS = s,%bindir%,$(bindir),g diff --git a/daemon/daemon.dox b/daemon/daemon.dox index 5d4a04f..8dfbe4f 100644 --- a/daemon/daemon.dox +++ b/daemon/daemon.dox @@ -44,4 +44,53 @@ overridden. @code{.unparsed} -o clock.secondary=America/New_York @endcode + +# Profile files (Linux, when profiles are implemented) + +Profiles map joystick buttons to keyboard events only. They are selected by +the main config option Profiles.Profile and loaded from Profiles.Directory. +Mouse (thumbstick, mouse buttons, scroll) is handled separately by the +virtual mouse and is not driven by profiles. + +## Mode and shift + +- The joystick has **3 modes**; only one is active at a time. Mode is determined + by BTN_MODE_1, BTN_MODE_2, BTN_MODE_3 (the physical mode switch). +- The **shift key** is configured in the profile file ([Profile] section, + key ShiftButton, e.g. BTN_PINKY). When held, the profile uses the shift + layer for the current mode. Default is BTN_PINKY if not set in the profile. + +So at runtime the effective layer is (mode 1/2/3) × (shift released or held). +Profile files use INI sections to define mappings per layer: + +- [Profile] — profile options: Name = display name; ShiftButton = BTN_PINKY (or another button name) +- [Mode1], [Mode2], [Mode3] — mappings when shift is not held +- [Mode1.Shift], [Mode2.Shift], [Mode3.Shift] — mappings when shift is held + (override the non-shift layer for that mode) + +Within each section, button mappings use the Button. = ... syntax: +- \c key \c KEY_X [\c KEY_Y ...] — single key or combo (e.g. Ctrl-X: \c key \c KEY_LEFTCTRL \c KEY_X). All keys go down on press and up on release in reverse order. +- \c macro \c KEY_A \c KEY_B ... — sequence of key down/up on button down only. The shift and mode selector buttons can be left unmapped +(recommended) or mapped like any other button. + +Fallback: if a mapping is not defined for the current layer, the next layer +in the chain is used. ModeN.Shift falls back to ModeN; Mode2 falls back to +Mode1; Mode3 falls back to Mode2 then Mode1. So you can define only Mode1 +and override in Mode1.Shift or Mode2 where needed. + +# Clutch (profile selection on MFD) + +When \c Profiles.ClutchEnabled is set, the clutch button enters a profile +selection UI on the MFD: + +- **ClutchLatched yes**: First press-and-release of the clutch engages clutch + mode; next press-and-release exits. **ClutchLatched no**: Clutch mode is + active only while the clutch button is held; releasing exits. + +In clutch mode, profile names (from \c Profiles.Directory *.conf files) are +shown on the MFD. Use the **hat** (AXIS_HATX / AXIS_HATY): **up/down** (HATY) to +scroll through profiles, **left** (HATX -1, swipe left) to clear and exit without +changing profile, **right** (HATX 1, swipe right) to select the highlighted +profile and load it. +The selected profile is applied immediately and in-memory config is updated. */ diff --git a/daemon/default.conf.example b/daemon/default.conf.example new file mode 100644 index 0000000..c37d108 --- /dev/null +++ b/daemon/default.conf.example @@ -0,0 +1,27 @@ +# Example X52 profile: keyboard mappings per mode and shift +# Copy to e.g. /etc/x52d/profiles.d/default.conf and edit. +# [Profile]: Name = display name; ShiftButton = button name (default BTN_PINKY if omitted) +# [Mode1], [Mode2], [Mode3], [Mode1.Shift], [Mode2.Shift], [Mode3.Shift]: +# Button.X = key KEY_Y [KEY_Z ...] (single key or combo, down on press/up on release) +# Button.X = macro KEY_A KEY_B (sequence on button down only) +# Fallback: ModeN.Shift -> ModeN; Mode2 -> Mode1; Mode3 -> Mode2 -> Mode1 + +[Profile] +Name=Example profile +ShiftButton=BTN_PINKY + +[Mode1] +# Single key: Fire -> E +Button.BTN_FIRE = key KEY_E +# Key combo: A -> Ctrl+X (modifiers first, then key) +Button.BTN_A = key KEY_LEFTCTRL KEY_X +# Alt+Y +Button.BTN_B = key KEY_LEFTALT KEY_Y +# Macro: T1 up -> Ctrl+Shift+T (on button down only) +Button.BTN_T1_UP = macro KEY_LEFTCTRL KEY_LEFTSHIFT KEY_T + +[Mode1.Shift] +Button.BTN_FIRE = key KEY_F + +[Mode2] +Button.BTN_A = key KEY_SPACE diff --git a/daemon/x52d.conf b/daemon/x52d.conf index b6a11cb..75c81f9 100644 --- a/daemon/x52d.conf +++ b/daemon/x52d.conf @@ -92,13 +92,18 @@ ReverseScroll=no # Profiles - only valid on Linux ###################################################################### [Profiles] -# TODO: Profiles are used to map the buttons and axis to keyboard events, and -# can be used to write macros. This is a placeholder only for now, and is not -# supported yet. +# Profiles map joystick buttons to keyboard events only. The joystick has +# 3 modes (only one active; selected by BTN_MODE_1/2/3) and an optional +# shift key. When shift is held, the profile's shift layer for the current +# mode is used. Profile files use sections [Mode1], [Mode2], [Mode3], and +# optionally [Mode1.Shift], [Mode2.Shift], [Mode3.Shift]. # Directory is the location of the folder containing the individual profiles. Directory=/etc/x52d/profiles.d +# Profile is the name of the active profile (e.g. file default.conf in Directory). +Profile=default + # ClutchEnabled determines if the clutch button is treated specially ClutchEnabled=no diff --git a/daemon/x52d_clutch.c b/daemon/x52d_clutch.c new file mode 100644 index 0000000..8fe9021 --- /dev/null +++ b/daemon/x52d_clutch.c @@ -0,0 +1,285 @@ +/* + * Saitek X52 Pro MFD & LED driver - Clutch (profile selection UI) + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include "config.h" +#include +#include +#include +#include +#include +#include + +#include "libx52.h" +#include "libx52io.h" + +#define PINELOG_MODULE X52D_MOD_CLUTCH +#include "pinelog.h" + +#include "x52d_config.h" +#include "x52d_const.h" +#include "x52d_device.h" +#include "x52d_clutch.h" +#include "x52d_profile.h" + +#define MFD_LINE_SIZE 16 +#define MFD_LINES 3 +#define MAX_PROFILES 32 +#define MAX_PROFILE_NAME 16 /* truncate for MFD display */ + +static bool clutch_engaged; +static char *profile_list[MAX_PROFILES]; +static int profile_count; +static int highlight_index; +static bool clutch_ui_active; /* true after we've shown the list at least once */ + +static bool config_clutch_enabled(void) +{ + const char *v = x52d_config_get("Profiles", "ClutchEnabled"); + if (v == NULL) return false; + return (strcasecmp(v, "yes") == 0 || strcasecmp(v, "true") == 0); +} + +static bool config_clutch_latched(void) +{ + const char *v = x52d_config_get("Profiles", "ClutchLatched"); + if (v == NULL) return false; + return (strcasecmp(v, "yes") == 0 || strcasecmp(v, "true") == 0); +} + +static void clutch_update_led(bool enabled, bool engaged) +{ + if (!enabled) { + x52d_dev_set_blink(0); + x52d_dev_set_led_state(LIBX52_LED_CLUTCH, LIBX52_LED_STATE_RED); + return; + } + if (!engaged) { + x52d_dev_set_blink(0); + x52d_dev_set_led_state(LIBX52_LED_CLUTCH, LIBX52_LED_STATE_GREEN); + return; + } + /* Engaged: amber with hardware blink */ + x52d_dev_set_led_state(LIBX52_LED_CLUTCH, LIBX52_LED_STATE_AMBER); + x52d_dev_set_blink(1); +} + +static void free_profile_list(void) +{ + int i; + for (i = 0; i < profile_count; i++) { + free(profile_list[i]); + profile_list[i] = NULL; + } + profile_count = 0; +} + +static int scan_profiles(void) +{ + const char *dir_path; + DIR *dir; + struct dirent *ent; + size_t len; + char *stem; + + free_profile_list(); + dir_path = x52d_config_get("Profiles", "Directory"); + if (dir_path == NULL || dir_path[0] == '\0') { + return 0; + } + dir = opendir(dir_path); + if (dir == NULL) { + PINELOG_WARN(_("Cannot open profiles directory %s: %s"), dir_path, strerror(errno)); + return 0; + } + while (profile_count < MAX_PROFILES && (ent = readdir(dir)) != NULL) { + len = strlen(ent->d_name); + if (len <= 4) continue; + if (strcasecmp(ent->d_name + len - 4, ".conf") != 0) continue; + stem = malloc(len - 4); /* strip .conf, need len-5+1 for NUL */ + if (stem == NULL) continue; + memcpy(stem, ent->d_name, len - 5); + stem[len - 5] = '\0'; + if (stem[0] == '\0') { + free(stem); + continue; + } + profile_list[profile_count++] = stem; + } + closedir(dir); + /* Sort by name (simple bubble sort) */ + { + int i, j; + for (i = 0; i < profile_count - 1; i++) { + for (j = i + 1; j < profile_count; j++) { + if (strcasecmp(profile_list[i], profile_list[j]) > 0) { + char *t = profile_list[i]; + profile_list[i] = profile_list[j]; + profile_list[j] = t; + } + } + } + } + return profile_count; +} + +static void clutch_clear_mfd(void) +{ + static const char blank[MFD_LINE_SIZE] = { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }; + x52d_dev_set_text(0, blank, MFD_LINE_SIZE); + x52d_dev_set_text(1, blank, MFD_LINE_SIZE); + x52d_dev_set_text(2, blank, MFD_LINE_SIZE); +} + +static void clutch_refresh_mfd(void) +{ + char line_buf[MFD_LINE_SIZE]; + int i; + size_t len; + + if (profile_count == 0) { + clutch_clear_mfd(); + return; + } + if (highlight_index >= profile_count) highlight_index = profile_count - 1; + if (highlight_index < 0) highlight_index = 0; + + for (i = 0; i < MFD_LINES; i++) { + int idx = highlight_index - 1 + i; /* line 0 = above, 1 = current, 2 = below */ + if (idx < 0 || idx >= profile_count) { + memset(line_buf, ' ', MFD_LINE_SIZE); + x52d_dev_set_text(i, line_buf, MFD_LINE_SIZE); + continue; + } + len = strlen(profile_list[idx]); + if (len > MAX_PROFILE_NAME) len = MAX_PROFILE_NAME; + memset(line_buf, ' ', MFD_LINE_SIZE); + if (idx == highlight_index) { + line_buf[0] = '>'; + if (len + 1 < MFD_LINE_SIZE) { + memcpy(line_buf + 1, profile_list[idx], len); + len++; + } else { + memcpy(line_buf + 1, profile_list[idx], MFD_LINE_SIZE - 1); + len = MFD_LINE_SIZE; + } + } else { + memcpy(line_buf, profile_list[idx], len); + } + x52d_dev_set_text(i, line_buf, MFD_LINE_SIZE); + } +} + +static void clutch_enter_ui(void) +{ + scan_profiles(); + highlight_index = 0; + clutch_ui_active = true; + clutch_refresh_mfd(); +} + +static void clutch_exit_ui(bool restore_mfd) +{ + clutch_ui_active = false; + if (restore_mfd) { + clutch_clear_mfd(); + } + free_profile_list(); +} + +static void clutch_do_clear(void) +{ + clutch_exit_ui(true); + clutch_engaged = false; +} + +static void clutch_do_select(void) +{ + if (profile_count > 0 && highlight_index >= 0 && highlight_index < profile_count) { + const char *name = profile_list[highlight_index]; + if (x52d_config_set("Profiles", "Profile", name) == 0) { + x52d_profile_init(); + PINELOG_INFO(_("Profile switched to: %s"), name); + } + } + clutch_exit_ui(true); + clutch_engaged = false; +} + +bool x52d_clutch_process(const libx52io_report *report, + const libx52io_report *prev) +{ + bool enabled = config_clutch_enabled(); + bool latched = config_clutch_latched(); + bool clutch_pressed = report->button[LIBX52IO_BTN_CLUTCH]; + bool prev_clutch = prev->button[LIBX52IO_BTN_CLUTCH]; + + /* Update clutch_engaged */ + if (latched) { + if (prev_clutch && !clutch_pressed) { + clutch_engaged = !clutch_engaged; + } + } else { + clutch_engaged = clutch_pressed; + } + + /* Clutch LED: disabled=red, enabled+not engaged=green, engaged=blink amber/off 250ms */ + clutch_update_led(enabled, clutch_engaged); + + if (!enabled) { + return false; + } + + if (!clutch_engaged) { + if (clutch_ui_active) { + clutch_exit_ui(true); + } + return false; + } + + /* Clutch engaged: show UI and handle hat/swipe */ + if (!clutch_ui_active) { + clutch_enter_ui(); + } + + /* + * Safety: if both HATX and HATY are non-zero (diagonal), use HATY alone + * and treat HATX as 0 to avoid accidental clear/select on diagonals. + */ + { + int32_t prev_x = prev->axis[LIBX52IO_AXIS_HATX]; + int32_t prev_y = prev->axis[LIBX52IO_AXIS_HATY]; + int32_t cur_x = report->axis[LIBX52IO_AXIS_HATX]; + int32_t cur_y = report->axis[LIBX52IO_AXIS_HATY]; + if (prev_x != 0 && prev_y != 0) prev_x = 0; + if (cur_x != 0 && cur_y != 0) cur_x = 0; + /* Edge detection: HATY -1=up, HATY 1=down, HATX -1=left, HATX 1=right */ + if (prev_x != -1 && cur_x == -1) { + clutch_do_clear(); + return true; + } + if (profile_count > 0) { + if (prev_y != -1 && cur_y == -1) { + highlight_index--; + if (highlight_index < 0) highlight_index = profile_count - 1; + clutch_refresh_mfd(); + } + if (prev_y != 1 && cur_y == 1) { + highlight_index++; + if (highlight_index >= profile_count) highlight_index = 0; + clutch_refresh_mfd(); + } + if (prev_x != 1 && cur_x == 1) { + clutch_do_select(); + return true; + } + } + } + + return true; /* consumed: in clutch UI, skip profile/mouse */ +} diff --git a/daemon/x52d_clutch.h b/daemon/x52d_clutch.h new file mode 100644 index 0000000..abb1323 --- /dev/null +++ b/daemon/x52d_clutch.h @@ -0,0 +1,46 @@ +/* + * Saitek X52 Pro MFD & LED driver - Clutch (profile selection UI) + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#ifndef X52D_CLUTCH_H +#define X52D_CLUTCH_H + +#include "libx52io.h" + +/** + * @file x52d_clutch.h + * @brief Clutch mode: profile selection on MFD (hat scroll, swipe left/right) + */ + +#if defined(HAVE_EVDEV) + +/** + * @brief Process clutch button and, when engaged, clutch UI (hat/swipe). + * + * When ClutchEnabled and the clutch is engaged (latched toggle or held), + * displays profile names on the MFD. Hat up/down scrolls; swipe left + * clears (exit without change); swipe right selects the highlighted profile. + * + * @param report Current joystick report. + * @param prev Previous report (for edge detection). + * @return true if clutch UI consumed the report (caller should skip profile/mouse). + */ +bool x52d_clutch_process(const libx52io_report *report, + const libx52io_report *prev); + +#else + +static inline bool x52d_clutch_process(const libx52io_report *report, + const libx52io_report *prev) { + (void)report; + (void)prev; + return false; +} + +#endif /* HAVE_EVDEV */ + +#endif /* !defined X52D_CLUTCH_H */ diff --git a/daemon/x52d_clutch_test.c b/daemon/x52d_clutch_test.c new file mode 100644 index 0000000..102270d --- /dev/null +++ b/daemon/x52d_clutch_test.c @@ -0,0 +1,165 @@ +/* + * Saitek X52 Pro MFD & LED driver - Clutch module tests + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include "config.h" +#include +#include +#include +#include + +#include "libx52.h" +#include "libx52io.h" + +#define PINELOG_MODULE X52D_MOD_CLUTCH +#include "pinelog.h" + +#include "x52d_config.h" +#include "x52d_const.h" +#include "x52d_device.h" +#include "x52d_profile.h" +#include "x52d_clutch.h" + +/* Stub config: return values set by test */ +static const char *stub_clutch_enabled = "no"; +static const char *stub_clutch_latched = "no"; + +const char *x52d_config_get(const char *section, const char *key) +{ + if (section != NULL && key != NULL) { + if (strcasecmp(section, "Profiles") == 0) { + if (strcasecmp(key, "ClutchEnabled") == 0) return stub_clutch_enabled; + if (strcasecmp(key, "ClutchLatched") == 0) return stub_clutch_latched; + } + } + return NULL; +} + +int x52d_config_set(const char *section, const char *key, const char *value) +{ + (void)section; + (void)key; + (void)value; + return 0; +} + +void x52d_profile_init(void) +{ +} + +/* Stub device: record and optionally check expected */ +int x52d_dev_set_led_state(libx52_led_id led, libx52_led_state state) +{ + check_expected(led); + check_expected(state); + return 0; +} + +int x52d_dev_set_blink(uint8_t state) +{ + (void)check_expected(state); + return 0; +} + +int x52d_dev_set_text(uint8_t line, const char *text, uint8_t length) +{ + (void)line; + (void)text; + (void)length; + return 0; +} + +static void test_clutch_disabled_red_led(void **state) +{ + libx52io_report report, prev; + + (void)state; + stub_clutch_enabled = "no"; + stub_clutch_latched = "no"; + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + + expect_value(x52d_dev_set_blink, state, 0); + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_RED); + + assert_false(x52d_clutch_process(&report, &prev)); +} + +static void test_clutch_enabled_not_engaged_green_led(void **state) +{ + libx52io_report report, prev; + + (void)state; + stub_clutch_enabled = "yes"; + stub_clutch_latched = "no"; + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.button[LIBX52IO_BTN_CLUTCH] = false; + + expect_value(x52d_dev_set_blink, state, 0); + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_GREEN); + + assert_false(x52d_clutch_process(&report, &prev)); +} + +static void test_clutch_engaged_amber_blink(void **state) +{ + libx52io_report report, prev; + + (void)state; + stub_clutch_enabled = "yes"; + stub_clutch_latched = "yes"; + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + + /* First: press clutch (no toggle yet for latched) */ + report.button[LIBX52IO_BTN_CLUTCH] = true; + expect_value(x52d_dev_set_blink, state, 0); + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_GREEN); + assert_false(x52d_clutch_process(&report, &prev)); + + /* Second: release clutch -> toggle engaged */ + memcpy(&prev, &report, sizeof(report)); + report.button[LIBX52IO_BTN_CLUTCH] = false; + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_AMBER); + expect_value(x52d_dev_set_blink, state, 1); + assert_true(x52d_clutch_process(&report, &prev)); +} + +static void test_clutch_held_not_latched(void **state) +{ + libx52io_report report, prev; + + (void)state; + stub_clutch_enabled = "yes"; + stub_clutch_latched = "no"; + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.button[LIBX52IO_BTN_CLUTCH] = true; + + /* Held (not latched): directly to engaged = amber + blink, no prior blink(0) */ + expect_value(x52d_dev_set_led_state, led, LIBX52_LED_CLUTCH); + expect_value(x52d_dev_set_led_state, state, LIBX52_LED_STATE_AMBER); + expect_value(x52d_dev_set_blink, state, 1); + assert_true(x52d_clutch_process(&report, &prev)); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_clutch_disabled_red_led), + cmocka_unit_test(test_clutch_enabled_not_engaged_green_led), + cmocka_unit_test(test_clutch_engaged_amber_blink), + cmocka_unit_test(test_clutch_held_not_latched), + }; + cmocka_set_message_output(CM_OUTPUT_TAP); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/daemon/x52d_command.c b/daemon/x52d_command.c index b4d2b59..176163f 100644 --- a/daemon/x52d_command.c +++ b/daemon/x52d_command.c @@ -261,6 +261,9 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv) [X52D_MOD_IO] = "io", [X52D_MOD_LED] = "led", [X52D_MOD_MOUSE] = "mouse", + [X52D_MOD_KEYBOARD] = "keyboard", + [X52D_MOD_PROFILE] = "profile", + [X52D_MOD_CLUTCH] = "clutch", [X52D_MOD_COMMAND] = "command", [X52D_MOD_CLIENT] = "client", [X52D_MOD_NOTIFY] = "notify", diff --git a/daemon/x52d_config.c b/daemon/x52d_config.c index a5bcff7..5b1f935 100644 --- a/daemon/x52d_config.c +++ b/daemon/x52d_config.c @@ -86,6 +86,7 @@ const char *x52d_config_get(const char *section, const char *key) * TODO: Remove the ones below when their implementation is complete */ void x52d_cfg_set_Profiles_Directory(char* param) { (void)param; } +void x52d_cfg_set_Profiles_Profile(char* param) { (void)param; } void x52d_cfg_set_Profiles_ClutchEnabled(bool param) { (void)param; } void x52d_cfg_set_Profiles_ClutchLatched(bool param) { (void)param; } diff --git a/daemon/x52d_config.def b/daemon/x52d_config.def index 3ba713b..0a073ca 100644 --- a/daemon/x52d_config.def +++ b/daemon/x52d_config.def @@ -84,6 +84,10 @@ CFG(Mouse, ReverseScroll, mouse_reverse_scroll, bool, false) // Directory is the location of the folder containing the individual profiles. CFG(Profiles, Directory, profiles_dir, string, /etc/x52d/profiles.d) +// Profile is the name of the active profile (file profiles_dir/Profile.conf). +// Profiles generate keyboard events only (single key or macro). +CFG(Profiles, Profile, profile_name, string, default) + // ClutchEnabled determines if the clutch button is treated specially CFG(Profiles, ClutchEnabled, clutch_enabled, bool, false) diff --git a/daemon/x52d_config.h b/daemon/x52d_config.h index 4ba7f3d..bbd55ff 100644 --- a/daemon/x52d_config.h +++ b/daemon/x52d_config.h @@ -45,6 +45,7 @@ struct x52d_config { bool clutch_latched; char profiles_dir[NAME_MAX]; + char profile_name[NAME_MAX]; }; /* Callback functions for configuration */ @@ -74,6 +75,7 @@ void x52d_cfg_set_Mouse_Enabled(bool param); void x52d_cfg_set_Mouse_Speed(int param); void x52d_cfg_set_Mouse_ReverseScroll(bool param); void x52d_cfg_set_Profiles_Directory(char* param); +void x52d_cfg_set_Profiles_Profile(char* param); void x52d_cfg_set_Profiles_ClutchEnabled(bool param); void x52d_cfg_set_Profiles_ClutchLatched(bool param); diff --git a/daemon/x52d_config_apply_stubs.c b/daemon/x52d_config_apply_stubs.c new file mode 100644 index 0000000..edf983c --- /dev/null +++ b/daemon/x52d_config_apply_stubs.c @@ -0,0 +1,45 @@ +/* + * Saitek X52 Pro MFD & LED driver - No-op config apply stubs for unit tests + * + * When linking the profile test we use x52d_config.c but not the full daemon + * (clock, led, mouse). x52d_config_apply() and x52d_config_apply_immediate() + * call every x52d_cfg_set_* from x52d_config.def. This file provides no-op + * implementations for Clock, LED, Brightness, and Mouse so the test links. + * Profiles_* are already defined as no-ops in x52d_config.c. + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include "x52d_config.h" + +/* Clock */ +void x52d_cfg_set_Clock_Enabled(bool param) { (void)param; } +void x52d_cfg_set_Clock_PrimaryIsLocal(bool param) { (void)param; } +void x52d_cfg_set_Clock_Secondary(char *param) { (void)param; } +void x52d_cfg_set_Clock_Tertiary(char *param) { (void)param; } +void x52d_cfg_set_Clock_FormatPrimary(libx52_clock_format param) { (void)param; } +void x52d_cfg_set_Clock_FormatSecondary(libx52_clock_format param) { (void)param; } +void x52d_cfg_set_Clock_FormatTertiary(libx52_clock_format param) { (void)param; } +void x52d_cfg_set_Clock_DateFormat(libx52_date_format param) { (void)param; } + +/* LED */ +void x52d_cfg_set_LED_Fire(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_Throttle(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_A(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_B(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_D(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_E(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_T1(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_T2(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_T3(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_POV(libx52_led_state param) { (void)param; } +void x52d_cfg_set_LED_Clutch(libx52_led_state param) { (void)param; } + +/* Brightness */ +void x52d_cfg_set_Brightness_MFD(uint16_t param) { (void)param; } +void x52d_cfg_set_Brightness_LED(uint16_t param) { (void)param; } + +/* Mouse */ +void x52d_cfg_set_Mouse_Enabled(bool param) { (void)param; } +void x52d_cfg_set_Mouse_Speed(int param) { (void)param; } +void x52d_cfg_set_Mouse_ReverseScroll(bool param) { (void)param; } diff --git a/daemon/x52d_const.h b/daemon/x52d_const.h index 5a24e6b..5b0bbc4 100644 --- a/daemon/x52d_const.h +++ b/daemon/x52d_const.h @@ -33,6 +33,9 @@ enum { X52D_MOD_IO, X52D_MOD_LED, X52D_MOD_MOUSE, + X52D_MOD_KEYBOARD, + X52D_MOD_PROFILE, + X52D_MOD_CLUTCH, X52D_MOD_COMMAND, X52D_MOD_CLIENT, X52D_MOD_NOTIFY, diff --git a/daemon/x52d_io.c b/daemon/x52d_io.c index 1cc30f1..b45526a 100644 --- a/daemon/x52d_io.c +++ b/daemon/x52d_io.c @@ -14,7 +14,10 @@ #include "x52d_const.h" #include "x52d_config.h" #include "x52d_io.h" +#include "x52d_clutch.h" +#include "x52d_keyboard.h" #include "x52d_mouse.h" +#include "x52d_profile.h" #include "libx52io.h" #define PINELOG_MODULE X52D_MOD_IO @@ -26,7 +29,14 @@ static pthread_t io_thr; static void process_report(libx52io_report *report, libx52io_report *prev) { - // TODO: Process changes +#if defined(HAVE_EVDEV) + if (x52d_clutch_process(report, prev)) { + /* Clutch UI consumed input (profile selection mode) */ + memcpy(prev, report, sizeof(*prev)); + return; + } + x52d_profile_apply(report, prev); +#endif x52d_mouse_report_event(report); memcpy(prev, report, sizeof(*prev)); } diff --git a/daemon/x52d_keyboard.h b/daemon/x52d_keyboard.h new file mode 100644 index 0000000..728d39c --- /dev/null +++ b/daemon/x52d_keyboard.h @@ -0,0 +1,62 @@ +/* + * Saitek X52 Pro MFD & LED driver - Virtual keyboard + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#ifndef X52D_KEYBOARD_H +#define X52D_KEYBOARD_H + +#include +#include + +/** + * @file x52d_keyboard.h + * @brief Virtual keyboard (uinput) for profile-generated key events + */ + +#if defined(HAVE_EVDEV) + +/** + * @brief Initialize the virtual keyboard device. + * + * Creates a uinput keyboard that can emit key events. Must be called + * before x52d_keyboard_evdev_key(). + */ +void x52d_keyboard_evdev_init(void); + +/** + * @brief Shut down and destroy the virtual keyboard device. + */ +void x52d_keyboard_evdev_exit(void); + +/** + * @brief Emit a key event (down or up). + * + * @param key_code Linux input event key code (e.g. KEY_A). + * @param value 1 for key down, 0 for key up. + * @return 0 on success, negative errno on failure. + */ +int x52d_keyboard_evdev_key(uint16_t key_code, int value); + +/** + * @brief Return whether the virtual keyboard is available. + */ +bool x52d_keyboard_evdev_available(void); + +#else + +static inline void x52d_keyboard_evdev_init(void) { (void)0; } +static inline void x52d_keyboard_evdev_exit(void) { (void)0; } +static inline int x52d_keyboard_evdev_key(uint16_t key_code, int value) { + (void)key_code; + (void)value; + return 0; +} +static inline bool x52d_keyboard_evdev_available(void) { return false; } + +#endif /* HAVE_EVDEV */ + +#endif /* !defined X52D_KEYBOARD_H */ diff --git a/daemon/x52d_keyboard_evdev.c b/daemon/x52d_keyboard_evdev.c new file mode 100644 index 0000000..255a351 --- /dev/null +++ b/daemon/x52d_keyboard_evdev.c @@ -0,0 +1,93 @@ +/* + * Saitek X52 Pro MFD & LED driver - Virtual keyboard (evdev/uinput) + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include "config.h" +#include +#include +#include + +#include "libevdev/libevdev.h" +#include "libevdev/libevdev-uinput.h" +#include "x52d_const.h" +#include "x52d_keyboard.h" + +#define PINELOG_MODULE X52D_MOD_KEYBOARD +#include "pinelog.h" + +/* Maximum key code to enable on the virtual keyboard (covers KEY_* range). */ +#ifndef KEY_MAX +#define KEY_MAX 0x2ff +#endif + +static struct libevdev_uinput *kbd_uidev; +static bool kbd_uidev_created = false; + +int x52d_keyboard_evdev_key(uint16_t key_code, int value) +{ + int rc; + + if (!kbd_uidev_created || kbd_uidev == NULL) { + return -ENODEV; + } + + rc = libevdev_uinput_write_event(kbd_uidev, EV_KEY, key_code, value); + if (rc != 0) { + PINELOG_ERROR(_("Error writing key event (code %u, value %d): %s"), + (unsigned)key_code, value, strerror(-rc)); + return rc; + } + + rc = libevdev_uinput_write_event(kbd_uidev, EV_SYN, SYN_REPORT, 0); + if (rc != 0) { + PINELOG_ERROR(_("Error writing keyboard sync event")); + return rc; + } + + return 0; +} + +bool x52d_keyboard_evdev_available(void) +{ + return kbd_uidev_created; +} + +void x52d_keyboard_evdev_init(void) +{ + int rc; + struct libevdev *dev; + unsigned int code; + + dev = libevdev_new(); + libevdev_set_name(dev, "X52 virtual keyboard"); + libevdev_enable_event_type(dev, EV_KEY); + + for (code = 1; code <= KEY_MAX; code++) { + libevdev_enable_event_code(dev, EV_KEY, code, NULL); + } + + rc = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, + &kbd_uidev); + libevdev_free(dev); + + if (rc != 0) { + PINELOG_ERROR(_("Error %d creating X52 virtual keyboard: %s"), + -rc, strerror(-rc)); + } else { + kbd_uidev_created = true; + PINELOG_INFO(_("X52 virtual keyboard created")); + } +} + +void x52d_keyboard_evdev_exit(void) +{ + kbd_uidev_created = false; + if (kbd_uidev != NULL) { + libevdev_uinput_destroy(kbd_uidev); + kbd_uidev = NULL; + } +} diff --git a/daemon/x52d_main.c b/daemon/x52d_main.c index 9b70164..7ffef23 100644 --- a/daemon/x52d_main.c +++ b/daemon/x52d_main.c @@ -21,7 +21,9 @@ #include "x52d_config.h" #include "x52d_device.h" #include "x52d_io.h" +#include "x52d_keyboard.h" #include "x52d_mouse.h" +#include "x52d_profile.h" #include "x52d_command.h" #include "x52d_notify.h" #include "x52dcomm-internal.h" @@ -327,6 +329,8 @@ int main(int argc, char **argv) #if defined(HAVE_EVDEV) x52d_io_init(); x52d_mouse_evdev_init(); + x52d_keyboard_evdev_init(); + x52d_profile_init(); #endif // Re-enable signals @@ -367,6 +371,8 @@ cleanup: x52d_command_exit(); x52d_notify_exit(); #if defined(HAVE_EVDEV) + x52d_profile_exit(); + x52d_keyboard_evdev_exit(); x52d_mouse_evdev_exit(); x52d_io_exit(); #endif diff --git a/daemon/x52d_profile.c b/daemon/x52d_profile.c new file mode 100644 index 0000000..3184a6d --- /dev/null +++ b/daemon/x52d_profile.c @@ -0,0 +1,460 @@ +/* + * Saitek X52 Pro MFD & LED driver - Profile (button-to-keyboard mapping) + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include "config.h" +#include +#include +#include +#include +#include + +#include "libevdev/libevdev.h" +#include "libx52io.h" +#include "ini.h" + +#define PINELOG_MODULE X52D_MOD_PROFILE +#include "pinelog.h" + +#include "x52d_config.h" +#include "x52d_const.h" +#include "x52d_keyboard.h" +#include "x52d_profile.h" + +#define NUM_LAYERS 6 /* 3 modes × 2 (no shift / shift) */ +#define MAX_FALLBACK 4 /* max layers in a fallback chain */ +#define BUTTON_PREFIX "Button." +#define PROFILE_SECTION "Profile" +#define PROFILE_NAME_KEY "Name" +#define SHIFT_BUTTON_KEY "ShiftButton" +#define DEFAULT_SHIFT_BUTTON "BTN_PINKY" +#define PROFILE_NAME_LEN 128 +#define SHIFT_BUTTON_STR_LEN 64 +#define BUTTON_PREFIX_LEN (sizeof(BUTTON_PREFIX) - 1) +#define KEY_PREFIX "key" +#define MACRO_PREFIX "macro" +#define MAX_MACRO_KEYS 32 + +typedef enum { + ACTION_NONE, + ACTION_KEY, + ACTION_MACRO +} action_type_t; + +typedef struct { + action_type_t type; + size_t key_len; /* for ACTION_KEY: number of keys in combo */ + uint16_t *key_codes; /* for ACTION_KEY, may be NULL */ + size_t macro_len; + uint16_t *macro_keys; /* for ACTION_MACRO, may be NULL */ +} profile_action_t; + +static profile_action_t layers[NUM_LAYERS][LIBX52IO_BUTTON_MAX]; +static int shift_button_index = -1; /* -1 = no shift button resolved */ +static bool profile_loaded = false; +/* [Profile] section options; set during parse, resolved after. */ +static char profile_name_str[PROFILE_NAME_LEN]; +static char profile_shift_button_str[SHIFT_BUTTON_STR_LEN]; + +static int button_name_to_index(const char *name) +{ + libx52io_button b; + const char *str; + + if (name == NULL || *name == '\0') { + return -1; + } + for (b = LIBX52IO_BTN_TRIGGER; b < LIBX52IO_BUTTON_MAX; b++) { + str = libx52io_button_to_str(b); + if (str != NULL && strcasecmp(str, name) == 0) { + return (int)b; + } + } + return -1; +} + +static int key_name_to_code(const char *name) +{ + int code; + + if (name == NULL || *name == '\0') { + return -1; + } + code = libevdev_event_code_from_name(EV_KEY, name); + return (code >= 0) ? code : -1; +} + +/** + * Parse value into a single key code (action KEY) or macro (action MACRO). + * Returns 0 on success, -1 on parse error. + */ +static int parse_action_value(const char *value, profile_action_t *out) +{ + char buf[256]; + char *tok; + char *p; + uint16_t keys[MAX_MACRO_KEYS]; + size_t n = 0; + int code; + + memset(out, 0, sizeof(*out)); + out->type = ACTION_NONE; + + if (value == NULL) { + return -1; + } + strncpy(buf, value, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + p = buf; + while (*p == ' ') p++; + if (*p == '\0') { + return -1; + } + + tok = (char *)p; + while (*p != '\0' && *p != ' ') p++; + if (*p != '\0') { + *p = '\0'; + p++; + } + + if (strcasecmp(tok, KEY_PREFIX) == 0) { + /* Parse key combo: key KEY_MOD KEY_KEY [KEY_KEY ...] e.g. key KEY_LEFTCTRL KEY_X */ + n = 0; + while (n < MAX_MACRO_KEYS) { + while (*p == ' ') p++; + if (*p == '\0') break; + tok = (char *)p; + while (*p != '\0' && *p != ' ') p++; + if (*p != '\0') { *p = '\0'; p++; } + code = key_name_to_code(tok); + if (code < 0) return -1; + keys[n++] = (uint16_t)code; + } + if (n == 0) return -1; + out->key_codes = malloc(n * sizeof(uint16_t)); + if (out->key_codes == NULL) return -1; + memcpy(out->key_codes, keys, n * sizeof(uint16_t)); + out->key_len = n; + out->type = ACTION_KEY; + return 0; + } + + if (strcasecmp(tok, MACRO_PREFIX) == 0) { + while (n < MAX_MACRO_KEYS) { + while (*p == ' ') p++; + if (*p == '\0') { + break; + } + tok = (char *)p; + while (*p != '\0' && *p != ' ') p++; + if (*p != '\0') { + *p = '\0'; + p++; + } + code = key_name_to_code(tok); + if (code < 0) { + return -1; + } + keys[n++] = (uint16_t)code; + } + if (n == 0) { + return -1; + } + out->macro_keys = malloc(n * sizeof(uint16_t)); + if (out->macro_keys == NULL) { + return -1; + } + memcpy(out->macro_keys, keys, n * sizeof(uint16_t)); + out->macro_len = n; + out->type = ACTION_MACRO; + return 0; + } + + return -1; +} + +static int section_to_layer(const char *section) +{ + if (section == NULL) { + return -1; + } + if (strcasecmp(section, "Mode1") == 0) return 0; + if (strcasecmp(section, "Mode2") == 0) return 2; + if (strcasecmp(section, "Mode3") == 0) return 4; + if (strcasecmp(section, "Mode1.Shift") == 0) return 1; + if (strcasecmp(section, "Mode2.Shift") == 0) return 3; + if (strcasecmp(section, "Mode3.Shift") == 0) return 5; + return -1; +} + +static void free_action(profile_action_t *a) +{ + if (a->type == ACTION_KEY && a->key_codes != NULL) { + free(a->key_codes); + a->key_codes = NULL; + a->key_len = 0; + } + if (a->type == ACTION_MACRO && a->macro_keys != NULL) { + free(a->macro_keys); + a->macro_keys = NULL; + a->macro_len = 0; + } + a->type = ACTION_NONE; +} + +static int profile_ini_handler(void *user, const char *section, const char *key, + const char *value) +{ + int layer; + int btn; + profile_action_t action; + profile_action_t (*arr)[NUM_LAYERS][LIBX52IO_BUTTON_MAX] = user; + + /* [Profile] section: profile-wide options (Name, ShiftButton) */ + if (section != NULL && strcasecmp(section, PROFILE_SECTION) == 0) { + if (key != NULL && value != NULL) { + if (strcasecmp(key, PROFILE_NAME_KEY) == 0) { + strncpy(profile_name_str, value, PROFILE_NAME_LEN - 1); + profile_name_str[PROFILE_NAME_LEN - 1] = '\0'; + } else if (strcasecmp(key, SHIFT_BUTTON_KEY) == 0) { + strncpy(profile_shift_button_str, value, SHIFT_BUTTON_STR_LEN - 1); + profile_shift_button_str[SHIFT_BUTTON_STR_LEN - 1] = '\0'; + } + } + return 1; + } + + layer = section_to_layer(section); + if (layer < 0) { + return 1; /* unknown section, ignore */ + } + + if (key == NULL || value == NULL) { + return 1; + } + if (strncasecmp(key, BUTTON_PREFIX, BUTTON_PREFIX_LEN) != 0) { + return 1; + } + key += BUTTON_PREFIX_LEN; + btn = button_name_to_index(key); + if (btn < 0) { + PINELOG_INFO(_("Unknown button name in profile: %s"), key); + return 1; + } + + if (parse_action_value(value, &action) != 0) { + PINELOG_INFO(_("Invalid action value for %s.%s: %s"), section, key, value); + return 1; + } + + free_action(&(*arr)[layer][btn]); + (*arr)[layer][btn] = action; + return 1; +} + +static void clear_all_layers(void) +{ + int layer; + libx52io_button btn; + + for (layer = 0; layer < NUM_LAYERS; layer++) { + for (btn = LIBX52IO_BTN_TRIGGER; btn < LIBX52IO_BUTTON_MAX; btn++) { + free_action(&layers[layer][btn]); + } + } +} + +static void load_profile(void) +{ + const char *dir; + const char *name; + char path[PATH_MAX]; + int rc; + + clear_all_layers(); + profile_loaded = false; + shift_button_index = -1; + profile_name_str[0] = '\0'; + profile_shift_button_str[0] = '\0'; + + dir = x52d_config_get("Profiles", "Directory"); + name = x52d_config_get("Profiles", "Profile"); + if (dir == NULL || *dir == '\0' || name == NULL || *name == '\0') { + PINELOG_INFO(_("Profile directory or name not set, using empty profile")); + return; + } + + rc = snprintf(path, sizeof(path), "%s/%s.conf", dir, name); + if (rc < 0 || (size_t)rc >= sizeof(path)) { + PINELOG_ERROR(_("Profile path too long")); + return; + } + + rc = ini_parse(path, profile_ini_handler, &layers); + if (rc < 0) { + PINELOG_ERROR(_("Failed to load profile %s: %s"), path, strerror(errno)); + return; + } + if (rc > 0) { + PINELOG_WARN(_("Profile %s had %d parse errors"), path, rc); + } + + profile_loaded = true; + + if (profile_name_str[0] != '\0') { + PINELOG_INFO(_("Loaded profile: %s"), profile_name_str); + } + + /* Resolve shift button from [Profile] ShiftButton; default BTN_PINKY if not set */ + name = (profile_shift_button_str[0] != '\0') + ? profile_shift_button_str + : DEFAULT_SHIFT_BUTTON; + shift_button_index = button_name_to_index(name); + if (shift_button_index < 0) { + PINELOG_WARN(_("Unknown ShiftButton '%s' in profile, shift disabled"), name); + } +} + +void x52d_profile_init(void) +{ + load_profile(); + PINELOG_INFO(_("Profile module initialized")); +} + +const char *x52d_profile_get_name(void) +{ + if (!profile_loaded || profile_name_str[0] == '\0') { + return NULL; + } + return profile_name_str; +} + +void x52d_profile_exit(void) +{ + clear_all_layers(); + shift_button_index = -1; + profile_name_str[0] = '\0'; + profile_shift_button_str[0] = '\0'; + profile_loaded = false; + PINELOG_INFO(_("Profile module shut down")); +} + +/* + * Fallback chain per (mode, shift): try these layer indices in order; + * use the first that has a mapping for the button. -1 terminates. + * Layer order: 0=Mode1, 1=Mode1.Shift, 2=Mode2, 3=Mode2.Shift, 4=Mode3, 5=Mode3.Shift. + * - Shift: ModeN.Shift falls back to ModeN. + * - Mode: Mode2 falls back to Mode1; Mode3 falls back to Mode2 then Mode1. + */ +static const int fallback_chain[NUM_LAYERS][MAX_FALLBACK] = { + { 0, -1, -1, -1 }, /* mode 1, no shift: Mode1 only */ + { 1, 0, -1, -1 }, /* mode 1, shift: Mode1.Shift then Mode1 */ + { 2, 0, -1, -1 }, /* mode 2, no shift: Mode2 then Mode1 */ + { 3, 2, 0, -1 }, /* mode 2, shift: Mode2.Shift then Mode2 then Mode1 */ + { 4, 2, 0, -1 }, /* mode 3, no shift: Mode3 then Mode2 then Mode1 */ + { 5, 4, 2, 0 }, /* mode 3, shift: Mode3.Shift then Mode3 then Mode2 then Mode1 */ +}; + +static unsigned int get_layer_index(const libx52io_report *report) +{ + unsigned int mode; + bool shift; + + mode = report->mode; + if (mode < 1) { + mode = 1; + } + if (mode > 3) { + mode = 3; + } + shift = (shift_button_index >= 0 && + shift_button_index < LIBX52IO_BUTTON_MAX && + report->button[shift_button_index]); + return (mode - 1u) * 2u + (shift ? 1u : 0u); +} + +/* Return the first non-NONE action for btn in the fallback chain for this report. */ +static const profile_action_t *get_action_for_button(const libx52io_report *report, + libx52io_button btn) +{ + unsigned int chain_index; + int layer; + const profile_action_t *a; + int i; + + chain_index = get_layer_index(report); + for (i = 0; i < MAX_FALLBACK; i++) { + layer = fallback_chain[chain_index][i]; + if (layer < 0) { + break; + } + a = &layers[layer][btn]; + if (a->type != ACTION_NONE) { + return a; + } + } + return NULL; +} + +static void emit_macro(const profile_action_t *a) +{ + size_t i; + + if (a->type != ACTION_MACRO || a->macro_keys == NULL || !x52d_keyboard_evdev_available()) { + return; + } + for (i = 0; i < a->macro_len; i++) { + x52d_keyboard_evdev_key(a->macro_keys[i], 1); + x52d_keyboard_evdev_key(a->macro_keys[i], 0); + } +} + +void x52d_profile_apply(const libx52io_report *report, + const libx52io_report *prev) +{ + libx52io_button btn; + const profile_action_t *a; + bool pressed; + bool was_pressed; + + if (!profile_loaded || !x52d_keyboard_evdev_available()) { + return; + } + + for (btn = LIBX52IO_BTN_TRIGGER; btn < LIBX52IO_BUTTON_MAX; btn++) { + was_pressed = prev->button[btn]; + pressed = report->button[btn]; + if (pressed == was_pressed) { + continue; + } + + a = get_action_for_button(report, btn); + if (a == NULL) { + continue; + } + + if (pressed) { + if (a->type == ACTION_KEY && a->key_codes != NULL) { + size_t i; + for (i = 0; i < a->key_len; i++) { + x52d_keyboard_evdev_key(a->key_codes[i], 1); + } + } else if (a->type == ACTION_MACRO) { + emit_macro(a); + } + } else { + if (a->type == ACTION_KEY && a->key_codes != NULL) { + size_t i; + for (i = a->key_len; i > 0; i--) { + x52d_keyboard_evdev_key(a->key_codes[i - 1], 0); + } + } + /* macro: nothing on release */ + } + } +} diff --git a/daemon/x52d_profile.h b/daemon/x52d_profile.h new file mode 100644 index 0000000..9c56f4e --- /dev/null +++ b/daemon/x52d_profile.h @@ -0,0 +1,71 @@ +/* + * Saitek X52 Pro MFD & LED driver - Profile (button-to-keyboard mapping) + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#ifndef X52D_PROFILE_H +#define X52D_PROFILE_H + +#include "libx52io.h" + +/** + * @file x52d_profile.h + * @brief Profile loading and application: map joystick buttons to keyboard events + */ + +#if defined(HAVE_EVDEV) + +/** + * @brief Initialize the profile module and load the active profile. + * + * Reads Profiles.Directory and Profiles.Profile from config, loads + * the profile file, and resolves ShiftButton from the [Profile] section. If loading + * fails, the profile is empty (no mappings). + */ +void x52d_profile_init(void); + +/** + * @brief Shut down the profile module and free resources. + */ +void x52d_profile_exit(void); + +/** + * @brief Return the profile display name from the loaded profile file. + * + * The name is set in the profile file with [Profile] Name = ... + * @return The profile name, or NULL if none set or no profile loaded. + * Valid until x52d_profile_exit() or next x52d_profile_init(). + */ +const char *x52d_profile_get_name(void); + +/** + * @brief Apply profile mappings to button state changes. + * + * Compares @a report with @a prev and emits keyboard events for any + * button that has a mapping in the current (mode, shift) layer. + * Single-key mappings: key down on press, key up on release. + * Macro mappings: sequence of key down/up on button down only. + * + * @param report Current joystick report. + * @param prev Previous report (for edge detection). + */ +void x52d_profile_apply(const libx52io_report *report, + const libx52io_report *prev); + +#else + +static inline void x52d_profile_init(void) { (void)0; } +static inline void x52d_profile_exit(void) { (void)0; } +static inline const char *x52d_profile_get_name(void) { return NULL; } +static inline void x52d_profile_apply(const libx52io_report *report, + const libx52io_report *prev) { + (void)report; + (void)prev; +} + +#endif /* HAVE_EVDEV */ + +#endif /* !defined X52D_PROFILE_H */ diff --git a/daemon/x52d_profile_test.c b/daemon/x52d_profile_test.c new file mode 100644 index 0000000..791250a --- /dev/null +++ b/daemon/x52d_profile_test.c @@ -0,0 +1,325 @@ +/* + * Saitek X52 Pro MFD & LED driver - Profile module tests + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include "config.h" +#include +#include +#include +#include +#include +#include +#include + +#include "libevdev/libevdev.h" +#include "libx52io.h" + +#define PINELOG_MODULE X52D_MOD_PROFILE +#include "pinelog.h" + +#include "x52d_config.h" +#include "x52d_const.h" +#include "x52d_keyboard.h" +#include "x52d_profile.h" + +/* Stub keyboard: record (key_code, value) for assertions */ +#define MAX_RECORDED 32 +static struct { uint16_t code; int value; } key_record[MAX_RECORDED]; +static size_t key_record_n; + +int x52d_keyboard_evdev_key(uint16_t key_code, int value) +{ + if (key_record_n < MAX_RECORDED) { + key_record[key_record_n].code = key_code; + key_record[key_record_n].value = value; + key_record_n++; + } + return 0; +} + +bool x52d_keyboard_evdev_available(void) +{ + return true; +} + +static void key_record_reset(void) +{ + key_record_n = 0; +} + +static int key_code_from_name(const char *name) +{ + int c = libevdev_event_code_from_name(EV_KEY, name); + return (c >= 0) ? c : -1; +} + +static void write_profile_file(const char *path, const char *content) +{ + FILE *f = fopen(path, "w"); + assert_non_null(f); + assert_true(fputs(content, f) >= 0); + fclose(f); +} + +static void test_profile_single_key(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Mode1]\n" + "Button.BTN_FIRE = key KEY_E\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 1; + report.button[LIBX52IO_BTN_FIRE] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 1); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_E")); + assert_int_equal(key_record[0].value, 1); + + memcpy(&prev, &report, sizeof(report)); + report.button[LIBX52IO_BTN_FIRE] = false; + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 1); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_E")); + assert_int_equal(key_record[0].value, 0); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_key_combo(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Mode1]\n" + "Button.BTN_A = key KEY_LEFTCTRL KEY_X\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 1; + report.button[LIBX52IO_BTN_A] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 2); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_LEFTCTRL")); + assert_int_equal(key_record[0].value, 1); + assert_int_equal(key_record[1].code, key_code_from_name("KEY_X")); + assert_int_equal(key_record[1].value, 1); + + memcpy(&prev, &report, sizeof(report)); + report.button[LIBX52IO_BTN_A] = false; + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 2); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_X")); + assert_int_equal(key_record[0].value, 0); + assert_int_equal(key_record[1].code, key_code_from_name("KEY_LEFTCTRL")); + assert_int_equal(key_record[1].value, 0); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_macro_button_down_only(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Mode1]\n" + "Button.BTN_T1_UP = macro KEY_A KEY_B\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 1; + report.button[LIBX52IO_BTN_T1_UP] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 4); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_A")); + assert_int_equal(key_record[0].value, 1); + assert_int_equal(key_record[1].code, key_code_from_name("KEY_A")); + assert_int_equal(key_record[1].value, 0); + assert_int_equal(key_record[2].code, key_code_from_name("KEY_B")); + assert_int_equal(key_record[2].value, 1); + assert_int_equal(key_record[3].code, key_code_from_name("KEY_B")); + assert_int_equal(key_record[3].value, 0); + + memcpy(&prev, &report, sizeof(report)); + report.button[LIBX52IO_BTN_T1_UP] = false; + key_record_reset(); + x52d_profile_apply(&report, &prev); + assert_int_equal(key_record_n, 0); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_fallback_mode2_to_mode1(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Mode1]\n" + "Button.BTN_FIRE = key KEY_E\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 2; + report.button[LIBX52IO_BTN_FIRE] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 1); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_E")); + assert_int_equal(key_record[0].value, 1); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_shift_layer(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + libx52io_report report, prev; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Profile]\n" + "ShiftButton=BTN_PINKY\n" + "[Mode1]\n" + "Button.BTN_FIRE = key KEY_E\n" + "[Mode1.Shift]\n" + "Button.BTN_FIRE = key KEY_F\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + memset(&report, 0, sizeof(report)); + memset(&prev, 0, sizeof(prev)); + report.mode = 1; + report.button[LIBX52IO_BTN_PINKY] = true; + report.button[LIBX52IO_BTN_FIRE] = true; + + key_record_reset(); + x52d_profile_apply(&report, &prev); + + assert_int_equal(key_record_n, 1); + assert_int_equal(key_record[0].code, key_code_from_name("KEY_F")); + assert_int_equal(key_record[0].value, 1); + + x52d_profile_exit(); + unlink(path); + rmdir(tmpdir); +} + +static void test_profile_get_name(void **state) +{ + char tmpdir[] = "/tmp/x52d_profile_test_XXXXXX"; + char path[256]; + const char *name; + + (void)state; + assert_non_null(mkdtemp(tmpdir)); + + snprintf(path, sizeof(path), "%s/test.conf", tmpdir); + write_profile_file(path, + "[Profile]\n" + "Name=My Test Profile\n" + "[Mode1]\n" + "Button.BTN_FIRE = key KEY_E\n"); + + x52d_config_set("Profiles", "Directory", tmpdir); + x52d_config_set("Profiles", "Profile", "test"); + x52d_profile_init(); + + name = x52d_profile_get_name(); + assert_non_null(name); + assert_string_equal(name, "My Test Profile"); + + x52d_profile_exit(); + name = x52d_profile_get_name(); + assert_null(name); + unlink(path); + rmdir(tmpdir); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_profile_single_key), + cmocka_unit_test(test_profile_key_combo), + cmocka_unit_test(test_profile_macro_button_down_only), + cmocka_unit_test(test_profile_fallback_mode2_to_mode1), + cmocka_unit_test(test_profile_shift_layer), + cmocka_unit_test(test_profile_get_name), + }; + cmocka_set_message_output(CM_OUTPUT_TAP); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/po/POTFILES.in b/po/POTFILES.in index 70c78d3..3302f68 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -14,6 +14,7 @@ joytest/x52_test_mfd.c daemon/x52d_main.c daemon/x52d_client.c +daemon/x52d_clutch.c daemon/x52d_clock.c daemon/x52d_command.c daemon/x52d_config.c @@ -21,7 +22,9 @@ daemon/x52d_config_dump.c daemon/x52d_config_parser.c daemon/x52d_device.c daemon/x52d_io.c +daemon/x52d_keyboard_evdev.c daemon/x52d_mouse.c daemon/x52d_mouse_evdev.c daemon/x52d_notify.c +daemon/x52d_profile.c daemon/x52ctl.c diff --git a/po/libx52.pot b/po/libx52.pot index 2b72451..238f8cb 100644 --- a/po/libx52.pot +++ b/po/libx52.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: libx52 0.3.2\n" "Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n" -"POT-Creation-Date: 2024-06-09 20:24-0700\n" +"POT-Creation-Date: 2026-02-26 22:35-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -493,17 +493,17 @@ msgstr "" msgid "OK" msgstr "" -#: daemon/x52d_main.c:64 +#: daemon/x52d_main.c:66 #, c-format msgid "Error %d setting log file: %s\n" msgstr "" -#: daemon/x52d_main.c:80 +#: daemon/x52d_main.c:82 #, c-format msgid "Error %d installing handler for signal %d: %s" msgstr "" -#: daemon/x52d_main.c:91 +#: daemon/x52d_main.c:93 #, c-format msgid "" "Usage: %s [-f] [-v] [-q]\n" @@ -513,88 +513,88 @@ msgid "" "\t[-b notify-socket-path]\n" msgstr "" -#: daemon/x52d_main.c:124 +#: daemon/x52d_main.c:126 #, c-format msgid "Daemon is already running as PID %u" msgstr "" -#: daemon/x52d_main.c:266 +#: daemon/x52d_main.c:268 #, c-format msgid "Unable to parse configuration override '%s'\n" msgstr "" -#: daemon/x52d_main.c:298 +#: daemon/x52d_main.c:300 #, c-format msgid "Foreground = %s" msgstr "" -#: daemon/x52d_main.c:298 daemon/x52d_main.c:299 +#: daemon/x52d_main.c:300 daemon/x52d_main.c:301 msgid "true" msgstr "" -#: daemon/x52d_main.c:298 daemon/x52d_main.c:299 +#: daemon/x52d_main.c:300 daemon/x52d_main.c:301 msgid "false" msgstr "" -#: daemon/x52d_main.c:299 -#, c-format -msgid "Quiet = %s" -msgstr "" - -#: daemon/x52d_main.c:300 -#, c-format -msgid "Verbosity = %d" -msgstr "" - #: daemon/x52d_main.c:301 #, c-format -msgid "Log file = %s" +msgid "Quiet = %s" msgstr "" #: daemon/x52d_main.c:302 #, c-format -msgid "Config file = %s" +msgid "Verbosity = %d" msgstr "" #: daemon/x52d_main.c:303 #, c-format -msgid "PID file = %s" +msgid "Log file = %s" msgstr "" #: daemon/x52d_main.c:304 #, c-format -msgid "Command socket = %s" +msgid "Config file = %s" msgstr "" #: daemon/x52d_main.c:305 #, c-format +msgid "PID file = %s" +msgstr "" + +#: daemon/x52d_main.c:306 +#, c-format +msgid "Command socket = %s" +msgstr "" + +#: daemon/x52d_main.c:307 +#, c-format msgid "Notify socket = %s" msgstr "" -#: daemon/x52d_main.c:316 +#: daemon/x52d_main.c:318 #, c-format msgid "Error %d blocking signals on child threads: %s" msgstr "" -#: daemon/x52d_main.c:335 +#: daemon/x52d_main.c:339 #, c-format msgid "Error %d unblocking signals on child threads: %s" msgstr "" -#: daemon/x52d_main.c:348 +#: daemon/x52d_main.c:352 msgid "Reloading X52 configuration" msgstr "" -#: daemon/x52d_main.c:355 +#: daemon/x52d_main.c:359 msgid "Saving X52 configuration to disk" msgstr "" -#: daemon/x52d_main.c:361 +#: daemon/x52d_main.c:365 #, c-format msgid "Received termination signal %s" msgstr "" -#: daemon/x52d_main.c:378 +#: daemon/x52d_main.c:384 msgid "Shutting down X52 daemon" msgstr "" @@ -622,6 +622,16 @@ msgstr "" msgid "Timed out when polling" msgstr "" +#: daemon/x52d_clutch.c:96 +#, c-format +msgid "Cannot open profiles directory %s: %s" +msgstr "" + +#: daemon/x52d_clutch.c:207 +#, c-format +msgid "Profile switched to: %s" +msgstr "" + #: daemon/x52d_clock.c:28 #, c-format msgid "Setting clock enable to %s" @@ -676,41 +686,41 @@ msgstr "" msgid "Shutting down X52 clock manager thread" msgstr "" -#: daemon/x52d_command.c:367 +#: daemon/x52d_command.c:370 #, c-format msgid "Error reading from client %d: %s" msgstr "" -#: daemon/x52d_command.c:378 +#: daemon/x52d_command.c:381 #, c-format msgid "Short write to client %d; expected %d bytes, wrote %d bytes" msgstr "" -#: daemon/x52d_command.c:402 +#: daemon/x52d_command.c:405 #, c-format msgid "Error %d during command loop: %s" msgstr "" -#: daemon/x52d_command.c:429 +#: daemon/x52d_command.c:432 #, c-format msgid "Error creating command socket: %s" msgstr "" -#: daemon/x52d_command.c:437 +#: daemon/x52d_command.c:440 #, c-format msgid "Error marking command socket as nonblocking: %s" msgstr "" -#: daemon/x52d_command.c:443 +#: daemon/x52d_command.c:446 #, c-format msgid "Error listening on command socket: %s" msgstr "" -#: daemon/x52d_command.c:447 +#: daemon/x52d_command.c:450 msgid "Starting command processing thread" msgstr "" -#: daemon/x52d_command.c:465 +#: daemon/x52d_command.c:468 msgid "Shutting down command processing thread" msgstr "" @@ -808,34 +818,52 @@ msgstr "" msgid "Error %d when updating X52 device: %s" msgstr "" -#: daemon/x52d_io.c:42 +#: daemon/x52d_io.c:52 msgid "Starting X52 I/O thread" msgstr "" -#: daemon/x52d_io.c:64 +#: daemon/x52d_io.c:74 #, c-format msgid "Error %d opening X52 I/O device: %s" msgstr "" -#: daemon/x52d_io.c:75 +#: daemon/x52d_io.c:85 #, c-format msgid "Error %d reading from X52 I/O device: %s" msgstr "" -#: daemon/x52d_io.c:102 +#: daemon/x52d_io.c:112 #, c-format msgid "Error %d initializing X52 I/O library: %s" msgstr "" -#: daemon/x52d_io.c:108 +#: daemon/x52d_io.c:118 #, c-format msgid "Error %d initializing I/O driver thread: %s" msgstr "" -#: daemon/x52d_io.c:115 +#: daemon/x52d_io.c:125 msgid "Shutting down X52 I/O driver thread" msgstr "" +#: daemon/x52d_keyboard_evdev.c:40 +#, c-format +msgid "Error writing key event (code %u, value %d): %s" +msgstr "" + +#: daemon/x52d_keyboard_evdev.c:47 +msgid "Error writing keyboard sync event" +msgstr "" + +#: daemon/x52d_keyboard_evdev.c:78 +#, c-format +msgid "Error %d creating X52 virtual keyboard: %s" +msgstr "" + +#: daemon/x52d_keyboard_evdev.c:82 +msgid "X52 virtual keyboard created" +msgstr "" + #: daemon/x52d_mouse.c:31 #, c-format msgid "Setting mouse enable to %s" @@ -941,6 +969,52 @@ msgstr "" msgid "Error %d initializing notify listener: %s" msgstr "" +#: daemon/x52d_profile.c:246 +#, c-format +msgid "Unknown button name in profile: %s" +msgstr "" + +#: daemon/x52d_profile.c:251 +#, c-format +msgid "Invalid action value for %s.%s: %s" +msgstr "" + +#: daemon/x52d_profile.c:288 +msgid "Profile directory or name not set, using empty profile" +msgstr "" + +#: daemon/x52d_profile.c:294 +msgid "Profile path too long" +msgstr "" + +#: daemon/x52d_profile.c:300 +#, c-format +msgid "Failed to load profile %s: %s" +msgstr "" + +#: daemon/x52d_profile.c:304 +#, c-format +msgid "Profile %s had %d parse errors" +msgstr "" + +#: daemon/x52d_profile.c:310 +#, c-format +msgid "Loaded profile: %s" +msgstr "" + +#: daemon/x52d_profile.c:319 +#, c-format +msgid "Unknown ShiftButton '%s' in profile, shift disabled" +msgstr "" + +#: daemon/x52d_profile.c:326 +msgid "Profile module initialized" +msgstr "" + +#: daemon/x52d_profile.c:344 +msgid "Profile module shut down" +msgstr "" + #: daemon/x52ctl.c:60 #, c-format msgid "Usage: %s [-i] [-s socket-path] [command]\n" diff --git a/po/xx_PL.po b/po/xx_PL.po index 6fd1039..a0a161d 100644 --- a/po/xx_PL.po +++ b/po/xx_PL.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: libx52 0.2.3\n" "Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n" -"POT-Creation-Date: 2024-06-09 20:24-0700\n" +"POT-Creation-Date: 2026-02-26 22:35-0800\n" "PO-Revision-Date: 2023-01-04 08:40-0800\n" "Last-Translator: Nirenjan Krishnan \n" "Language-Team: Dummy Language for testing i18n\n" @@ -536,17 +536,17 @@ msgstr "Estingtay aracterchay 0x%02x..." msgid "OK" msgstr "OKay" -#: daemon/x52d_main.c:64 +#: daemon/x52d_main.c:66 #, c-format msgid "Error %d setting log file: %s\n" msgstr "Erroray %d ettingsay oglay ilefay: %s\n" -#: daemon/x52d_main.c:80 +#: daemon/x52d_main.c:82 #, c-format msgid "Error %d installing handler for signal %d: %s" msgstr "Erroray %d installingay andlerhay orfay ignalsay %d: %s" -#: daemon/x52d_main.c:91 +#: daemon/x52d_main.c:93 #, c-format msgid "" "Usage: %s [-f] [-v] [-q]\n" @@ -562,88 +562,88 @@ msgstr "" "\t[-b otifynay-ocketsay-athpay]\n" "\n" -#: daemon/x52d_main.c:124 +#: daemon/x52d_main.c:126 #, c-format msgid "Daemon is already running as PID %u" msgstr "Aemonday isay alreadyay unningray asay IDPay %u" -#: daemon/x52d_main.c:266 +#: daemon/x52d_main.c:268 #, c-format msgid "Unable to parse configuration override '%s'\n" msgstr "Unableay otay arsepay onfigurationcay overrideay '%s'\n" -#: daemon/x52d_main.c:298 +#: daemon/x52d_main.c:300 #, c-format msgid "Foreground = %s" msgstr "Oregroundfay = %s" -#: daemon/x52d_main.c:298 daemon/x52d_main.c:299 +#: daemon/x52d_main.c:300 daemon/x52d_main.c:301 msgid "true" msgstr "uetray" -#: daemon/x52d_main.c:298 daemon/x52d_main.c:299 +#: daemon/x52d_main.c:300 daemon/x52d_main.c:301 msgid "false" msgstr "alsefay" -#: daemon/x52d_main.c:299 +#: daemon/x52d_main.c:301 #, c-format msgid "Quiet = %s" msgstr "Uietqay = %s" -#: daemon/x52d_main.c:300 +#: daemon/x52d_main.c:302 #, c-format msgid "Verbosity = %d" msgstr "Erbosityvay = %d" -#: daemon/x52d_main.c:301 +#: daemon/x52d_main.c:303 #, c-format msgid "Log file = %s" msgstr "Oglay ilefay = %s" -#: daemon/x52d_main.c:302 +#: daemon/x52d_main.c:304 #, c-format msgid "Config file = %s" msgstr "Onfigcay ilefay = %s" -#: daemon/x52d_main.c:303 +#: daemon/x52d_main.c:305 #, c-format msgid "PID file = %s" msgstr "IDPay ilefay = %s" -#: daemon/x52d_main.c:304 +#: daemon/x52d_main.c:306 #, c-format msgid "Command socket = %s" msgstr "Ommandcay ocketsay = %s" -#: daemon/x52d_main.c:305 +#: daemon/x52d_main.c:307 #, c-format msgid "Notify socket = %s" msgstr "Otifynay ocketsay = %s" -#: daemon/x52d_main.c:316 +#: daemon/x52d_main.c:318 #, c-format msgid "Error %d blocking signals on child threads: %s" msgstr "Erroray %d ockingblay ignalssay onay ildchay eadsthray: %s" -#: daemon/x52d_main.c:335 +#: daemon/x52d_main.c:339 #, c-format msgid "Error %d unblocking signals on child threads: %s" msgstr "Erroray %d unblockingay ignalssay onay ildchay eadsthray: %s" -#: daemon/x52d_main.c:348 +#: daemon/x52d_main.c:352 msgid "Reloading X52 configuration" msgstr "Eloadingray X52 onfigurationcay" -#: daemon/x52d_main.c:355 +#: daemon/x52d_main.c:359 msgid "Saving X52 configuration to disk" msgstr "Avingsay X52 onfigurationcay otay iskday" -#: daemon/x52d_main.c:361 +#: daemon/x52d_main.c:365 #, c-format msgid "Received termination signal %s" msgstr "Eceivedray erminationtay ignalsay %s" -#: daemon/x52d_main.c:378 +#: daemon/x52d_main.c:384 msgid "Shutting down X52 daemon" msgstr "Uttingshay ownday X52 aemonday" @@ -671,6 +671,16 @@ msgstr "Erroray %d enwhay ollingpay %d escriptorsday: %s" msgid "Timed out when polling" msgstr "Imedtay outay enwhay ollingpay" +#: daemon/x52d_clutch.c:96 +#, c-format +msgid "Cannot open profiles directory %s: %s" +msgstr "Annotcay openay ofilespray irectoryday %s: %s" + +#: daemon/x52d_clutch.c:207 +#, c-format +msgid "Profile switched to: %s" +msgstr "Ofilepray itchedsway otay: %s" + #: daemon/x52d_clock.c:28 #, c-format msgid "Setting clock enable to %s" @@ -728,42 +738,42 @@ msgstr "Erroray %d initializingay ockclay eadthray: %s" msgid "Shutting down X52 clock manager thread" msgstr "Uttingshay ownday X52 ockclay anagermay eadthray" -#: daemon/x52d_command.c:367 +#: daemon/x52d_command.c:370 #, c-format msgid "Error reading from client %d: %s" msgstr "Erroray eadingray omfray ientclay %d: %s" -#: daemon/x52d_command.c:378 +#: daemon/x52d_command.c:381 #, c-format msgid "Short write to client %d; expected %d bytes, wrote %d bytes" msgstr "" "Ortshay itewray otay ientclay %d; expecteday %d ytesbay, otewray %d ytesbay" -#: daemon/x52d_command.c:402 +#: daemon/x52d_command.c:405 #, c-format msgid "Error %d during command loop: %s" msgstr "Erroray %d uringday ommandcay ooplay: %s" -#: daemon/x52d_command.c:429 +#: daemon/x52d_command.c:432 #, c-format msgid "Error creating command socket: %s" msgstr "Erroray eatingcray ommandcay ocketsay: %s" -#: daemon/x52d_command.c:437 +#: daemon/x52d_command.c:440 #, c-format msgid "Error marking command socket as nonblocking: %s" msgstr "Erroray arkingmay ommandcay ocketsay asay onblockingnay: %s" -#: daemon/x52d_command.c:443 +#: daemon/x52d_command.c:446 #, c-format msgid "Error listening on command socket: %s" msgstr "Erroray isteninglay onay ommandcay ocketsay: %s" -#: daemon/x52d_command.c:447 +#: daemon/x52d_command.c:450 msgid "Starting command processing thread" msgstr "Artingstay ommandcay ocessingpray eadthray" -#: daemon/x52d_command.c:465 +#: daemon/x52d_command.c:468 msgid "Shutting down command processing thread" msgstr "Uttingshay ownday ommandcay ocessingpray eadthray" @@ -861,34 +871,52 @@ msgstr "Erroray %d enwhay updatingay X52 arameterpay: %s" msgid "Error %d when updating X52 device: %s" msgstr "Erroray %d enwhay updatingay X52 eviceday: %s" -#: daemon/x52d_io.c:42 +#: daemon/x52d_io.c:52 msgid "Starting X52 I/O thread" msgstr "Artingstay X52 I/O eadthray" -#: daemon/x52d_io.c:64 +#: daemon/x52d_io.c:74 #, c-format msgid "Error %d opening X52 I/O device: %s" msgstr "Erroray %d openingay X52 I/O eviceday: %s" -#: daemon/x52d_io.c:75 +#: daemon/x52d_io.c:85 #, c-format msgid "Error %d reading from X52 I/O device: %s" msgstr "Erroray %d eadingray omfray X52 I/O eviceday: %s" -#: daemon/x52d_io.c:102 +#: daemon/x52d_io.c:112 #, c-format msgid "Error %d initializing X52 I/O library: %s" msgstr "Erroray %d initializingay X52 ibrarylay: %s" -#: daemon/x52d_io.c:108 +#: daemon/x52d_io.c:118 #, c-format msgid "Error %d initializing I/O driver thread: %s" msgstr "Erroray %d initializingay I/O iverdray eadthray: %s" -#: daemon/x52d_io.c:115 +#: daemon/x52d_io.c:125 msgid "Shutting down X52 I/O driver thread" msgstr "Uttingshay ownday X52 I/O iverdray eadthray" +#: daemon/x52d_keyboard_evdev.c:40 +#, c-format +msgid "Error writing key event (code %u, value %d): %s" +msgstr "Erroray itingwray eykay eventay (odecay %u, aluevay %d): %s" + +#: daemon/x52d_keyboard_evdev.c:47 +msgid "Error writing keyboard sync event" +msgstr "Erroray itingwray eyboardkay yncsay eventay" + +#: daemon/x52d_keyboard_evdev.c:78 +#, c-format +msgid "Error %d creating X52 virtual keyboard: %s" +msgstr "Erroray %d eatingcray X52 irtualvay eyboardkay: %s" + +#: daemon/x52d_keyboard_evdev.c:82 +msgid "X52 virtual keyboard created" +msgstr "X52 irtualvay eyboardkey eatedcray" + #: daemon/x52d_mouse.c:31 #, c-format msgid "Setting mouse enable to %s" @@ -994,6 +1022,53 @@ msgstr "Erroray %d initializingay otifynay eadthray: %s" msgid "Error %d initializing notify listener: %s" msgstr "Erroray %d initializingay otifynay istenerlay: %s" +#: daemon/x52d_profile.c:246 +#, c-format +msgid "Unknown button name in profile: %s" +msgstr "Unknownay uttonbay amenay inay ofilepray: %s" + +#: daemon/x52d_profile.c:251 +#, c-format +msgid "Invalid action value for %s.%s: %s" +msgstr "Invaliday actionay aluevay orfay %s.%s: %s" + +#: daemon/x52d_profile.c:288 +msgid "Profile directory or name not set, using empty profile" +msgstr "" +"Ofilepray irectoryday oray amenay otnay etsay, usingay emptyay ofilepray" + +#: daemon/x52d_profile.c:294 +msgid "Profile path too long" +msgstr "Ofilepray athpay ootay onglay" + +#: daemon/x52d_profile.c:300 +#, c-format +msgid "Failed to load profile %s: %s" +msgstr "Ailedfay otay oadlay ofilepray %s: %s" + +#: daemon/x52d_profile.c:304 +#, c-format +msgid "Profile %s had %d parse errors" +msgstr "Ofilepray %s adhay %d arsepay errorsay" + +#: daemon/x52d_profile.c:310 +#, c-format +msgid "Loaded profile: %s" +msgstr "Oadedlay ofilepray: %s" + +#: daemon/x52d_profile.c:319 +#, c-format +msgid "Unknown ShiftButton '%s' in profile, shift disabled" +msgstr "Unknownay iftButtonShay '%s' inay ofilepray, iftshay isabledday" + +#: daemon/x52d_profile.c:326 +msgid "Profile module initialized" +msgstr "Ofilepray odulemay initializeday" + +#: daemon/x52d_profile.c:344 +msgid "Profile module shut down" +msgstr "Ofilepray odulemay utshay ownday" + #: daemon/x52ctl.c:60 #, c-format msgid "Usage: %s [-i] [-s socket-path] [command]\n"