手柄别闲着
start at 2024/01/27.

手柄别闲着

廉价的复古手柄一直插着电脑, 之前都没太注意, 今天看到了, 就寻思能不能折腾一翻, 结果还真给折腾成了 ()

复古的廉价手柄/廉价的复古手柄

这个手柄之前用来玩Mednaffe的 GBA 游戏, 还想用来直播来着, 对了, 我写了一个基于无头浏览器的直播TUI弹幕工具, 要不要也来一篇(不要).

1. 思路

想起之前装了一个软件, 可以显示调试X11窗口的工具, 但是名字想不起来了, 这确实是我目前使用习惯的一个痛点, 除了做笔记, 另外也没想到什么好的办法.

第一反应当然是问 GPT:

- Linux下x11有一个软件可以在一个测试窗口里输入, 然后终端里会显示当前的xevent, 那个软件叫什么

- Linux下用于测试和显示X11事件的软件是xevxev创建一个窗口,并且会在终端中报告该窗口中发生的所有X事件,如按键、鼠标移动、窗口聚焦等。这对于开发者和系统管理员来说是一个非常有用的工具,用于调试和理解X窗口系统中的事件处理。要使用xev,通常只需在终端中输入xev命令即可。

󰔓

xev窗口里测试了下手柄按键, 不出所料, 无事发生, 这说明手柄的事件与键盘不同, 不会发起一个XEvent, 我现在也不太明白Xlib这套东西的具体细节, 到底会不会被wayland替代呢?

然后我又问GPT:

- 有办法捕获手柄的输入吗

- 有:

  1. jtest/evtest
  2. udev
  3. 写程序读取/dev/input
  4. 使用软件

pacman可以直接安装evtest, 试了一下确实可以识别手柄输入, 但是我们当然最终目的是用程序实现

- 有c语言的库支持读取手柄事件吗

- 有:

  1. SDL
  2. GLFW
  3. linux的evdev
  4. SFML

没错, 除了evdev, 其他都是用来写游戏用的, 那么如此喜欢游戏的我, 理所当然地选择了evdev.

然后直接给了我一个demo

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/input.h>

int main() {
    int fd;
    struct input_event ev;
    // 替换为你的设备文件路径
    fd = open("/dev/input/eventX", O_RDONLY);
    if (fd < 0) {
        perror("Failed to open device");
        return -1;
    }
    while (1) {
        read(fd, &ev, sizeof(struct input_event));
        printf("Type: %d, Code: %d, Value: %d\n", ev.type, ev.code, ev.value);
    }
    close(fd);
    return 0;
}

经过我的插拔实验, 发现我的手柄设备为/dev/input/event14, 修改之后惊奇的发现居然真可以, 而且还发现和键盘事件不同, 按下按键的时候不会持续发送信号, 按下到松起过程中无论按下的时长, 都是相同事件, 由type, code, value可以定位到按键, 比较有趣的是十字轴, 上和下是同一个code, 而上的value0, 下的value为255, 回中的时候有一个127的信号, ->0-127-255.

到这里我们完全可以自定义想干什么事了, 但是当我拔出手柄的时候就一直输出Type: 0, Code: 0, Value: 0了, CPU占用巨高. 拔出设备之后设备文件应该是被删除了, 但是read到了一堆0, 我们加点简单的判断, 再在外面加一个大循环, 就能实现简单的巡检了

while (1) {
    fd = open("/dev/input/event14", 0);
    if (fd < 0) {
        usleep(10000000);
        continue;
    }
    int last = 0;
    int now = 100;
    while (1) {
        read(fd, &ev, sizeof(struct input_event));
        now = ev.type + ev.code + ev.value;
        if (last == 0 && now == 0) break;
        printf("Type: %d, Code: %d, Value: %d\n", ev.type, ev.code, ev.value);
        last = now;
    }
}

可以在无法连接和连上的时候用notify-send来发送通知, 那个usleep的单位居然是微秒(汗)…

2. 脑洞

现在我们总共有

  • 上/下/左/右
  • A/B/X/Y/L/R
  • start/select

12个按键可以自定义

当然还有组合键可以自定义, 不过我才懒的写

首先, 上和下肯定是用来调整音量的, 这没得抢!

