Linux中使用XLib注册系统级热键

本文介绍在Linux X11环境下如何注册操作系统级的热键。使你的应用程序C/C++/Java…能够在没有焦点的情况下触发其功能。本文所实现的功能源自一个Linux触摸屏软键盘的项目,该项目需要在一台工控机上部署Linux(Lubuntu),并用面板上的GPIO按键(转USB键盘芯片发送标准键值给PC)随时呼出软键盘进行操作。
思路灵感来自于这篇blog:http://blog.163.com/ojb_123/blog/static/2417742420094138384658/ ,在此感谢作者。

知识点

  1. X Window是Linux图形系统,X client(一般是各种应用程序)跟X server(Linux提供)进行通信从而在屏幕上显示各式窗体和图像。
  2. 「Linux注册系统热键,在应用程序没有焦点的情况触发功能事件」,这样的需求只会存在于使用X Window的情况,当使用命令行终端时并不太可能有这样的需求。
  3. XLib中提供XGrabKeyboard()XGrabKey()函数供抓取键盘事件,前者用来让应用程序打开键盘监听,以后在获取焦点的情况下就能捕获任意键盘事件;后者是我们需要用到的,指定特定的键值捕获,并且以passtive的方式捕获,也就是无论是否能获取焦点,KeyEvent事件都能透传到我们的应用程序,详细描述请查阅XLib手册:https://tronche.com/gui/x/xlib/input/keyboard-grabbing.html
  4. Linux下的XLib动态库是以libX11.so的命名方式提供的,g++编译时需加上-lX11参数表示引用libX11.so/usr/lib/路径下有我们需要的lib,并且libX11.so是一个link,指向另外的名字中带上版本号的.so,比如libX11.so.0.6.0
  5. X Window虽然在操作系统运行时就提供了,但XLib和所要用的头文件,需要另外下载:sudo apt install libx11-dev
  6. Linux X环境下,如果按住一个按键不松开,其Press和Release事件会一直循环触发,如果程序中需要对物理键盘一次敲击只响应一次,需要自己加一点逻辑判断。
  7. 基于C/C++的全局注册模块编译出来后,可以通过JNI技术可以轻松为上层JAVA程序使用,甚至你可以使用JavaFX构建美观的GUI,实际上我就是这么干的,留到下次写文章来介绍。

1. 准备工作

  • 一台Lubuntu16.04,或者其他Ubunut LTS版本,或者其他Debian。(个人偏好使用Lubuntu长期支持版)
  • sudo apt install libx11-dev
  • sudo apt install g++
  • sudo apt install vim
  • sudo apt install make

2. 编码

vim hotkey.cpp

#include <stdio.h>
#include <X11/Xlib.h>

int main(){
    Window root;
    XEvent e;
    int F4, F6;

    //Open the display
    Display *dpy = XOpenDisplay(0);
    if(!dpy)
        return 1;

    root = DefaultRootWindow(dpy);

    F4 = XKeysymToKeycode(dpy, XStringToKeysym("F4"));
    F6 = XKeysymToKeycode(dpy, XStringToKeysym("F6"));

    //Register the keys
    XGrabKey(dpy, F4, 0, root, True, GrabModeAsync, GrabModeAsync);
    XGrabKey(dpy, F6, 0, root, True, GrabModeAsync, GrabModeAsync);

    //Wait for events
    for(;;){
        XNextEvent(dpy, &e);
        if(e.type == KeyPress){
            if(e.xkey.keycode == F4)
                break;
            else if(e.xkey.keycode == F6)
                printf("F6 pressed\n");
        }
    }

    XUngrabKey(dpy, F4, 0, root);
    XUngrabKey(dpy, F6, 0, root);
}

OK,最原始的测试DEMO就写好了,这份代码主要是打通和验证注册热键的功能。下面进行编译测试:

g++ -o hotkey.o hotkey.cpp -lX11

不出意外的话就可以编译出来,在X Window环境下Ctrl+Alt+T打开图形界面下的终端,而不要直接Ctrl+Alt+F1打开纯命令行,执行编译出来的hotkey.o,程序应该在for(;;)死循环中停住,然后按下F6功能键,则输出F6 pressed,按下F4则退出程序,截图如下:

