-
Notifications
You must be signed in to change notification settings - Fork 113
Description
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
-
Install prerequisites
sudo apt install libevdev-dev libudev-dev build-essential
-
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 -
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
-
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 -
Configure piStorm
keyboard k nograb autoconnect
kbfile /dev/input/vkbd-aggregator -
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;
}