然后左右用来切换桌面标签比较合理, 之前写了跳转相邻的标签的函数, 之前还想和切换显示屏的快捷键换一下来着…

到这里我们已经可以确定要和dwm联动了

其他几个按键随便分配几个得了, 随便绑几个dwm函数得了

3. 折腾

要联动dwm, 肯定意味着又要动源码了()

思路模仿一下按键, dwm 的按键机制之前研究VNC输入的时候探索过, 用一个特殊的按键来取消所有dwm的快捷键, 要不要水一篇来着(又来!). 写在config.h里的keys[]会在dwm初始化的时候用XGrabKey绑定上对应制定的函数和参数, 这就涉及到X11的通信了, 什么window, client的概念就都要扯出来了, 就不扯了哈.

那我们能不能把手柄按键也绑定上处理函数呢, 答案是不行, 因为按下手柄按键这个动作, 压根不会产生XEvent, 怎么XGrabKey

我们再看一下我们有什么, 我们有一个死循环, 可以检测设备数据, 然后识别出按键后进行处理, 关键就是这是一个循环, 有两条思路, 一个思路是把检测事件放在dwm的单次循环中, 一个是创建另外一个线程, 用来监听手柄输入, 然后调用dwm函数. 然而看一下这段dwm的源码

void
run(void)
{
    XEvent ev;
    /* main event loop */
    XSync(dpy, False);
    while (running && !XNextEvent(dpy, &ev))
        if (handler[ev.type])
            handler[ev.type](&ev); /* call handler */
}

懂了吧, dwm的运行是阻塞的, 只管处理XEvent, 那么自然就没有选项了, 然后知道了linux源码里实现多线程的库一般是pthread, 我都没写过c的多线程, 然后经过一顿暴改, 改的很骚, 直接把dwmKey类型造抄过来

typedef struct {
    unsigned int type;
    unsigned int code;
    unsigned int value;
    void (*func)(const Arg *);
    const Arg arg;
} GP;

typedef struct {
    unsigned int mod;
    KeySym keysym;
    void (*func)(const Arg *);
    const Arg arg;
} Key;

非常的优雅

static const GP gps[] = {
    { 3, 1, 0,   spawn, SHCMD("~/.scripts/volup.sh")},
    { 3, 1, 255, spawn, SHCMD("~/.scripts/voldown.sh")},
    { 3, 0, 0,   goprevvis, {0}},
    { 3, 0, 255, gonextvis, {0}},
};

然而, 残酷的现实告诉我, 折腾不可能这么快, 在我刚测试完声音调节的时候, 已经想开香槟的时候, 发现切换左右的时候, 虽然功能是正常的, 但是状态栏的颜色开始乱跳, 这怎么能忍!

我怀疑是多线程导致绘制冲突了, 然后我问了下GPT

- 如果我在dwm源码中使用多线程会不会不太好

- 在 dwm 源码中引入多线程需要谨慎考虑,因为这可能违背了 dwm 的设计哲学和原始意图。dwm 以其简洁性、高效性和单线程架构而著称,这些特点使得其代码易于理解和维护。在 dwm 中引入多线程可能会导致以下几个问题:

  • xxx
  • 设计哲学冲突: dwm 的设计哲学强调简单和高效。多线程可能会与这种哲学相冲突,尤其是如果多线程的引入并没有显著改善性能或功能。
  • xxx

看到 设计哲学冲突 的时候我感觉自己像犯了罪, 但是我都写到这了, 总要写完吧()

直接给自己的猜想确症了, 那么smart boy, 你能想到解决方案吗?

Bingo! 不用线程调函数, 而是用线程发起XEvent, 显而易见的非常不优雅()

又经过一翻折腾, 直接XEvent失败 -> 使用XTest可以发送 -> 试图自定义键码失败 -> 直接用绑定的按键, 终于把这东西写完了, 然后我开了给新分支存这个代码, 取名为suck, 不动master, 因为仔细分析一下就可以知道, 这部分的代码完全可以分离出来. 于是建了一个新仓库放分离出来的代码

显然这个方法不是很优雅, 那是因为我还欠缺对X11的理解, 深入学习之后应该还有优雅的实现方法. 但是我至少折腾出来了~

贫穷的快乐

2024/01/28
> CLICK TO back <