手柄别闲着
廉价的复古手柄一直插着电脑, 之前都没太注意, 今天看到了, 就寻思能不能折腾一翻, 结果还真给折腾成了 ()
这个手柄之前用来玩Mednaffe
的 GBA 游戏, 还想用来直播来着, 对了, 我写了一个基于无头浏览器的直播TUI弹幕工具, 要不要也来一篇(不要).
1. 思路
想起之前装了一个软件, 可以显示调试X11窗口的工具, 但是名字想不起来了, 这确实是我目前使用习惯的一个痛点, 除了做笔记, 另外也没想到什么好的办法.
第一反应当然是问 GPT:
- Linux下x11有一个软件可以在一个测试窗口里输入, 然后终端里会显示当前的xevent, 那个软件叫什么
- Linux下用于测试和显示X11事件的软件是
xev
。xev
创建一个窗口,并且会在终端中报告该窗口中发生的所有X事件,如按键、鼠标移动、窗口聚焦等。这对于开发者和系统管理员来说是一个非常有用的工具,用于调试和理解X窗口系统中的事件处理。要使用xev
,通常只需在终端中输入xev
命令即可。
在xev
窗口里测试了下手柄按键, 不出所料, 无事发生, 这说明手柄的事件与键盘不同, 不会发起一个XEvent
, 我现在也不太明白Xlib
这套东西的具体细节, 到底会不会被wayland
替代呢?
然后我又问GPT:
- 有办法捕获手柄的输入吗
- 有:
- jtest/evtest
- udev
- 写程序读取/dev/input
- 使用软件
pacman
可以直接安装evtest
, 试了一下确实可以识别手柄输入, 但是我们当然最终目的是用程序实现
- 有c语言的库支持读取手柄事件吗
- 有:
- SDL
- GLFW
- linux的evdev
- 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
, 而上的value
为0
, 下的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
的多线程, 然后经过一顿暴改, 改的很骚, 直接把dwm
的Key
类型造抄过来
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
, 因为仔细分析一下就可以知道, 这部分的代码完全可以分离出来. 于是建了一个新仓库放分离出来的代码
2024/01/28显然这个方法不是很优雅, 那是因为我还欠缺对
X11
的理解, 深入学习之后应该还有优雅的实现方法. 但是我至少折腾出来了~