3. 剖析和魔改

观察上述代码:

  • 首先定义了两个int F4,F6,对其用F4 = XKeysymToKeycode(dpy, XStringToKeysym("F4"))进行赋值,可以看出XStringToKeysym()一定是一个能把字符串解析成键值的高端玩意儿。
  • 接着用XGrabKey(dpy, F4, 0, root, True, GrabModeAsync, GrabModeAsync);,对F4和F6按键进行系统级的热键注册。
  • 最后写了一个死循环监听键盘事件,在监听循环中,判断是否是KeyPress事件,如果是的话,键值为F4就退出,键值为F6就输出字符串。
  • 程序的最后解除了针对F4和F6的键盘监听

代码非常容易理解,于是我们尝试深入一下头文件,并修改代码。

刚才提到XStringToKeysym()这个函数挺有趣的,能将字符串转成键值,但我并不觉得好用,因为键盘上那么多按键,我也不知道除了F功能键以外的按键应该用什么字符串来表达,我还是希望能够用确切的键值来表示,最好是整型。

于是执行

grep -rn "XStringToKeysym" /usr/include/X11/
#搜索到XStringToKeysym返回的是KeySym类型
grep -rn "KeySym" /usr/include/X11/
#搜索到KeySym是CARD32的别名
grep -rn "CARD32" /usr/include/X11/
#搜索到CARD32是unsigned int的别名

OK,确实返回的是一个整型,那么我们直接用确切的整型吧,就别用XStringToKeysym再去转了,也方便以后用其他键值的时候不知道输入啥字符串。

那么整型的键值在哪里定义的呢?同样的,使用暴力的搜索命令

grep -rn "F4" /usr/include/X11/


找到两个比较靠谱的位置,打开/usr/include/X11/keysymdef.h瞧瞧吧:

全是我们想要的!OK,丢掉XStringToKeysym,潇洒地用键值吧。

4. 重复触发问题

我们在Windows下处理键盘事件,几乎都有KeyPressKeyRelease可以处理,XLib同样也有,在事件枚举中同样提供了KeyRelease事件条目,理论上如果按键按下不松开,那么会触发一次KeyPress事件,直到按键松开,再触发一次KeyRelease事件。然而,XLib为了解决光标键,回车键,DEL键等按键的连续响应问题,将所有按键都设置成AutoRepeat了。这就有点讨厌了,设想一下我们设计一个软键盘程序,在用户按一下F6的时候呼出,再按一下F6的时候隐藏,如果操作人员按F6的时间有点长,那这个软键盘窗体会一直在屏幕上闪动,不停地切换显示/隐藏的状态。因此我们在程序中如果想只响应一次,就必须做出调整。

借鉴这个ISSUE:https://stackoverflow.com/questions/2100654/ignore-auto-repeat-in-x11-applications

可以看到,这个ISSUE中,有人用e.xkey.time,也就是触发时间来处理这个问题,但给出的代码还是有些晦涩,那么我们来自己动手研究一下。

可以看出每个键盘事件响应时,都会带上xkey.time这个值,目测是事件响应的时间,那么我们继续暴力搜索,寻找蛛丝马迹吧:
Xlib.h和X.h中有如下定义:

OK,Time是一个unsigned long int,长整。那么我们触发时将其输出一下试试:

修改代码:

    for(;;){
        XNextEvent(dpy, &e);
        if(e.type == KeyPress){
            if(e.xkey.keycode == F4)
                break;
            else if(e.xkey.keycode == F6)
                printf("F6 pressed, press time : %ld\n", e.xkey.time);            
        } else if(e.type == KeyRelease){
            if(e.xkey.keycode == F6)
                printf("F6 pressed, release time : %ld\n", e.xkey.time);
        }
    }

重新编译运行,按住F6不松开,如下图所示

