在Linux下实现一个使用键盘控制的虚拟鼠标

在Linux下建立一个虚拟鼠标设备仍是比较简单的,使用内核uinput模块提供的函数便可。建立出虚拟鼠标之后,在主线程监听键盘的事件,当特定的键(此处使用了小键盘的数字键八、二、四、6)被按下或弹起后,进行记录。在另外一个线程根据主线程记录的flag建立输入事件,而后将输入事件写入虚拟鼠标设备便可。
在实现程序时一个让我思考时间比较长的问题是:是否须要另外建立一个线程来写虚拟鼠标设备。当一个键被按下后,咱们须要不断的写向相应方向移动的事件,这里是须要定时循环写的。若是不另开线程的话,咱们须要在这个循环中先使用select或poll读键盘事件,可能等到了事件,也可能等不到事件;而后对相应的键进行记录;而后写虚拟鼠标设备;最后睡眠必定的时间,睡眠的时间是受前面等待事件的时间影响的,这样才能使得每一个循环花费时间是相同的。这样作应该也行得通,但我以为不如另开一个线程,在两个循环里分别读写事件实现起来比较方便。不过另开线程须要为线程安全作一点小工做,缘由是有些flag可能须要在两个线程里写,使用标准库提供的atomic模板就能作到。
本来觉得没几行代码,没想到写着写着就长了,功能也不太全,没有实现左右按键的功能,不过好歹能直接跑起来,有须要的时候进行扩展就是了。实现代码时主要参考了这些资料:
https://www.kernel.org/doc/html/v4.12/input/uinput.html
https://cgit.freedesktop.org/evtest
代码地址为:https://github.com/im-red/virtualmouse
也把代码贴在这里:html

#include <err.h>
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/uinput.h>
#include <string.h>
#include <dirent.h>
#include <vector>
#include <string>
#include <iostream>
#include <poll.h>
#include <signal.h>
#include <pthread.h>
#include <atomic>

#define STR(X) STR2(X)
#define STR2(X) #X

#define ENSURE(CONDITION, ...) if (!(CONDITION)) { err(EXIT_FAILURE, __FILE__ ":" STR(__LINE__) " " __VA_ARGS__); }

static const char *EVENT_DEV_NAME = "event";
static const char *EVENT_DEV_PREFIX = "/dev/input/";
static const char *UINPUT_NAME = "/dev/uinput";

static const int BITS_PER_LONG = sizeof(long) * 8;

inline int nbytes(int x)
{
    return (x - 1) / BITS_PER_LONG + 1;
}

inline bool testBits(int bit, const unsigned long array[])
{
    unsigned long result = array[bit / BITS_PER_LONG] >> bit % BITS_PER_LONG & 1;
    return (result == 1);
}

static int isEventDevice(const struct dirent *dir) 
{
	return strncmp(EVENT_DEV_NAME, dir->d_name, strlen(EVENT_DEV_NAME)) == 0;
}

static void createVirtualMouse(int fd)
{
    // enable mouse button left and relative events
    ioctl(fd, UI_SET_EVBIT, EV_KEY);
    ioctl(fd, UI_SET_KEYBIT, BTN_LEFT);

    ioctl(fd, UI_SET_EVBIT, EV_REL);
    ioctl(fd, UI_SET_RELBIT, REL_X);
    ioctl(fd, UI_SET_RELBIT, REL_Y);

    struct uinput_user_dev uud;
    memset(&uud, 0, sizeof(uud));

    snprintf(uud.name, UINPUT_MAX_NAME_SIZE, "Virtual Mouse");

    int ret = write(fd, &uud, sizeof(uud));
    ENSURE(ret == sizeof(uud));

    ioctl(fd, UI_DEV_CREATE);
}

static std::vector<std::string> getAllEventDevicePath()
{
    struct dirent **direntList;

    int ndev = scandir(EVENT_DEV_PREFIX, &direntList, isEventDevice, versionsort);
    ENSURE(ndev > 0, "No event device found");

    std::vector<std::string> result(ndev);

    for (int i = 0; i < ndev; i++)
    {
        result[i] = std::string(EVENT_DEV_PREFIX) + std::string(direntList[i]->d_name);
    }
    return std::move(result);
}

// A simple RAII class
class FileOpener
{
public:
    explicit FileOpener(const char *pathname, int flags)
    {
        fd = open(pathname, flags);
    }

    explicit FileOpener(const char *pathname, int flags, mode_t mode)
    {
        fd = open(pathname, flags, mode);
    }

    ~FileOpener()
    {
        if (fd >= 0)
        {
            close(fd);
        }
    }

    int getFd()
    {
        return fd;
    }

private:
    int fd;
};

