feat: Add profile support to x52d

This change adds initial support for profiles, which map joystick button
events into keyboard events. This supports single key/key combo events
which map directly to the button state, as well as keyboard macro events
which fire only on button down.

This commit also adds the clutch button functionality to the daemon,
allowing the user to disengage the profiles temporarily, and selecting a
profile or clearing the profile using the joystick hat and MFD.
clutch-profile-support
nirenjan 2026-02-26 13:30:03 -08:00
parent 004eca2418
commit 966c6efd5f
23 changed files with 1967 additions and 84 deletions

View File

@ -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

View File

@ -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.<name> = ... 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.
*/

View File

@ -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

View File

@ -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

View File

@ -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 <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <dirent.h>
#include <limits.h>
#include <errno.h>
#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 */
}

View File

@ -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 */

View File

@ -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 <stdio.h>
#include <setjmp.h>
#include <string.h>
#include <cmocka.h>
#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);
}

View File

@ -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",

View File

@ -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; }

View File

@ -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)

View File

@ -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);

View File

@ -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; }

View File

@ -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,

View File

@ -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));
}

View File

@ -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 <stdint.h>
#include <stdbool.h>
/**
* @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 */

View File

@ -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 <stdbool.h>
#include <errno.h>
#include <string.h>
#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;
}
}

View File

@ -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

View File

@ -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 <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <limits.h>
#include <errno.h>
#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 */
}
}
}

View File

@ -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 */

View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <setjmp.h>
#include <unistd.h>
#include <cmocka.h>
#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);
}

View File

@ -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

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

View File

@ -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 <nirenjan@gmail.com>\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"