OK,我们看出端倪来了,原来KeyPress会在上一次KeyRelease发生后立即触发,二者的时间是相同的,那么好了,只要我们记录一下上一次KeyRelease的时间,下一次KeyPress发生时再判断一下,如果KeyPress的时间跟上一次KeyRelease的时间不同,则表示是两次独立的事件,如果相同,则表示用户的手没有松开按键,不应该响应。

编码如下:

    //if press-release event repeat,the last release time = the next press time
    unsigned long lastReleaseTime = 0;

    //Wait for events
    for(;;){
        XNextEvent(dpy, &e);
        if(e.type == KeyPress && e.xkey.time != lastReleaseTime){
            if(e.xkey.keycode == F4)
                //do something...
            else if(e.xkey.keycode == F6)
                //do something...
        }else if(e.type == KeyRelease){
            lastReleaseTime = e.xkey.time;            
        }
    }

OK,本文到此结束了,可以成功注册Linux系统热键了,后面还会写一篇小文介绍一下如何将这个模块编译成.so动态链接库,使用JNI技术,供上层JAVA调用,并使用JavaFX设计一个美观的软键盘。

最后上一张软键盘做好的截图:

STM32F4+DP83848以太网通信指南系列(九):自己写一个ARP协议

前言:项目需求使用STM32F407进行以太网通信,并涉及到数据链路层的工业以太网通信,使用LWIP协议栈并不能满足需求,因此需要自己摸清STM32F407调用外部PHY进行网络收发包的过程,并在此基础上尝试自己构建适用于项目的网络协议栈。我基本上是从零开始着手这个项目的,之前只有一些STC51系列和STM32F1系列单片机开发的经验,项目开发过程中学习、参考、借鉴了很多网络上的教程和博客,在此尤其感谢正点原子团队发布的相关视频教程。公司买的开发板主芯片是一颗STM32F407,搭配了一颗DP83848的PHY,因此本系列教程将使用DP83848进行适配,同时原子哥的学习板和教程中是使用LAN8720这颗PHY进行适配的,本系列指南也会花一些篇幅介绍各种PHY与STM32芯片进行适配的方法。

为了您更好地阅读本系列,请点击原创连接进行浏览:

本章为系列指南的第九章,终结篇,本章主要来分析一下完整的ARP协议,并在STM32F4中实现一个精简的ARP协议响应流程。

ARP协议的本质是使局域网内的其他主机能够知道我在哪儿,比如在局域网上有人冲着所有人喊了一句「IP为XXXX的家伙,你在哪儿」,我一听,XXXX不是我的IP吗,我得回答他啊,于是我冲着所有人(也可以是单独的这个人)喊一句「我在这儿呢,我的MAC是YYYY」,这样局域网内所有用户,包括交换机就知道IP为XXXX的家伙MAC地址是YYYY,并且交换机知道了我连在它的第N个端口上,以后有人要通过交换机向IP为XXXX的我发信息,交换机就把数据包发到N port上去了。没有上面这套流程,局域网上就没人知道你是谁,你的IP多少,你的MAC多少,你连在交换机大佬的第几个端口上,自然你也收不到任何非广播包数据了。

ARP请求

如果我要向一个之前没有任何联系的主机主动发一个数据包,一般情况下都会触发一个ARP询问,比如我们上一章《STM32F4+DP83848以太网通信指南第八章:收包流程》中,最后的实验,我打开CMD命令行,输入了一个指令ping 192.168.1.201,这时候PC的底层设备(一般是网卡)首先判断一下自己的缓存中有没有192.168.1.201这个IP的缓存信息,如果没有通信记录或者时间过长失效了,就得在网络上广播一下,找找看当前有没有谁在用192.168.1.201这个IP地址,包内容如下:

ff ff ff ff ff ff 00 0e c6 d4 1d d4 08 06 00 01
08 00 06 04 00 01 00 0e c6 d4 1d d4 c0 a8 01 c8
00 00 00 00 00 00 c0 a8 01 C9

WireShark分析以上报文的解析如下图所示:

ARP响应

