mirror of https://github.com/nirenjan/libx52.git
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.pull/88/head
parent
293d9dff87
commit
51ce442f2c
|
|
@ -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 <stdatomic.h>
|
||||
#include <errno.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <libx52/libx52.h>
|
||||
#include <libx52/libx52io.h>
|
||||
|
||||
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
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdint.h>
|
||||
#include <stdatomic.h>
|
||||
#include <errno.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <libx52/libx52io.h>
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue