From 541f8f739c0a8eb76e7b3c723f9135939784562e Mon Sep 17 00:00:00 2001 From: nirenjan Date: Tue, 3 Mar 2026 12:28:11 -0800 Subject: [PATCH] feat: Add support for keycombos in macros and move to worker thread This change adds support for key combinations in macro sequences. This allows a profile to support a sequence of key combos such as Ctrl-X, Alt-Y, Shift-Z, Ctrl-Alt-Shift-A, etc. Previously, the profile could only support a single key or key combo in the `key` mode. This allows for a longer key combo sequence. In addition, the macro sequences have been moved to a worker thread, with a fixed delay in between each key press. This simulates a human entering keys on a keyboard, albeit one who is super fast. Also, this avoids blocking the main IO handler for long key sequences. --- daemon/default.conf.example | 3 +- daemon/x52d_profile.c | 311 ++++++++++++++++++++++++++++++++---- daemon/x52d_profile.h | 11 +- daemon/x52d_profile_test.c | 55 ++++++- 4 files changed, 344 insertions(+), 36 deletions(-) diff --git a/daemon/default.conf.example b/daemon/default.conf.example index c37d108..7e79ded 100644 --- a/daemon/default.conf.example +++ b/daemon/default.conf.example @@ -3,7 +3,8 @@ # [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) +# Button.X = macro KEY_A KEY_B (sequence: A then B) +# Button.X = macro KEY_LEFTCTRL KEY_C | KEY_A (steps separated by |: combo Ctrl+C then key A) # Fallback: ModeN.Shift -> ModeN; Mode2 -> Mode1; Mode3 -> Mode2 -> Mode1 [Profile] diff --git a/daemon/x52d_profile.c b/daemon/x52d_profile.c index 3184a6d..2762be6 100644 --- a/daemon/x52d_profile.c +++ b/daemon/x52d_profile.c @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include "libevdev/libevdev.h" #include "libx52io.h" @@ -38,6 +40,9 @@ #define KEY_PREFIX "key" #define MACRO_PREFIX "macro" #define MAX_MACRO_KEYS 32 +#define MAX_MACRO_STEPS 32 +#define MACRO_JOB_QUEUE_SIZE 32 +#define MACRO_DELAY_MS 20 typedef enum { ACTION_NONE, @@ -49,8 +54,11 @@ 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 */ + /* ACTION_MACRO: steps separated by |; each step is one or more keys (combo) */ + size_t macro_len; /* total key count (flat) */ + size_t macro_step_count; + size_t *macro_step_len; /* length of each step */ + uint16_t *macro_keys; /* flat key codes, may be NULL */ } profile_action_t; static profile_action_t layers[NUM_LAYERS][LIBX52IO_BUTTON_MAX]; @@ -145,32 +153,85 @@ static int parse_action_value(const char *value, profile_action_t *out) } if (strcasecmp(tok, MACRO_PREFIX) == 0) { - while (n < MAX_MACRO_KEYS) { + size_t step_len_buf[MAX_MACRO_STEPS]; + size_t step_count = 0; + size_t total_keys = 0; + size_t *step_len_alloc = NULL; + size_t keys_in_step; + char *segment_start; + char *segment_end; + char seg_buf[128]; + size_t seg_len; + char *seg_p; + bool has_pipe = (strchr(value, '|') != NULL); + + /* If no |, legacy format: each key is its own step (sequence of single keys) */ + if (!has_pipe) { + n = 0; + while (n < MAX_MACRO_KEYS && step_count < MAX_MACRO_STEPS) { + while (*p == ' ') p++; + if (*p == '\0') break; + tok = (char *)p; + while (*p != '\0' && *p != ' ') p++; + if (*p != '\0') *p++ = '\0'; + code = key_name_to_code(tok); + if (code < 0) return -1; + keys[n++] = (uint16_t)code; + step_len_buf[step_count++] = 1; + total_keys++; + } + } else { + /* Steps separated by |; each step is space-separated key names (single key or combo) */ + /* First token "macro" was NUL-terminated in buf; skip past it to the rest of the value */ + p = buf; + while (*p != '\0') p++; + p++; while (*p == ' ') p++; - if (*p == '\0') { - break; + while (step_count < MAX_MACRO_STEPS && total_keys < MAX_MACRO_KEYS) { + while (*p == ' ') p++; + if (*p == '\0') break; + segment_start = p; + while (*p != '\0' && *p != '|') p++; + segment_end = p; + if (*p == '|') p++; + seg_len = (size_t)(segment_end - segment_start); + while (seg_len > 0 && segment_start[seg_len - 1] == ' ') seg_len--; + if (seg_len == 0) return -1; + if (seg_len >= sizeof(seg_buf)) return -1; + memcpy(seg_buf, segment_start, seg_len); + seg_buf[seg_len] = '\0'; + seg_p = seg_buf; + keys_in_step = 0; + while (*seg_p != '\0') { + while (*seg_p == ' ') seg_p++; + if (*seg_p == '\0') break; + tok = seg_p; + while (*seg_p != '\0' && *seg_p != ' ') seg_p++; + if (*seg_p != '\0') *seg_p++ = '\0'; + code = key_name_to_code(tok); + if (code < 0) return -1; + if (total_keys >= MAX_MACRO_KEYS) return -1; + keys[total_keys++] = (uint16_t)code; + keys_in_step++; + } + if (keys_in_step == 0) return -1; + step_len_buf[step_count++] = keys_in_step; } - 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) { + if (step_count == 0 || total_keys == 0) return -1; + out->macro_keys = malloc(total_keys * sizeof(uint16_t)); + if (out->macro_keys == NULL) return -1; + memcpy(out->macro_keys, keys, total_keys * sizeof(uint16_t)); + step_len_alloc = malloc(step_count * sizeof(size_t)); + if (step_len_alloc == NULL) { + free(out->macro_keys); + out->macro_keys = NULL; 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; + memcpy(step_len_alloc, step_len_buf, step_count * sizeof(size_t)); + out->macro_len = total_keys; + out->macro_step_len = step_len_alloc; + out->macro_step_count = step_count; out->type = ACTION_MACRO; return 0; } @@ -199,10 +260,17 @@ static void free_action(profile_action_t *a) 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; + if (a->type == ACTION_MACRO) { + if (a->macro_keys != NULL) { + free(a->macro_keys); + a->macro_keys = NULL; + } + if (a->macro_step_len != NULL) { + free(a->macro_step_len); + a->macro_step_len = NULL; + } a->macro_len = 0; + a->macro_step_count = 0; } a->type = ACTION_NONE; } @@ -320,8 +388,164 @@ static void load_profile(void) } } +/* Macro job queue: each entry is one macro (steps; each step is single key or combo). */ +struct macro_job { + uint16_t *keys; + size_t *step_len; + size_t step_count; +}; + +static struct { + struct macro_job ring[MACRO_JOB_QUEUE_SIZE]; + unsigned int head; + unsigned int tail; + unsigned int count; + pthread_mutex_t mutex; + pthread_cond_t cond; + pthread_cond_t drained; + pthread_t thread; + bool shutdown; + bool thread_started; +} macro_queue; + +static bool macro_queue_push_job(const uint16_t *keys, const size_t *step_len, + size_t step_count) +{ + size_t total_keys = 0; + size_t s; + uint16_t *keys_copy = NULL; + size_t *step_len_copy = NULL; + bool ok = false; + + if (step_count == 0 || keys == NULL || step_len == NULL) { + return false; + } + for (s = 0; s < step_count; s++) { + total_keys += step_len[s]; + } + if (total_keys == 0) { + return false; + } + keys_copy = malloc(total_keys * sizeof(uint16_t)); + if (keys_copy == NULL) { + return false; + } + memcpy(keys_copy, keys, total_keys * sizeof(uint16_t)); + step_len_copy = malloc(step_count * sizeof(size_t)); + if (step_len_copy == NULL) { + free(keys_copy); + return false; + } + memcpy(step_len_copy, step_len, step_count * sizeof(size_t)); + + pthread_mutex_lock(¯o_queue.mutex); + if (macro_queue.count < MACRO_JOB_QUEUE_SIZE) { + macro_queue.ring[macro_queue.tail].keys = keys_copy; + macro_queue.ring[macro_queue.tail].step_len = step_len_copy; + macro_queue.ring[macro_queue.tail].step_count = step_count; + macro_queue.tail = (macro_queue.tail + 1) % MACRO_JOB_QUEUE_SIZE; + macro_queue.count++; + ok = true; + pthread_cond_signal(¯o_queue.cond); + } + pthread_mutex_unlock(¯o_queue.mutex); + if (!ok) { + free(step_len_copy); + free(keys_copy); + } + return ok; +} + +static void *macro_worker_thread(void *arg) +{ + (void)arg; + for (;;) { + struct macro_job job = { NULL, 0 }; + bool got = false; + + pthread_mutex_lock(¯o_queue.mutex); + while (macro_queue.count == 0 && !macro_queue.shutdown) { + pthread_cond_wait(¯o_queue.cond, ¯o_queue.mutex); + } + if (macro_queue.shutdown && macro_queue.count == 0) { + pthread_cond_broadcast(¯o_queue.drained); + pthread_mutex_unlock(¯o_queue.mutex); + break; + } + if (macro_queue.count > 0) { + job = macro_queue.ring[macro_queue.head]; + macro_queue.ring[macro_queue.head].keys = NULL; + macro_queue.ring[macro_queue.head].step_len = NULL; + macro_queue.ring[macro_queue.head].step_count = 0; + macro_queue.head = (macro_queue.head + 1) % MACRO_JOB_QUEUE_SIZE; + macro_queue.count--; + got = true; + } + pthread_mutex_unlock(¯o_queue.mutex); + + if (got && job.keys != NULL && job.step_len != NULL) { + size_t offset = 0; + size_t s; + for (s = 0; s < job.step_count; s++) { + size_t len = job.step_len[s]; + size_t i; + /* Combo: all keys down in order, delay, all keys up in reverse */ + for (i = 0; i < len; i++) { + x52d_keyboard_evdev_key(job.keys[offset + i], 1); + } + usleep((useconds_t)MACRO_DELAY_MS * 1000); + for (i = len; i > 0; i--) { + x52d_keyboard_evdev_key(job.keys[offset + i - 1], 0); + } + offset += len; + usleep((useconds_t)MACRO_DELAY_MS * 1000); + } + free(job.step_len); + free(job.keys); + /* Signal drained only after all keys emitted, so wait_drained is accurate */ + pthread_mutex_lock(¯o_queue.mutex); + pthread_cond_broadcast(¯o_queue.drained); + pthread_mutex_unlock(¯o_queue.mutex); + } + } + pthread_mutex_lock(¯o_queue.mutex); + pthread_cond_broadcast(¯o_queue.drained); + pthread_mutex_unlock(¯o_queue.mutex); + return NULL; +} + void x52d_profile_init(void) { + int rc; + + macro_queue.head = 0; + macro_queue.tail = 0; + macro_queue.count = 0; + macro_queue.shutdown = false; + rc = pthread_mutex_init(¯o_queue.mutex, NULL); + if (rc != 0) { + PINELOG_ERROR(_("Failed to create macro queue mutex: %s"), strerror(rc)); + } else { + rc = pthread_cond_init(¯o_queue.cond, NULL); + if (rc != 0) { + pthread_mutex_destroy(¯o_queue.mutex); + PINELOG_ERROR(_("Failed to create macro queue cond: %s"), strerror(rc)); + } else if (pthread_cond_init(¯o_queue.drained, NULL) != 0) { + pthread_cond_destroy(¯o_queue.cond); + pthread_mutex_destroy(¯o_queue.mutex); + PINELOG_ERROR(_("Failed to create macro queue drained cond")); + } else { + rc = pthread_create(¯o_queue.thread, NULL, macro_worker_thread, NULL); + if (rc != 0) { + pthread_cond_destroy(¯o_queue.drained); + pthread_cond_destroy(¯o_queue.cond); + pthread_mutex_destroy(¯o_queue.mutex); + PINELOG_ERROR(_("Failed to start macro worker thread: %s"), strerror(rc)); + } else { + macro_queue.thread_started = true; + } + } + } load_profile(); PINELOG_INFO(_("Profile module initialized")); } @@ -336,6 +560,17 @@ const char *x52d_profile_get_name(void) void x52d_profile_exit(void) { + if (macro_queue.thread_started) { + pthread_mutex_lock(¯o_queue.mutex); + macro_queue.shutdown = true; + pthread_cond_broadcast(¯o_queue.cond); + pthread_mutex_unlock(¯o_queue.mutex); + pthread_join(macro_queue.thread, NULL); + pthread_cond_destroy(¯o_queue.drained); + pthread_cond_destroy(¯o_queue.cond); + pthread_mutex_destroy(¯o_queue.mutex); + macro_queue.thread_started = false; + } clear_all_layers(); shift_button_index = -1; profile_name_str[0] = '\0'; @@ -401,16 +636,26 @@ static const profile_action_t *get_action_for_button(const libx52io_report *repo return NULL; } -static void emit_macro(const profile_action_t *a) +void x52d_profile_macro_wait_drained(void) { - size_t i; - - if (a->type != ACTION_MACRO || a->macro_keys == NULL || !x52d_keyboard_evdev_available()) { + if (!macro_queue.thread_started) { 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); + pthread_mutex_lock(¯o_queue.mutex); + while (macro_queue.count > 0) { + pthread_cond_wait(¯o_queue.drained, ¯o_queue.mutex); + } + pthread_mutex_unlock(¯o_queue.mutex); +} + +static void emit_macro(const profile_action_t *a) +{ + if (a->type != ACTION_MACRO || a->macro_keys == NULL || a->macro_step_len == NULL || + !x52d_keyboard_evdev_available()) { + return; + } + if (!macro_queue_push_job(a->macro_keys, a->macro_step_len, a->macro_step_count)) { + PINELOG_WARN(_("Macro queue full, dropping macro")); } } diff --git a/daemon/x52d_profile.h b/daemon/x52d_profile.h index 9c56f4e..04b2981 100644 --- a/daemon/x52d_profile.h +++ b/daemon/x52d_profile.h @@ -47,7 +47,8 @@ const char *x52d_profile_get_name(void); * 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. + * Macro mappings: sequence of key down/up on button down only (queued + * and emitted by a worker thread with a short delay between events). * * @param report Current joystick report. * @param prev Previous report (for edge detection). @@ -55,6 +56,13 @@ const char *x52d_profile_get_name(void); void x52d_profile_apply(const libx52io_report *report, const libx52io_report *prev); +/** + * @brief Block until all queued macro key events have been emitted. + * + * Useful for tests or before switching profile so macros finish playing. + */ +void x52d_profile_macro_wait_drained(void); + #else static inline void x52d_profile_init(void) { (void)0; } @@ -65,6 +73,7 @@ static inline void x52d_profile_apply(const libx52io_report *report, (void)report; (void)prev; } +static inline void x52d_profile_macro_wait_drained(void) { (void)0; } #endif /* HAVE_EVDEV */ diff --git a/daemon/x52d_profile_test.c b/daemon/x52d_profile_test.c index 791250a..4632fc3 100644 --- a/daemon/x52d_profile_test.c +++ b/daemon/x52d_profile_test.c @@ -182,7 +182,7 @@ static void test_profile_macro_button_down_only(void **state) key_record_reset(); x52d_profile_apply(&report, &prev); - + x52d_profile_macro_wait_drained(); 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); @@ -204,6 +204,58 @@ static void test_profile_macro_button_down_only(void **state) rmdir(tmpdir); } +static void test_profile_macro_pipe_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_T1_UP = macro KEY_LEFTCTRL KEY_C | KEY_A\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); + x52d_profile_macro_wait_drained(); + /* Step 1: Ctrl+C (down order: Ctrl, C; up order: C, Ctrl). Step 2: A (down, up) */ + assert_int_equal(key_record_n, 6); + 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_C")); + assert_int_equal(key_record[1].value, 1); + assert_int_equal(key_record[2].code, key_code_from_name("KEY_C")); + assert_int_equal(key_record[2].value, 0); + assert_int_equal(key_record[3].code, key_code_from_name("KEY_LEFTCTRL")); + assert_int_equal(key_record[3].value, 0); + assert_int_equal(key_record[4].code, key_code_from_name("KEY_A")); + assert_int_equal(key_record[4].value, 1); + assert_int_equal(key_record[5].code, key_code_from_name("KEY_A")); + assert_int_equal(key_record[5].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"; @@ -316,6 +368,7 @@ int main(void) 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_macro_pipe_combo), cmocka_unit_test(test_profile_fallback_mode2_to_mode1), cmocka_unit_test(test_profile_shift_layer), cmocka_unit_test(test_profile_get_name),