static bool isSupportType(const std::string &path, int type)
{
    FileOpener opener(path.c_str(), O_RDONLY);
    int fd = opener.getFd();
    if (fd < 0)
    {
        return false;
    }

    unsigned long supportType[nbytes(EV_MAX)];
    ioctl(fd, EVIOCGBIT(0, EV_MAX), supportType);
    if (!testBits(type, supportType))
    {
        return false;
    }

    return true;
}

static bool isSupportTypeCode(const std::string &path, int type, int code)
{
    if (!isSupportType(path, type))
    {
        return false;
    }

    FileOpener opener(path.c_str(), O_RDONLY);
    int fd = opener.getFd();
    if (fd < 0)
    {
        return false;
    }

    unsigned long supportCode[nbytes(KEY_MAX)];
    ioctl(fd, EVIOCGBIT(type, KEY_MAX), supportCode);
    if (!testBits(code, supportCode))
    {
        return false;
    }
    return true;
}

static const int NEEDED_KEY[] = { KEY_KP8, KEY_KP2, KEY_KP4, KEY_KP6 };
// device reports KEY_KP8 KEY_KP2 KEY_KP4 KEY_KP6 is valid keyboard
static bool isValidKeyboard(const std::string &path)
{
    for (unsigned int i = 0; i < (sizeof(NEEDED_KEY) / sizeof(int)); i++)
    {
        if (!isSupportTypeCode(path, EV_KEY, NEEDED_KEY[i]))
        {
            return false;
        }
    }
    return true;
}

static std::vector<std::string> getAllValidKeyboard()
{
    std::vector<std::string> validDevice;
    std::vector<std::string> allDevice = getAllEventDevicePath();

    for (unsigned int i = 0; i < allDevice.size(); i++)
    {
        if (isValidKeyboard(allDevice[i]))
        {
            validDevice.push_back(allDevice[i]);
        }
    }

    return validDevice;
}

static std::string getOneNumLockDevice()
{
    std::vector<std::string> allDevice = getAllEventDevicePath();

    for (unsigned int i = 0; i < allDevice.size(); i++)
    {
        if (isSupportTypeCode(allDevice[i], EV_LED, LED_CAPSL))
        {
            return allDevice[i];
        }
    }
    return std::string("");
}

static int uinputFd = 0;

// program should exit
static volatile bool shouldStop = false;

// if numlock is on, we don't move mouse
static bool numlockOn = false;

const static int UP = 0;
const static int DOWN = 1;
const static int LEFT = 2;
const static int RIGHT = 3;
const static int DIRECTION_NUM = 4;

// move status
// change to true/false when corresponding key is pressed/released
static bool isMoving[DIRECTION_NUM] = { false };

// move times from the point that corresponding key is pressed
// reset to 0 when key is released
static std::atomic_int moveTimes[DIRECTION_NUM];

inline void resetMoveTimes()
{
    for (int i = 0; i < DIRECTION_NUM; i++)
    {
        moveTimes[i] = 0;
    }
}

// move step is affected by move times
// so we can apply some acceleration strategy
static int moveStep(int times)
{
    const int MIN_STEP = 1;
    const int MAX_STEP = 10;
    const int MIN_POINT = 50;
    const int MAX_POINT = 200;

    if (times <= MIN_POINT)
    {
        return MIN_STEP;
    }
    else if (times <= MAX_POINT)
    {
        return MIN_STEP + (times - MIN_POINT) * 1.0 / (MAX_POINT - MIN_POINT) * (MAX_STEP - MIN_STEP);
    }
    else
    {
        return MAX_STEP;
    }
}

// write mouse move event every timeInterval ms
static int timeInterval = 10;

static void setIsMoving(int index, int value)
{
    isMoving[index] = value;
    if (!value)
    {
        moveTimes[index] = 0;
    }
}

static void keyAction(int index, int value)
{
    ENSURE(index >= 0 && index < DIRECTION_NUM);
    if (value == 1)
    {
        setIsMoving(index, true);
    }
    else if (value == 0)
    {
        setIsMoving(index, false);
    }
    else
    {
        // do nothing
    }
}

static void handleKeyEvent(const struct input_event &ev)
{
    if (numlockOn)
    {
        return;
    }

    int index = -1;
    switch (ev.code)
    {
    case KEY_KP8: index = UP; break;
    case KEY_KP2: index = DOWN; break;
    case KEY_KP4: index = LEFT; break;
    case KEY_KP6: index = RIGHT; break;
    default: index = -1; break;
    }
    if (index != -1)
    {
        keyAction(index, ev.value);
    }
}

static void handleLEDEvent(const struct input_event &ev)
{
    if (ev.code == LED_NUML)
    {
        if (ev.value == 0)
        {
            numlockOn = false;
        }
        else if (ev.value == 1)
        {
            numlockOn = true;
            for (int i = 0; i < DIRECTION_NUM; i++)
            {
                setIsMoving(i, false);
            }
        }
    }
}

