From 1902c111ef6413e1511ef71ecd2947808b5b61db Mon Sep 17 00:00:00 2001 From: Timo Behrendt Date: Mon, 6 May 2024 20:38:43 +0200 Subject: [PATCH] feat: mvp --- .gitignore | 1 + .vscode/launch.json | 7 + .vscode/settings.json | 77 +++++++++++ CMakeLists.txt | 46 +++++++ Makefile | 3 - README.md | 42 ++++++ config.yaml | 7 + schemas/config.schema.json | 43 ++++++ src/main.cpp | 267 +++++++++++++++++++++++++++++++++++++ 9 files changed, 490 insertions(+), 3 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt delete mode 100644 Makefile create mode 100644 config.yaml create mode 100644 schemas/config.schema.json create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore index d93251f..66feb77 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ _deps *.out *.app +build/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5c7247b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..53f11ff --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,77 @@ +{ + "C_Cpp.errorSquiggles": "Disabled", + "yaml.schemas": { + "schemas/config.schema.json": "config.yaml", + }, + "files.associations": { + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "list": "cpp", + "map": "cpp", + "set": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "format": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "variant": "cpp", + "__bit_reference": "cpp", + "__config": "cpp", + "__hash_table": "cpp", + "__locale": "cpp", + "__node_handle": "cpp", + "__split_buffer": "cpp", + "__threading_support": "cpp", + "__tree": "cpp", + "__verbose_abort": "cpp", + "execution": "cpp", + "ios": "cpp", + "locale": "cpp" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..be41204 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.10) + +project(UsbMakroBoard VERSION 1.0) + +# Add the executable target +add_executable(usbmakroboard + src/main.cpp # Add your source files here +) + +# Include yaml-cpp as a dependency +include(FetchContent) + +FetchContent_Declare( + yaml-cpp + GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git + GIT_TAG master +) +FetchContent_GetProperties(yaml-cpp) + +if(NOT yaml-cpp_POPULATED) + message(STATUS "Fetching yaml-cpp...") + FetchContent_Populate(yaml-cpp) + add_subdirectory(${yaml-cpp_SOURCE_DIR} ${yaml-cpp_BINARY_DIR}) +endif() + +# Link yaml-cpp with your executable +target_link_libraries(usbmakroboard PUBLIC yaml-cpp::yaml-cpp) + +# Include spdlog as a dependency +include(FetchContent) + +FetchContent_Declare( + spdlog + GIT_REPOSITORY https://github.com/gabime/spdlog.git + GIT_TAG v1.14.1 +) +FetchContent_GetProperties(spdlog) + +if(NOT spdlog_POPULATED) + message(STATUS "Fetching spdlog...") + FetchContent_Populate(spdlog) + add_subdirectory(${spdlog_SOURCE_DIR} ${spdlog_BINARY_DIR}) +endif() + +# Link spdlog with your executable +target_link_libraries(usbmakroboard PRIVATE spdlog::spdlog) diff --git a/Makefile b/Makefile deleted file mode 100644 index eb317bb..0000000 --- a/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -main : main.c - gcc -o main main.c - sudo chmod a+r /dev/input/by-id/usb-MAX_Falcon_20_RGB-if02-event-kbd diff --git a/README.md b/README.md index 68e3579..6cb1a80 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ # UsbMakroBoard +## Cudos + +For info that with "EVIOCGRAB" the keyboard events can be consumed exclusively by one application: https://stackoverflow.com/questions/29942421/read-barcodes-from-input-event-linux-c/29956584#29956584 + +## Arguments + +``` +-c +``` + +## Configuration + +The configuration can be + +## Allowing non-root access to the device + +By default, accessing input devices requires root privileges. However, it's possible to allow a regular user to access a specific input device by creating a udev rule: + +1. Identify the device's vendor and product IDs. You can do this by running `lsusb` in the terminal and looking for your device. + This may looke like this where `195d` is the `vendor_id` and `6008` is the `product_id`: + ```bash + # lsusb + Bus 003 Device 005: ID 195d:6008 Itron Technology iONE Falcon 20 RGB + ``` + +1. Create a new udev rule. Open a new file in the `/etc/udev/rules.d/` directory. The filename should end with `.rules`, for example `90-input.rules` (the number determins the order in which the rules are loaded. It is generally recommended to chose higher numbers for changes that are relevant for the user-space). + + In the new file, write a rule that matches your device and sets the mode to `0666` (read and write permissions for everyone). Replace `vendor_id` and `product_id` with your device's IDs from the first step. + + ```bash + SUBSYSTEM=="input", ATTRS{idVendor}=="vendor_id", ATTRS{idProduct}=="product_id", MODE="0666" + ``` + +3. Reload the udev rules with the command `sudo udevadm control --reload-rules && sudo udevadm trigger`. + +Now, every time the device is connected, it will be accessible by all users. The rule can be made more specific to only make the device accessible to a certain user or user group. You can use the `OWNER`, `GROUP`, and `MODE` parameters in the udev rule. For example, to restrict access to the user `username` and the group `usergroup`, you can use: + +```bash +SUBSYSTEM=="input", ATTRS{idVendor}=="vendor_id", ATTRS{idProduct}=="product_id", OWNER="username", GROUP="usergroup", MODE="0660" +``` + +This will give read and write permissions to the user and/or the group, and no permissions to others. diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..ce86fc2 --- /dev/null +++ b/config.yaml @@ -0,0 +1,7 @@ +logLevel: "trace" +devicePath: "/dev/input/by-id/usb-MAX_Falcon_20_RGB-if02-event-kbd" +keyboard_layout: + row0: + keys: + key0: + script: "/home/tbehrendt/.screenlayout/default.sh" diff --git a/schemas/config.schema.json b/schemas/config.schema.json new file mode 100644 index 0000000..2f8a6c8 --- /dev/null +++ b/schemas/config.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "devicePath": { + "type": "string" + }, + "logLevel": { + "type": "string", + "enum": ["trace", "debug", "info", "warn", "error", "critical"], + "default": "info" + }, + "keyboard_layout": { + "type": "object", + "patternProperties": { + "^row[0-4]+": { + "type": "object", + "properties": { + "keys": { + "type": "object", + "patternProperties": { + "^key[0-3]+$": { + "type": "object", + "properties": { + "script": { + "type": "string" + } + }, + "required": ["script"], + "additionalProperties": false + } + } + } + }, + "required": ["keys"], + "additionalProperties": false + } + } + } + }, + "required": ["keyboard_layout", "devicePath"], + "additionalProperties": false +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..e6dcf92 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,267 @@ +// https://stackoverflow.com/questions/20943322/accessing-keys-from-linux-input-device + +/** + * TODO: Handle the keyboard being unplugged/turned off and replugged/turned on again + */ +#include +#include +#include +#include +#include +#include +#include + +struct KeyPosition +{ + int row; + int col; +}; + +struct ConfigKey +{ + std::string name; + std::string script; +}; + +struct ConfigRow +{ + std::vector keys; +}; + +struct KeyboardConfig +{ + std::string devicePath; + std::vector keyboardLayout; + std::string logLevel; +}; + +static const char *const eventValues[3] = { + "RELEASED", + "PRESSED ", + "REPEATED"}; + +std::shared_ptr logger; + +std::pair mapKeyEventToRowColumn(int keyEventNumber, const std::unordered_map &keyMap) +{ + auto it = keyMap.find(keyEventNumber); + if (it != keyMap.end()) + { + return std::make_pair(it->second.row, it->second.col); + } + else + { + return std::make_pair(-1, -1); + } +} + +std::string getConfigPathFromCliArguments(int argc, char *argv[]) +{ + std::string configPath = "config.yaml"; + int opt; + while ((opt = getopt(argc, argv, "c:")) != -1) + { + switch (opt) + { + case 'c': + configPath = optarg; + break; + default: + std::cerr << "Usage: " << argv[0] << " [-c config_file_path]\n"; + exit(EXIT_FAILURE); + } + } + return configPath; +} + +YAML::Node loadConfig(const std::string &configPath) +{ + return YAML::LoadFile(configPath); +} + +int openDevice(const std::string &devicePath) +{ + int fdKeyboard = open(devicePath.c_str(), O_RDONLY); + + if (fdKeyboard == -1) + { + logger->error("Cannot open {}: {}.\n", devicePath.c_str(), strerror(errno)); + exit(EXIT_FAILURE); + } + + return fdKeyboard; +} + +KeyboardConfig parseConfigYAML(const std::string &filename) +{ + KeyboardConfig config; + + YAML::Node root = YAML::LoadFile(filename); + + config.devicePath = root["devicePath"].as(); + config.logLevel = root["logLevel"].as(); + + // Initialize keyboard layout + config.keyboardLayout.resize(5); + for (int i = 0; i < 5; ++i) + { + config.keyboardLayout[i].keys.resize(4); + } + + YAML::Node keyboardLayout = root["keyboard_layout"]; + for (const auto &row : keyboardLayout) + { + int rowIdx = std::stoi(row.first.as().substr(3)); // Extract row index from key name + YAML::Node keys = row.second["keys"]; + for (const auto &key : keys) + { + int keyIdx = std::stoi(key.first.as().substr(3)); // Extract key index from key name + config.keyboardLayout[rowIdx].keys[keyIdx].script = key.second["script"].as(); + } + } + + return config; +} + +void initLogger(const spdlog::level::level_enum logLevel) +{ + auto consoleSink = std::make_shared(); + + logger = std::make_shared("logger", consoleSink); + + logger->set_level(logLevel); + spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%L] %v"); + spdlog::register_logger(logger); +} + +spdlog::level::level_enum convertLogLevelToSpdLog(const std::string logLevelStr) +{ + if (logLevelStr == "trace") + { + return spdlog::level::trace; + } + else if (logLevelStr == "debug") + { + return spdlog::level::debug; + } + else if (logLevelStr == "info") + { + return spdlog::level::info; + } + else if (logLevelStr == "warn") + { + return spdlog::level::warn; + } + else if (logLevelStr == "error") + { + return spdlog::level::err; + } + else if (logLevelStr == "critical") + { + return spdlog::level::critical; + } + else + { + return spdlog::level::info; + } +} + +void grabReleaseInputDevice(const int fdKeyboard) +{ + const int ioControlResult = ioctl(fdKeyboard, EVIOCGRAB, 1); + + if (ioControlResult == -1) + { + logger->error("Cannot grab input device: {}", strerror(errno)); + close(fdKeyboard); + exit(EXIT_FAILURE); + } +} + +int main(int argc, char *argv[]) +{ + /** + * +--+--+--+--+ + * |30|48|46|32| + * +--+--+--+--+ + * |18|33|34|35| + * +--+--+--+--+ + * |23|36|37|38| + * +--+--+--+--+ + * |50|49|24|25| + * +--+--+--+--+ + * |16|19|31|20| + * +--+--+--+--+ + * */ + const std::unordered_map keyMap = { + {30, {0, 0}}, {48, {0, 1}}, {46, {0, 2}}, {32, {0, 3}}, {18, {1, 0}}, {33, {1, 1}}, {34, {1, 2}}, {35, {1, 3}}, {23, {2, 0}}, {36, {2, 1}}, {37, {2, 2}}, {38, {2, 3}}, {50, {3, 0}}, {49, {3, 1}}, {24, {3, 2}}, {25, {3, 3}}, {16, {4, 0}}, {19, {4, 1}}, {31, {4, 2}}, {20, {4, 3}}}; + + const KeyboardConfig config = parseConfigYAML(getConfigPathFromCliArguments(argc, argv)); + + initLogger(convertLogLevelToSpdLog(config.logLevel)); + + const int fdKeyboard = openDevice(config.devicePath); + + grabReleaseInputDevice(fdKeyboard); + + struct input_event inputEvent; + ssize_t eventLength; + + while (1) + { + eventLength = read(fdKeyboard, &inputEvent, sizeof inputEvent); + + if (eventLength == (ssize_t)-1) + { + if (errno == EINTR) + { + logger->warn("Interrupted system call"); + continue; + } + else if (errno == ENODEV) + { + logger->error("Device disconnected. Exiting"); + break; + } + else + { + printf("Error reading from device: %s (%d)\n", strerror(errno), errno); + break; + } + } + + /** + * inputEvent.value can be + * 0: RELEASED + * 1: PRESSED + * 2: REPEATED + */ + if (inputEvent.type == EV_KEY && inputEvent.value == 0) + { + const std::pair keyPosition = mapKeyEventToRowColumn(inputEvent.code, keyMap); + + if (keyPosition.first == -1 || keyPosition.second == -1) + { + logger->error("Key {} not found in key map", inputEvent.code); + continue; + } + + const std::string scriptToRun = config.keyboardLayout[keyPosition.first].keys[keyPosition.second].script; + + logger->debug("Pressed key: {}x{}", keyPosition.first, keyPosition.second); + + if (!scriptToRun.empty()) + { + logger->info("Running script: {}", scriptToRun); + system(scriptToRun.c_str()); + } + else + { + logger->warn("No script found for key: {}x{}", keyPosition.first, keyPosition.second); + } + } + } + + close(fdKeyboard); + return EXIT_FAILURE; +}