From 10769b50f951a3adf889b479a3c7eaa9a2858cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 18 Nov 2024 10:13:42 +0100 Subject: [PATCH 1/2] New plugin: Add linux input device support --- linuxinput/inputdevice.cpp | 155 +++++++++++++++++++++++++ linuxinput/inputdevice.h | 60 ++++++++++ linuxinput/inputdeviceeventmonitor.cpp | 62 ++++++++++ linuxinput/inputdeviceeventmonitor.h | 24 ++++ nymea-plugins.pro | 1 + 5 files changed, 302 insertions(+) create mode 100644 linuxinput/inputdevice.cpp create mode 100644 linuxinput/inputdevice.h create mode 100644 linuxinput/inputdeviceeventmonitor.cpp create mode 100644 linuxinput/inputdeviceeventmonitor.h diff --git a/linuxinput/inputdevice.cpp b/linuxinput/inputdevice.cpp new file mode 100644 index 000000000..3852964d8 --- /dev/null +++ b/linuxinput/inputdevice.cpp @@ -0,0 +1,155 @@ +#include "inputdevice.h" + +#include +#include +#include + +#include "extern-plugininfo.h" +#include "inputdeviceeventmonitor.h" + +#include + +#include +#include + +InputDevice::InputDevice(QObject *parent) + : QObject{parent} +{ + +} + +InputDevice::InputDevice(Info inputDeviceInfo, QObject *parent) + : QObject{parent}, + m_inputDeviceInfo{inputDeviceInfo}, + m_pollThread{new QThread(nullptr)} +{ + qCDebug(dcLinuxInput()) << "Constructing" << inputDeviceInfo; + InputDeviceEventMonitor *monitor = new InputDeviceEventMonitor(inputDeviceInfo.eventFilePath, nullptr); + monitor->moveToThread(m_pollThread); + + connect(m_pollThread, &QThread::started, monitor, &InputDeviceEventMonitor::process); + connect(monitor, &InputDeviceEventMonitor::finished, m_pollThread, &QThread::quit); + connect(monitor, &InputDeviceEventMonitor::finished, monitor, &InputDeviceEventMonitor::deleteLater); + connect(m_pollThread, &QThread::finished, monitor, &InputDeviceEventMonitor::deleteLater); + connect(m_pollThread, &QThread::finished, m_pollThread, [this](){ + qCDebug(dcLinuxInput()) << "Monitoring thread finished" << m_inputDeviceInfo; + m_pollThread->deleteLater(); + m_pollThread = nullptr; + }); + + connect(monitor, &InputDeviceEventMonitor::eventOccurred, this, &InputDevice::onEventOccurred); + + qCDebug(dcLinuxInput()) << "Start monitoring" << inputDeviceInfo; + m_pollThread->start(); +} + +InputDevice::~InputDevice() +{ + if (m_pollThread) { + m_pollThread->terminate(); + } +} + +InputDevice::Info InputDevice::inputDeviceInfo() const +{ + return m_inputDeviceInfo; +} + +QList InputDevice::availableInputDevices() +{ + QList inputeDevices; + QStringList addedInputIds; + QDir inputByIdDir("/dev/input/by-path/"); + + // Get the keyboards + qCDebug(dcLinuxInput()) << "---------- Keyboards:"; + foreach (const QString &inputIdName, inputByIdDir.entryList({"*event-kbd"}, QDir::Dirs | QDir::NoDotAndDotDot)) { + Info info = parseDeviceInfo(Keyboard, QFileInfo(inputByIdDir.absolutePath() + QDir::separator() + inputIdName)); + qCDebug(dcLinuxInput()) << " --> " << info; + inputeDevices.append(info); + addedInputIds.append(inputIdName); + } + + qCDebug(dcLinuxInput()) << "---------- Mouse:"; + foreach (const QString &inputIdName, inputByIdDir.entryList({"*event-mouse"}, QDir::Dirs | QDir::NoDotAndDotDot)) { + Info info = parseDeviceInfo(Mouse, QFileInfo(inputByIdDir.absolutePath() + QDir::separator() + inputIdName)); + qCDebug(dcLinuxInput()) << " --> " << info; + inputeDevices.append(info); + addedInputIds.append(inputIdName); + } + + qCDebug(dcLinuxInput()) << "---------- Other:"; + foreach (const QString &inputIdName, inputByIdDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + if (addedInputIds.contains(inputIdName)) + continue; + + Info info = parseDeviceInfo(Other, QFileInfo(inputByIdDir.absolutePath() + QDir::separator() + inputIdName)); + qCDebug(dcLinuxInput()) << " --> " << info; + inputeDevices.append(info); + addedInputIds.append(inputIdName); + } + + return inputeDevices; +} + +void InputDevice::onEventOccurred(quint16 type, quint16 code, qint32 value) +{ + if (type != EV_KEY) + return; + + qCDebug(dcLinuxInput()) << "Event occurred" << type << code << value; + emit keyPressed(code, value); +} + +QString InputDevice::readFileContent(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCWarning(dcLinuxInput()) << "Could not open file" << fileName << ":" << file.errorString(); + return QString(); + } + + QString content; + QTextStream in(&file); + content = in.readAll(); + file.close(); + + return content.trimmed(); +} + +InputDevice::Info InputDevice::parseDeviceInfo(Type type, const QFileInfo &inputLinkFileInfo) +{ + QFileInfo inputFileInfo(inputLinkFileInfo.symLinkTarget()); + + QDir inputsDir("/sys/class/input/"); + QDir inputDir(inputsDir.absolutePath() + QDir::separator() + inputFileInfo.baseName() + QDir::separator() + "device"); + + Info info; + info.type = type; + info.id = QString(inputFileInfo.baseName()).remove("event").toInt(); + info.name = readFileContent(inputDir.absolutePath() + QDir::separator() + "name"); + info.phys = readFileContent(inputDir.absolutePath() + QDir::separator() + "phys"); + info.eventFilePath = inputLinkFileInfo.symLinkTarget(); + + return info; +} + +QDebug operator<<(QDebug debug, InputDevice::Info inputDeviceInfo) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "Info(" << inputDeviceInfo.type + << ", name: " << inputDeviceInfo.name + << ", Phys: " << inputDeviceInfo.phys + << ", " << inputDeviceInfo.eventFilePath + << ')'; + return debug; +} + +QDebug operator<<(QDebug debug, InputDevice *inputDevice) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "InputDevice(" << inputDevice->inputDeviceInfo() << ')'; + return debug; +} + + diff --git a/linuxinput/inputdevice.h b/linuxinput/inputdevice.h new file mode 100644 index 000000000..1a0544e0c --- /dev/null +++ b/linuxinput/inputdevice.h @@ -0,0 +1,60 @@ +#ifndef INPUTDEVICE_H +#define INPUTDEVICE_H + +#include +#include +#include +#include + +#include +#include + +class InputDevice : public QObject +{ + Q_OBJECT +public: + enum Type { + Unknown, + Keyboard, + Mouse, + Other + }; + Q_ENUM(Type) + + typedef struct Info { + int id = -1; + Type type = Unknown; + QString name; + QString description; + QString phys; + QString eventFilePath; + } InputDeviceInfo; + + explicit InputDevice(QObject *parent = nullptr); + explicit InputDevice(Info inputDeviceInfo, QObject *parent = nullptr); + ~InputDevice(); + + InputDevice::Info inputDeviceInfo() const; + + static QList availableInputDevices(); + +signals: + void keyPressed(quint16 code, qint32 value); + +private slots: + void onEventOccurred(quint16 type, quint16 code, qint32 value); + +private: + QString m_eventFileName; + static QString readFileContent(const QString &fileName); + Info m_inputDeviceInfo; + QThread *m_pollThread = nullptr; + + static InputDevice::Info parseDeviceInfo(Type type, const QFileInfo &inputLinkFileInfo); +}; + +QDebug operator<<(QDebug debug, InputDevice::Info inputDeviceInfo); +QDebug operator<<(QDebug debug, InputDevice *inputDevice); + + +#endif // INPUTDEVICE_H diff --git a/linuxinput/inputdeviceeventmonitor.cpp b/linuxinput/inputdeviceeventmonitor.cpp new file mode 100644 index 000000000..dbf3dc0d3 --- /dev/null +++ b/linuxinput/inputdeviceeventmonitor.cpp @@ -0,0 +1,62 @@ +#include "inputdeviceeventmonitor.h" +#include "extern-plugininfo.h" + +#include +#include + +#include +#include +#include + +InputDeviceEventMonitor::InputDeviceEventMonitor(const QString &fileName, QObject *parent) + : QObject{parent}, + m_fileName{fileName} +{ + +} + +void InputDeviceEventMonitor::process() +{ + struct libevdev *device = nullptr; + + int fd = open(m_fileName.toStdString().c_str(), O_RDWR|O_CLOEXEC); + if (fd < 0) { + qCWarning(dcLinuxInput()) << "Could not open file" << m_fileName; + emit finished(); + return; + } + + int status = libevdev_new_from_fd(fd, &device); + if (status != 0) { + qCWarning(dcLinuxInput()) << "Failed to create evdev from file descriptor"; + close(fd); + emit finished(); + return; + } + + qCDebug(dcLinuxInput()) << "Start monitoring events on" << m_fileName; + qCDebug(dcLinuxInput()) << "- name: " << libevdev_get_name(device); + qCDebug(dcLinuxInput()) << "- phys: " << libevdev_get_phys(device); + + struct input_event event = {}; + auto hasError = [](int v) { + return v < 0 && v != -EAGAIN; + }; + auto hasNextEvent = [](int v) { + return v >= 0; + }; + + while (status = libevdev_next_event(device, LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_BLOCKING, &event), !hasError(status)) { + if (!hasNextEvent(status)) + continue; + + emit eventOccurred(event.type, event.code, event.value); + } + + libevdev_free(device); + device = nullptr; + + close(fd); + + emit finished(); +} diff --git a/linuxinput/inputdeviceeventmonitor.h b/linuxinput/inputdeviceeventmonitor.h new file mode 100644 index 000000000..d6d49aa2f --- /dev/null +++ b/linuxinput/inputdeviceeventmonitor.h @@ -0,0 +1,24 @@ +#ifndef INPUTDEVICEEVENTMONITOR_H +#define INPUTDEVICEEVENTMONITOR_H + +#include + +class InputDeviceEventMonitor : public QObject +{ + Q_OBJECT +public: + explicit InputDeviceEventMonitor(const QString &fileName, QObject *parent = nullptr); + +public slots: + void process(); + +signals: + void eventOccurred(quint16 type, quint16 code, qint32 value); + void finished(); + +private: + QString m_fileName; + +}; + +#endif // INPUTDEVICEEVENTMONITOR_H diff --git a/nymea-plugins.pro b/nymea-plugins.pro index b03b09504..b31433908 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -37,6 +37,7 @@ PLUGIN_DIRS = \ kodi \ lgsmarttv \ lifx \ + linuxinput \ logilink \ mecelectronics \ meross \ From dc780ac4fbf4b75c96ba767fdd21500388be0391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Tue, 19 Nov 2024 11:04:21 +0100 Subject: [PATCH 2/2] Add debian packaging for linux-input --- debian/control | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/debian/control b/debian/control index e894e2a71..06fd5bda8 100644 --- a/debian/control +++ b/debian/control @@ -347,6 +347,17 @@ Conflicts: nymea-plugins-translations (<< 1.0.1) Description: nymea integration plugin for lifx This package contains the nymea integration plugin for lifx devices + +Package: nymea-plugin-linuxinput +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Conflicts: nymea-plugins-translations (<< 1.0.1) +Description: nymea integration plugin for linux input devices + This package contains the nymea integration plugin integrating generic linux + input devices like keyboards, mouse and joystick devices. + + Package: nymea-plugin-logilink Architecture: any Section: libs