Skip to content

Offering a solution : Keyboard aggregator for musashi (Survives KVM switching for USB keyboards) #84

@chapmanworld

Description

@chapmanworld

This isn’t really a bug report, just something I hacked together that might help others.
I needed more stable USB keyboard input for PiStorm (Musashi), especially when using a KVM switch.

( Note: I "vibe coded" this. I could have coded it myself, but I'm lazy and my c is very rusty. The source, and most of the instructions were generated by GPT )


Background

  • I rolled my A2000 back to Musashi, because EMU68 (though vastly superior performance-wise) doesn’t have USB-HID support yet.
  • I don’t own a genuine A2000 keyboard (and they’re getting expensive).
  • With USB keyboards through a KVM, Musashi loses the keyboard device on switches. The emulator doesn’t aggregate keyboards, so reconnects break input.
  • Instead of adding this into the emulator itself, I threw together a small helper service that creates a virtual keyboard aggregator. It listens to all real keyboards and presents one stable /dev/input/vkbd-aggregator device that survives KVM switching.

It would be even nicer if the emulator itself could handle multiple keyboards or hotplug gracefully, but until then, here’s a working solution.

--- (Source is at the end, I put mine in /home/pi/vkbd-aggregator, adjust to taste and edit services accordingly.)

Build & Install

  1. Install prerequisites

    sudo apt install libevdev-dev libudev-dev build-essential
    
  2. Build the binary
    cd /home/pi/vkbd-aggregator
    gcc -O2 -Wall -pthread vkbd_aggregator.c -ludev -levdev
    -I/usr/include/libevdev-1.0
    -o vkbd_aggregator

  3. Install service unit : sudo nano /etc/systemd/system/vkbd-aggregator.service
    [Unit]
    Description=Virtual Keyboard Aggregator (uinput)
    After=network.target

[Service]
Type=simple
ExecStart=/usr/local/sbin/vkbd_aggregator
Restart=on-failure
RestartSec=1
User=root
AmbientCapabilities=CAP_SYS_ADMIN
CapabilityBoundingSet=CAP_SYS_ADMIN
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

-- and then
sudo install -m 0755 ./vkbd_aggregator /usr/local/sbin/vkbd_aggregator
sudo systemctl daemon-reload
sudo systemctl enable --now vkbd-aggregator.service

  1. Stable device symlink via udev
    echo 'KERNEL=="event*", SUBSYSTEM=="input", ATTRS{name}=="vkbd-aggregator", SYMLINK+="input/vkbd-aggregator"'
    | sudo tee /etc/udev/rules.d/99-vkbd-aggregator.rules
    sudo udevadm control --reload-rules
    sudo udevadm trigger

  2. Configure piStorm
    keyboard k nograb autoconnect
    kbfile /dev/input/vkbd-aggregator

  3. Optional dependency
    To ensure the Amiga emulator waits for the keyboard aggregator, add to the pistorm service:
    [Unit]
    Requires=vkbd-aggregator.service
    After=vkbd-aggregator.service

---- Source...

// vkbd_aggregator.c
// Aggregates multiple real keyboards into one virtual keyboard (uinput).
// - Filters out mice that expose keyboard interfaces
// - Grabs physical keyboards so other processes can't steal them
// - Hotplugs via libudev
// Build:
// gcc -O2 -Wall -pthread vkbd_aggregator.c -ludev -levdev \
//    -I/usr/include/libevdev-1.0 \
//    -o vkbd_aggregator
// Run (root): ./vkbd_aggregator

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <libevdev/libevdev.h>
#include <libudev.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#define LOG(...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } while (0)

static volatile sig_atomic_t g_running = 1;

static void on_signal(int s) { (void)s; g_running = 0; }

typedef struct DevNode {
    char *path;                     // /dev/input/eventX
    int fd;
    struct libevdev *evdev;
    pthread_t thread;
    bool running;
    struct DevNode *next;
} DevNode;

typedef struct {
    int ufd;                        // uinput fd
    pthread_mutex_t ulock;          // serialize writes to uinput
    DevNode *head;
    pthread_mutex_t list_lock;
} AppState;

static AppState app = {
    .ufd = -1,
    .ulock = PTHREAD_MUTEX_INITIALIZER,
    .head = NULL,
    .list_lock = PTHREAD_MUTEX_INITIALIZER
};

static int uinput_setup()
{
    int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
    if (fd < 0) {
        perror("open /dev/uinput");
        return -1;
    }

    // Enable basic event types for a keyboard
    if (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0) { perror("UI_SET_EVBIT EV_KEY"); goto fail; }
    if (ioctl(fd, UI_SET_EVBIT, EV_SYN) < 0) { perror("UI_SET_EVBIT EV_SYN"); goto fail; }
    ioctl(fd, UI_SET_EVBIT, EV_REP); // allow key repeat
    // Optionally forward real MSC_SCAN when present (we won't synthesize)
    ioctl(fd, UI_SET_EVBIT, EV_MSC);
    ioctl(fd, UI_SET_MSCBIT, MSC_SCAN);

    // Enable a broad set of keys (0..255 covers classic KEY_* range)
    for (int code = 0; code <= 255; ++code) {
        ioctl(fd, UI_SET_KEYBIT, code);
    }
    // A few common extra keys above 255 (just in case)
    int extras[] = { KEY_MICMUTE, KEY_FN, KEY_PROG1, KEY_PROG2, KEY_BRIGHTNESSUP, KEY_BRIGHTNESSDOWN };
    for (size_t i = 0; i < sizeof(extras)/sizeof(extras[0]); ++i) {
        ioctl(fd, UI_SET_KEYBIT, extras[i]);
    }

    struct uinput_setup us = {0};
    us.id.bustype = BUS_VIRTUAL;
    us.id.vendor  = 0x1d6b;   // Linux Foundation (arbitrary but plausible)
    us.id.product = 0x0104;
    us.id.version = 1;
    snprintf(us.name, sizeof(us.name), "vkbd-aggregator");

    if (ioctl(fd, UI_DEV_SETUP, &us) < 0) { perror("UI_DEV_SETUP"); goto fail; }
    if (ioctl(fd, UI_DEV_CREATE) < 0) { perror("UI_DEV_CREATE"); goto fail; }

    LOG("uinput virtual keyboard created as 'vkbd-aggregator'");
    return fd;

fail:
    close(fd);
    return -1;
}

static void uinput_emit(int fd, __u16 type, __u16 code, __s32 value)
{
    struct input_event ev = {0};
    struct timeval tv;
    gettimeofday(&tv, NULL);
    ev.time = tv;
    ev.type = type;
    ev.code = code;
    ev.value = value;
    if (write(fd, &ev, sizeof(ev)) < 0) {
        if (g_running) perror("write uinput");
    }
}

static void uinput_key(int fd, __u16 code, int value)
{
    uinput_emit(fd, EV_KEY, code, value);
    uinput_emit(fd, EV_SYN, SYN_REPORT, 0);
}

// Decide whether a device is a "real keyboard" and not a mouse exposing keys
static bool is_real_keyboard(struct udev_device *ud, struct libevdev *ev)
{
    // Ignore our own virtual device, and any virtual-bus device
    const char *self_name = libevdev_get_name(ev);
    if (self_name && strcmp(self_name, "vkbd-aggregator") == 0) return false;
    if (libevdev_get_id_bustype(ev) == BUS_VIRTUAL) return false;

    // Prefer udev properties — fast and reliable
    const char *is_kbd  = udev_device_get_property_value(ud, "ID_INPUT_KEYBOARD");
    const char *is_mouse = udev_device_get_property_value(ud, "ID_INPUT_MOUSE");
    if (is_kbd && strcmp(is_kbd, "1") == 0) {
        // Exclude if it is also flagged as a mouse
        if (is_mouse && strcmp(is_mouse, "1") == 0) return false;
        // Additionally exclude if it reports relative motion like a mouse
        if (libevdev_has_event_type(ev, EV_REL) &&
            (libevdev_has_event_code(ev, EV_REL, REL_X) ||
             libevdev_has_event_code(ev, EV_REL, REL_Y))) return false;
        // Must actually have EV_KEY
        if (!libevdev_has_event_type(ev, EV_KEY)) return false;

        // Ensure it has "real" keys beyond just mouse buttons
        int has_typing = 0;
        for (int k = KEY_ESC; k <= KEY_KPDOT; ++k) {
            if (libevdev_has_event_code(ev, EV_KEY, k)) { has_typing = 1; break; }
        }
        if (!has_typing) return false;
        return true;
    }
    return false;
}

static void *reader_thread(void *arg)
{
    DevNode *node = (DevNode *)arg;
    node->running = true;
    LOG("reading %s", node->path);

    // Grab ONLY non-virtual physical devices to avoid grabbing our own uinput
    if (libevdev_get_id_bustype(node->evdev) != BUS_VIRTUAL) {
        int rc = libevdev_grab(node->evdev, LIBEVDEV_GRAB);
        if (rc < 0) {
            LOG("warn: failed to grab %s: %s", node->path, strerror(-rc));
        }
    }

    while (g_running) {
        struct input_event ev;
        int rc = libevdev_next_event(node->evdev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
        if (rc == 0) {
            if (ev.type == EV_KEY) {
                pthread_mutex_lock(&app.ulock);
                // Forward only the key event; do not synthesize fake MSC_SCAN
                uinput_emit(app.ufd, EV_KEY, ev.code, ev.value);
                uinput_emit(app.ufd, EV_SYN, SYN_REPORT, 0);
                pthread_mutex_unlock(&app.ulock);
            } else if (ev.type == EV_MSC && ev.code == MSC_SCAN) {
                // If a real scancode arrives from hardware, forward it as-is (optional)
                pthread_mutex_lock(&app.ulock);
                uinput_emit(app.ufd, EV_MSC, MSC_SCAN, ev.value);
                pthread_mutex_unlock(&app.ulock);
            } else {
                // ignore other types (REL/ABS/etc.) and synthetic SYN (we emit our own)
            }
        } else if (rc == -EAGAIN) {
            usleep(1000);
        } else if (rc == -ENODEV) {
            LOG("device gone: %s", node->path);
            break;
        } else {
            usleep(1000);
        }
    }

    // Ungrab only if we actually grabbed (non-virtual devices)
    if (libevdev_get_id_bustype(node->evdev) != BUS_VIRTUAL) {
        libevdev_grab(node->evdev, LIBEVDEV_UNGRAB);
    }
    node->running = false;
    return NULL;
}

static void add_device(const char *devnode, struct udev_device *ud)
{
    // Open
    int fd = open(devnode, O_RDONLY | O_NONBLOCK);
    if (fd < 0) return;

    struct libevdev *ev = NULL;
    int rc = libevdev_new_from_fd(fd, &ev);
    if (rc < 0) { close(fd); return; }

    // Ignore our own virtual device and any virtual-bus devices
    const char *self_name = libevdev_get_name(ev);
    if ((self_name && strcmp(self_name, "vkbd-aggregator") == 0) ||
        libevdev_get_id_bustype(ev) == BUS_VIRTUAL) {
        libevdev_free(ev);
        close(fd);
        return;
    }

    if (!is_real_keyboard(ud, ev)) {
        libevdev_free(ev);
        close(fd);
        return;
    }

    // Create node & thread
    DevNode *node = calloc(1, sizeof(DevNode));
    node->path = strdup(devnode);
    node->fd = fd;
    node->evdev = ev;

    pthread_mutex_lock(&app.list_lock);
    node->next = app.head;
    app.head = node;
    pthread_mutex_unlock(&app.list_lock);

    pthread_create(&node->thread, NULL, reader_thread, node);
    LOG("attached keyboard: %s (%s)", devnode, libevdev_get_name(ev));
}

static void remove_device(const char *devnode)
{
    pthread_mutex_lock(&app.list_lock);
    DevNode **pp = &app.head;
    while (*pp) {
        DevNode *n = *pp;
        if (strcmp(n->path, devnode) == 0) {
            *pp = n->next;
            pthread_mutex_unlock(&app.list_lock);

            // stop
            if (n->running) {
                // libevdev will break when fd closes
            }
            libevdev_free(n->evdev);
            close(n->fd);
            if (n->thread) pthread_join(n->thread, NULL);

            LOG("detached keyboard: %s", devnode);
            free(n->path);
            free(n);
            return;
        }
        pp = &((*pp)->next);
    }
    pthread_mutex_unlock(&app.list_lock);
}

static void initial_scan(struct udev *udev)
{
    struct udev_enumerate *en = udev_enumerate_new(udev);
    udev_enumerate_add_match_subsystem(en, "input");
    udev_enumerate_add_match_property(en, "ID_INPUT_KEYBOARD", "1");
    udev_enumerate_scan_devices(en);

    struct udev_list_entry *it;
    udev_list_entry_foreach(it, udev_enumerate_get_list_entry(en)) {
        const char *syspath = udev_list_entry_get_name(it);
        struct udev_device *ud = udev_device_new_from_syspath(udev, syspath);
        const char *devnode = udev_device_get_devnode(ud);
        if (devnode && strncmp(devnode, "/dev/input/event", 16) == 0) {
            add_device(devnode, ud);
        }
        udev_device_unref(ud);
    }
    udev_enumerate_unref(en);
}

int main(void)
{
    signal(SIGINT,  on_signal);
    signal(SIGTERM, on_signal);

    app.ufd = uinput_setup();
    if (app.ufd < 0) {
        LOG("uinput setup failed");
        return 1;
    }

    struct udev *udev = udev_new();
    if (!udev) { LOG("udev_new failed"); return 1; }

    // Attach existing keyboards
    initial_scan(udev);

    // Monitor hotplug
    struct udev_monitor *mon = udev_monitor_new_from_netlink(udev, "udev");
    udev_monitor_filter_add_match_subsystem_devtype(mon, "input", NULL);
    udev_monitor_enable_receiving(mon);
    int mfd = udev_monitor_get_fd(mon);

    LOG("vkbd_aggregator running. Exposing a single virtual keyboard.");

    while (g_running) {
        fd_set fds;
        FD_ZERO(&fds);
        FD_SET(mfd, &fds);
        struct timeval tv = { .tv_sec = 0, .tv_usec = 250000 };
        int r = select(mfd + 1, &fds, NULL, NULL, &tv);
        if (r > 0 && FD_ISSET(mfd, &fds)) {
            struct udev_device *ud = udev_monitor_receive_device(mon);
            if (!ud) continue;

            const char *action  = udev_device_get_action(ud);
            const char *devnode = udev_device_get_devnode(ud);

            if (devnode && strncmp(devnode, "/dev/input/event", 16) == 0) {
                if (action && strcmp(action, "add") == 0) {
                    add_device(devnode, ud);
                } else if (action && strcmp(action, "remove") == 0) {
                    remove_device(devnode);
                } else if (action && strcmp(action, "change") == 0) {
                    // Some devices re-enumerate; be conservative: detach + reattach
                    remove_device(devnode);
                    add_device(devnode, ud);
                }
            }
            udev_device_unref(ud);
        }
    }

    // teardown
    pthread_mutex_lock(&app.list_lock);
    for (DevNode *n = app.head; n;) {
        DevNode *next = n->next;
        libevdev_free(n->evdev);
        close(n->fd);
        if (n->thread) pthread_join(n->thread, NULL);
        free(n->path);
        free(n);
        n = next;
    }
    app.head = NULL;
    pthread_mutex_unlock(&app.list_lock);

    if (app.ufd >= 0) {
        ioctl(app.ufd, UI_DEV_DESTROY);
        close(app.ufd);
    }

    udev_unref(udev);
    LOG("vkbd_aggregator stopped.");
    return 0;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions