From 51ce442f2cf0e2e48d937ac751263ffeaf6a100e Mon Sep 17 00:00:00 2001 From: nirenjan Date: Sun, 8 Nov 2026 22:09:15 -0800 Subject: [PATCH] feat: Add event handler module to daemon This change adds an event handler module, which will allow a single central event dispatch instead of handling the event diff inside each input module such as the mouse handler. --- daemon/event_handler.c | 84 ++++++++++++++++++++++++++ daemon/event_handler.h | 23 ++++++++ daemon/meson.build | 10 ++++ daemon/test_event_handler.c | 115 ++++++++++++++++++++++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 daemon/event_handler.c create mode 100644 daemon/event_handler.h create mode 100644 daemon/test_event_handler.c diff --git a/daemon/event_handler.c b/daemon/event_handler.c new file mode 100644 index 0000000..b4bf81d --- /dev/null +++ b/daemon/event_handler.c @@ -0,0 +1,84 @@ +/* + * Saitek X52 Pro MFD & LED driver - Event handler + * + * Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include +#include + +#include "event_handler.h" + +typedef struct { + _Atomic x52_event_handler_fn fn; + _Atomic uint64_t mask; +} x52_event_handler; + +#define MAX_EVENT_HANDLERS 8 + +static x52_event_handler handlers[MAX_EVENT_HANDLERS]; +static atomic_int num_handlers = 0; + +int x52d_register_event_handler(uint64_t mask, x52_event_handler_fn fn) +{ + int ticket; + + if (fn == NULL) { + // Don't bother registering a NULL callback + errno = EINVAL; + return -1; + } + + ticket = atomic_fetch_add(&num_handlers, 1); + if (ticket >= MAX_EVENT_HANDLERS) { + // Already hit the maximum tickets + errno = ENOSPC; + return -1; + } + + atomic_store(&handlers[ticket].mask, mask); + atomic_store(&handlers[ticket].fn, fn); + + return ticket; +} + +int x52d_update_event_handler_mask(int handler, uint64_t new_mask) +{ + if (handler < 0 || handler >= MAX_EVENT_HANDLERS) { + errno = ERANGE; + return -1; + } + + if (atomic_load(&handlers[handler].fn) == NULL) { + errno = EINVAL; + return -1; + } + + atomic_store(&handlers[handler].mask, new_mask); + return 0; +} + +void x52d_dispatch_event(libx52io_report *prev, libx52io_report *curr) +{ + int current_limit = atomic_load(&num_handlers); + if (current_limit >= MAX_EVENT_HANDLERS) { + current_limit = MAX_EVENT_HANDLERS; + } + + uint64_t changed = libx52io_compare_reports(prev, curr); + + + for (int i = 0; i < current_limit; i++) { + x52_event_handler_fn callback = atomic_load(&handlers[i].fn); + if (callback == NULL) { + continue; + } + + uint64_t mask = atomic_load(&handlers[i].mask) & changed; + if (mask != 0) { + callback(mask, prev, curr); + } + } +} diff --git a/daemon/event_handler.h b/daemon/event_handler.h new file mode 100644 index 0000000..9e4526c --- /dev/null +++ b/daemon/event_handler.h @@ -0,0 +1,23 @@ +/* + * Saitek X52 Pro MFD & LED driver - Event handler header + * + * Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#ifndef X52D_EVENT_HANDLER_H +#define X52D_EVENT_HANDLER_H + +#include +#include + +typedef void (*x52_event_handler_fn)(uint64_t changed, + const libx52io_report *prev, + const libx52io_report *curr); + +int x52d_register_event_handler(uint64_t mask, x52_event_handler_fn fn); +int x52d_update_event_handler_mask(int handler, uint64_t new_mask); +void x52d_dispatch_event(libx52io_report *prev, libx52io_report *curr); + +#endif // !defined X52D_EVENT_HANDLER_H diff --git a/daemon/meson.build b/daemon/meson.build index 1b66d20..2bbe187 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -60,6 +60,7 @@ x52d_sources = [ 'config_dump.c', 'config.c', 'device.c', + 'event_handler.c', 'client.c', 'clock.c', 'mouse.c', @@ -159,6 +160,15 @@ test('layout-compile-py', python_layout_test, args: [join_paths(meson.project_source_root(), 'tools', 'test_x52compile_layout.py')], protocol: 'tap') +event_handler_test = executable('event-handler-test', + 'test_event_handler.c', + build_by_default: false, + include_directories: [includes, root_includes], + extra_files: files('event_handler.c'), + dependencies: [dep_cmocka, dep_config_h]) + +test('event-handler', event_handler_test, protocol: 'tap') + # Install service file if dep_systemd.found() systemd_system_unit_dir = get_option('systemd-unit-dir') diff --git a/daemon/test_event_handler.c b/daemon/test_event_handler.c new file mode 100644 index 0000000..47022c6 --- /dev/null +++ b/daemon/test_event_handler.c @@ -0,0 +1,115 @@ +/* + * Saitek X52 daemon event handler test suite + * + * Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +// Mocked dependencies called by our module under test +uint64_t libx52io_compare_reports(const libx52io_report *prev, const libx52io_report *curr) { + // Grab the mock return value defined by the test driver + return (uint64_t)mock(); +} + +// THE TRICK: Directly include the source file to expose its internal static state +#include "event_handler.c" + +/* --- 1. MOCK CALLBACK FUNCTIONS --- */ +static int g_callback_count_1 = 0; +static void mock_callback_1(uint64_t mask, const libx52io_report *prev, const libx52io_report *curr) { + g_callback_count_1++; + assert_int_equal(mask, 0x0F); // Verify the callback receives the correct mask +} + +/* --- 2. SETUP / TEARDOWN FIXTURES --- */ +static int clear_table_state(void **state) { + // Reset our static atomic counter and table state between tests + atomic_store(&num_handlers, 0); + for (int i = 0; i < MAX_EVENT_HANDLERS; i++) { + atomic_store(&handlers[i].fn, NULL); + atomic_store(&handlers[i].mask, 0); + } + g_callback_count_1 = 0; + return 0; +} + +/* --- 3. THE UNIT TESTS --- */ + +// Test basic parameters and expected input failures +static void test_register_null_callback(void **state) { + errno = 0; + int ticket = x52d_register_event_handler(0xFF, NULL); + assert_int_equal(ticket, -1); + assert_int_equal(errno, EINVAL); +} + +// Test successful registration and ticket bounds constraints +static void test_register_until_full(void **state) { + for (int i = 0; i < MAX_EVENT_HANDLERS; i++) { + int ticket = x52d_register_event_handler(0x01, mock_callback_1); + assert_int_equal(ticket, i); // Ticket should match incremental sequence + } + + // The 9th registration attempt should fail with ENOSPC + errno = 0; + int overflow_ticket = x52d_register_event_handler(0x01, mock_callback_1); + assert_int_equal(overflow_ticket, -1); + assert_int_equal(errno, ENOSPC); +} + +// Test runtime mask updates and out-of-bound protections +static void test_update_mask_out_of_bounds(void **state) { + errno = 0; + int status = x52d_update_event_handler_mask(-1, 0xFF); + assert_int_equal(status, -1); + assert_int_equal(errno, ERANGE); + + errno = 0; + status = x52d_update_event_handler_mask(MAX_EVENT_HANDLERS, 0xFF); + assert_int_equal(status, -1); + assert_int_equal(errno, ERANGE); +} + +// Test dispatch functionality and mask matching logic +static void test_dispatch_matching_event(void **state) { + libx52io_report prev, curr; + + // Register handler matching mask bit 0x0F + int ticket = x52d_register_event_handler(0x0F, mock_callback_1); + assert_true(ticket >= 0); + + // Setup our mock return for libx52io_compare_reports + will_return(libx52io_compare_reports, 0x0F); // Report changed bit matches mask + + // Run the dispatch engine + x52d_dispatch_event(&prev, &curr); + + // Verify callback executed exactly once + assert_int_equal(g_callback_count_1, 1); +} + +/* --- 4. ENGINE RUNNER --- */ +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_register_null_callback, clear_table_state, NULL), + cmocka_unit_test_setup_teardown(test_register_until_full, clear_table_state, NULL), + cmocka_unit_test_setup_teardown(test_update_mask_out_of_bounds, clear_table_state, NULL), + cmocka_unit_test_setup_teardown(test_dispatch_matching_event, clear_table_state, NULL), + }; + + cmocka_set_message_output(CM_OUTPUT_TAP); + cmocka_run_group_tests(tests, NULL, NULL); + return 0; +} +