目标主机的MAC地址为FF FF FF FF FF FF时,交换机会将此包广播到所有端口,这时候所有节点的网卡上都能收到这个广播包,如果某一节点的IP地址为192.168.1.201则需要对此包进行响应,判断依据为上述包的以下特征:

  • 第一行倒数第4,第3字节:0806,代表的是ARP协议
  • 第二行第6字节:01,代表的是ARP请求
  • 第三行最后4字节:c0 a8 01 C9,代表的是192.168.1.1,跟自己寄存器中的值匹配

响应数据包为以下内容:

00 0e c6 d4 1d d4 00 11 0e 0b 03 8a 08 06 00 01
08 00 06 04 00 02 00 11 0e 0b 03 8a c0 a8 01 c9
00 0e c6 d4 1d d4 c0 a8 01 c8 00 00 00 00 00 00
00 00 00 00 00 00 00 00 20 20 20 20

上述中,以下数据是动态变化的:

  • 第一行前6个字节:00 0e c6 d4 1d d4,根据请求包中的第7-12六个字节决定,表示目标MAC地址
  • 第一行第7-12六个字节:00 11 0e 0b 03 8a,自由设置的自己的MAC地址
  • 第二行第7-12六个字节:00 11 0e 0b 03 8a,自由设置的自己的MAC地址
  • 第二行倒数4个字节:c0 a8 01 c9,自己的IP地址,也就是请求包中所呼唤的IP地址,192.168.1.201
  • 第三行前6个字节:00 0e c6 d4 1d d4,根据请求包中的第7-12六个字节决定,表示目标MAC地址
  • 第三行第7-10四个字节:c0 a8 01 c8,根据请求包第二行倒数四个字节决定,表示目标IP地址

这些信息都可以用WireShark分析出来,WireShark对每一个字节的含义都有明确的解释。

编码实现

有了特征匹配及填充规则,配合我们之前的STM32发包和收包DEMO,我们就可以编程实现ARP数据包的响应了。

#define LEN_ARP 42

/* 以下为业务逻辑需要用到的全局变量 */
u8 IPAddr[4];

vu8  SystemStatus; /* 系统状态,bit0:是否需要发送被动响应数据包,bit1:是否打开了PNIO开关,bit2:是否需要发送PNControl request */
vu16 sendLen;      /* 发送包长度 */
u8 sendBuffer[1024];   /* 发送包BUFFER */
u8 arp_answer[LEN_ARP]={
    0,0,0,0,0,0, /* ArpAskerMac */ 0,0,0,0,0,0, /* myMac */ 0x08, 0x06, 0x00, 0x01,
    0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0,0,0,0,0,0, /* myMac */ 0,0,0,0, /* IP address */
    0,0,0,0,0,0, /* ArpAskerMac */ 0,0,0,0/* ArpAskerIP */
};

int main() {
    SystemStatus = SS_NOTHING;
    sendLen = 0;    

    /* 默认调用SystemInit,系统时钟168MHz */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);        //4位抢占,0位响应

    DelayInit(168);        //初始化168MHz

    NVIC_SetPriority (SysTick_IRQn, 0);        //系统滴答定时器优先级最高  

    DP83848Init(MyMacAddr);        //初始化DP83848

    while(1) {
        if (0x01 == (SystemStatus & 0x01)){
            /* 如果需要发送被动网卡响应 */
            DP83848Send(sendBuffer, sendLen);            
            sendLen = 0;
            SystemStatus = SystemStatus & 0xFE; /* bit0置为0 */
        }
    }
}