static void handleEvent(const struct input_event &ev)
{
    if (ev.type == EV_KEY)
    {
        handleKeyEvent(ev);
    }
    else if (ev.type == EV_LED)
    {
        handleLEDEvent(ev);
    }
}

static void handleDevice(int fd)
{
    struct input_event ev[64];
    int rd = read(fd, ev, sizeof(ev));
    ENSURE(rd % sizeof(struct input_event) == 0, "expected %d bytes, got %d\n", (int) sizeof(struct input_event), rd % (int) sizeof(struct input_event));
    int n = rd / sizeof(struct input_event);
    for (int j = 0; j < n; j++)
    {
        handleEvent(ev[j]);
    }
}

static void interrupt_handler(int sig)
{
    (void) sig;
	shouldStop = true;
}

static bool queryNumlock()
{
    std::string ledDevice = getOneNumLockDevice();

    // if there is no led device, we suppose that numlock is always off
    if (ledDevice == std::string(""))
    {
        return false;
    }

    FileOpener opener(ledDevice.c_str(), O_RDONLY);

    int fd = opener.getFd();
    ENSURE(fd >= 0);

    unsigned long supportLED[nbytes(LED_MAX)];
    ioctl(fd, EVIOCGLED(sizeof(supportLED)), supportLED);
    if (testBits(LED_NUML, supportLED))
    {
        return true;
    }
    else
    {
        return false;
    }
}

static void incMoveTimes()
{
    for (int i = 0; i < DIRECTION_NUM; i++)
    {
        if (isMoving[i])
        {
            moveTimes[i]++;
        }
    }
}

static void calcMoveSteps(int *steps)
{
    for (int i = 0; i < DIRECTION_NUM; i++)
    {
        if (isMoving[i])
        {
            steps[i] = moveStep(moveTimes[i]);
        }
    }

    // if opposite key is pressed, we set the steps all to 0
    if (isMoving[UP] && isMoving[DOWN])
    {
        steps[UP] = 0;
        steps[DOWN] = 0;
    }
    if (isMoving[LEFT] && isMoving[RIGHT])
    {
        steps[LEFT] = 0;
        steps[RIGHT] = 0;
    }
}

// ok, let's write device to move the mouse
static void writeDevice(int *steps)
{
    int x = steps[RIGHT] - steps[LEFT];
    int y = steps[DOWN] - steps[UP];

    struct input_event ev[3];
    memset(ev, 0, sizeof(ev));

    ev[0].type = EV_REL;
    ev[0].code = REL_X;
    ev[0].value = x;

    ev[1].type = EV_REL;
    ev[1].code = REL_Y;
    ev[1].value = y;

    ev[2].type = EV_SYN;
    ev[2].code = SYN_REPORT;

    int ret = write(uinputFd, ev, sizeof(ev));
    ENSURE(ret == sizeof(ev));
}

static void moveMouse()
{
    incMoveTimes();
    int steps[DIRECTION_NUM] = { 0 };
    calcMoveSteps(steps);
    writeDevice(steps);
}

static void *moveMouseThread(void *arg)
{
    (void) arg;

    while(true)
    {
        moveMouse();
        usleep(timeInterval * 1000);
    }

    return nullptr;
}

int main()
{
    uinputFd  = open(UINPUT_NAME, O_WRONLY);
    ENSURE(uinputFd >= 0);

    createVirtualMouse(uinputFd);

    numlockOn = queryNumlock();

    std::vector<std::string> kbds = getAllValidKeyboard();

    ENSURE(kbds.size() >= 1, "There should be at least one keyboard");

    struct pollfd fds[kbds.size()];
    memset(fds, 0, sizeof(fds));

    for (unsigned int i = 0; i < kbds.size(); i++)
    {
        fds[i].fd = open(kbds[i].c_str(), O_RDONLY);
        ENSURE(fds[i].fd >= 0, "Open keyboard %s failed", kbds[i].c_str());
        fds[i].events = POLLIN;
    }

    signal(SIGINT, interrupt_handler);
	signal(SIGTERM, interrupt_handler);

    pthread_t tid;
    int ret = pthread_create(&tid, nullptr, moveMouseThread, nullptr);
    ENSURE(ret == 0);

    resetMoveTimes();

    while (!shouldStop)
    {
        for (unsigned int i = 0; i < kbds.size(); i++)
        {
            fds[i].revents = 0;
        }
        poll(fds, kbds.size(), -1);
        for (unsigned int i = 0; i < kbds.size(); i++)
        {
            if (fds[i].revents & POLLIN)
            {
                handleDevice(fds[i].fd);
            }
        }
    }

    for (unsigned int i = 0; i < kbds.size(); i++)
    {
        close(fds[i].fd);
    }
    return 0;
}