void Pkt_Handle(void) {
    FrameTypeDef frame;
    __IO ETH_DMADESCTypeDef *DMARxNextDesc;

    /* get received frame */
    frame = ETH_Get_Received_Frame();
    /* Obtain the size of the packet and put it into the "len" variable. */
    receiveLen = frame.length;
    receiveBuffer = (u8 *)frame.buffer;

    if (Match(receiveBuffer, ArpProtocol, 12, 2) && Match(receiveBuffer, IPAddr, 38, 4)){
        /* ARP */
        //printf("ARP test received\n");
        memcpy(sendBuffer, arp_answer, LEN_ARP);

        for (int i = 0; i < 6; i++) {
            /* ArpAskerMac */
            sendBuffer[i] = receiveBuffer[6 + i];    
            sendBuffer[32 + i] = receiveBuffer[6 + i];
        }
        for (int i = 0; i < 4; i++) {
            /* ArpAskerIP */
            sendBuffer[38 + i] = receiveBuffer[28 + i];    
        }

        sendLen = LEN_ARP;
        SystemStatus = (SystemStatus | 0x01); /* 打开网卡被动数据发送开关 */
    }

    /* Check if frame with multiple DMA buffer segments */
    if (DMA_RX_FRAME_infos->Seg_Count > 1) {
        DMARxNextDesc = DMA_RX_FRAME_infos->FS_Rx_Desc;
    } else {
        DMARxNextDesc = frame.descriptor;
    }

    /* Set Own bit in Rx descriptors: gives the buffers back to DMA */
    for (i = 0; i < DMA_RX_FRAME_infos->Seg_Count; i++) {
        DMARxNextDesc->Status = ETH_DMARxDesc_OWN;
        DMARxNextDesc = (ETH_DMADESCTypeDef *)(DMARxNextDesc->Buffer2NextDescAddr);
    }

    /* Clear Segment_Count */
    DMA_RX_FRAME_infos->Seg_Count = 0;

    /* When Rx Buffer unavailable flag is set: clear it and resume reception */
    if ((ETH->DMASR & ETH_DMASR_RBUS) != (u32)RESET) {
        /* Clear RBUS ETHERNET DMA flag */
        ETH->DMASR = ETH_DMASR_RBUS;
        /* Resume DMA reception */
        ETH->DMARPDR = 0;
    }
}

编码的总体思想就是中断中快进快出,用变量来标记状态,主循环中不断判断状态并复位状态。

使用这种方式,可以继续扩展ICMP协议,实现PING指令,以及更多按需裁剪的TCP/IP协议,当然扩展的协议越多,这种纯粹的if判断也会越多,以后会细分各个层,在层次中继续if判断,当到了顶层应用层之后,就只管TCP/UDP协议了,内容一般都是应用协议自己规划的比如HTTP,FTP这些协议,只需要把上层协议的数据结构填充到下层协议的内容区域就行了。当遇到数据包比较庞大的时候,还需要分包传输,暂时我们没有用到,LWIP中有相应的实现,有需求的话可以去研究。此外,我们实验中的代码都是基于数组进行数据装箱拆箱操作的,而网络包的协议是按层次来的,使用数组不断的进行数据的填充是非常消耗性能的,因此在遇到数据量比较大,需要实现的协议比较多时,仍然建议按照LWIP的思路使用收尾相接的链式结构进行拆装箱。

系列总结

到此我们这个系列就算告一段落了,后面的工业以太网的协议的分析和实现我不方便做公开的教程,也是自己一步一步摸索过来的,目前自己在设计PCB板,后期这个工程需要制板贴片并做成最终的工业产品。

我们来回顾总结一下这个系列教程,首先我们对STM32F407的时钟、中断等相关知识做了一个梳理,接着我们认识了STM32中的MAC,以及跟MAC搭配的PHY,同时还对DMA技术做了一个粗略的了解。试着初始化相关的GPIO,使能了MAC,DMA,PHY,这样一个DP83848Init()函数就搞定了;再后来我们又完成DP83848Send()函数,能够发包了,接着配置了以太网中断,能够在Pkt_Handle()函数中进行以太网收包了,以上函数的编写,我们都是参考的ethernetif.c文件,它的几个底层函数low_level_initlow_level_outputlow_level_input给我们提供了重要的线索。最后对TCP/IP协议栈中的ARP协议进行了分析,并运用之前的全部知识,进行了ARP响应的编码实现,并对以后其他协议的扩展实现提出了思路和优化建议。

好了,期待今后有更多的系列教程跟大家分享。再见!

丁丁生于 1987.07.01 ,30岁,英文ID:newflydd

  • 现居住地 江苏 ● 泰州 ● 姜堰
  • 创建了 Jblog 开源博客系统
  • 坚持十余年的 独立博客 作者
  • 大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。
  • Git OSChina 上积极参与开源社区