diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c44ba31 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.14) +project(logiops) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -Wall") + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_subdirectory(src/logid) + +if(NOT EXISTS "${PROJECT_BINARY_DIR}/logid.cfg") + configure_file("logid.example.cfg" "logid.cfg" COPYONLY) +endif() \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0944179 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# logiops + +This is an unofficial driver for Logitech mice and keyboard. + +This is currently only compatible with HID++ \>2.0 devices. + +## Building + +This project requires a C++14 compiler, cmake, libevdev, libconfig, and [libhidpp](https://github.com/cvuchener/hidpp) + +To build this project, run: + +``` +mkdir build +cd build +cmake .. +make +``` + +Installation is currently not implemented. + +## Compatible Devices + +| Device | Compatible? | +|:---------:|:-----------:| +| MX Master | Yes | +| T400 | Untested | +| K400r | Untested | +| K350 | Untested | +| M325c | Untested | + +I own the MX Master, T400, K400r, K350, and M325c. Feel free to add to this list if you can test any additional devices. \ No newline at end of file diff --git a/logid.example.cfg b/logid.example.cfg new file mode 100644 index 0000000..1ecf303 --- /dev/null +++ b/logid.example.cfg @@ -0,0 +1,71 @@ +devices: ( +{ + name: "MX Master"; + smartshift: + { + on: true; + threshold: 30; + }; + hiresscroll: true; + dpi: 1000; + + buttons: ( + { + cid: 0xc3; + action = + { + type: "Gestures"; + gestures: ( + { + direction: "Up"; + mode: "OnRelease"; + action = + { + type: "Keypress"; + keys: ["KEY_UP"]; + }; + }, + { + direction: "Down"; + mode: "OnRelease"; + action = + { + type: "Keypress"; + keys: ["KEY_DOWN"]; + }; + }, + { + direction: "Left"; + mode: "OnRelease"; + action = + { + type: "CycleDPI"; + dpis: [400, 600, 800, 1000, 1200, 1400, 1600]; + }; + }, + { + direction: "Right"; + mode: "OnRelease"; + action = + { + type = "ToggleSmartshift"; + } + }, + { + direction: "None" + mode: "NoPress" + } + ); + }; + }, + { + cid: 0xc4; + action = + { + type: "Keypress"; + keys: ["KEY_A"]; + }; + } + ); +} +); \ No newline at end of file diff --git a/src/logid/Actions.cpp b/src/logid/Actions.cpp new file mode 100644 index 0000000..e031df1 --- /dev/null +++ b/src/logid/Actions.cpp @@ -0,0 +1,154 @@ +#include +#include +#include + +#include "Actions.h" +#include "util.h" +#include "EvdevDevice.h" + +KeyAction::KeyAction(const KeyAction &a, Device* d) : ButtonAction(Action::Keypress) +{ + device = d; + std::copy(a.keys.begin(), a.keys.end(), std::back_inserter(keys)); +} + +void KeyAction::press() +{ + //KeyPress event for each in keys + for(unsigned int i : keys) + global_evdev->send_event(EV_KEY, i, 1); +} + +void KeyAction::release() +{ + //KeyRelease event for each in keys + for(unsigned int i : keys) + global_evdev->send_event(EV_KEY, i, 0); +} + +void GestureAction::press() +{ + held = true; + x = 0; + y = 0; +} + +void GestureAction::move(HIDPP20::IReprogControlsV4::Move m) +{ + x += m.x; + y += m.y; +} + +void GestureAction::release() +{ + held = false; + auto direction = get_direction(x, y); + + auto g = gestures.find(direction); + + if(g != gestures.end() && g->second->mode == GestureMode::OnRelease) + { + g->second->action->press(); + g->second->action->release(); + } +} + +void SmartshiftAction::press() +{ + if(device->features.find(0x2110) == device->features.end()) + { + log_printf(DEBUG, "Error toggling smart shift, feature is non-existent."); + return; + } + const uint8_t f_index = device->features.find(0x2110)->second; + std::vector results; + + try + { + results = device->hidpp_dev->callFunction(f_index, 0x00); + if(results[0] == 0x02) + results = device->hidpp_dev->callFunction(f_index, 0x01, {0x01}); + else if(results[0] == 0x01) + results = device->hidpp_dev->callFunction(f_index, 0x01, {0x02}); + else + results = device->hidpp_dev->callFunction(f_index, 0x01, {0x01}); + } + catch(HIDPP20::Error &e) + { + log_printf(ERROR, "Error toggling smart shift, code %d: %s\n", e.errorCode(), e.what()); + return; + } +} + +void HiresScrollAction::press() +{ + if(device->features.find(0x2110) == device->features.end()) + { + log_printf(DEBUG, "Error toggling hires scroll, feature is non-existent."); + return; + } + const uint8_t f_index = device->features.find(0x2121)->second; + std::vector results; + + try + { + results = device->hidpp_dev->callFunction(f_index, 0x01); + if(results[0] == 0x02) + results = device->hidpp_dev->callFunction(f_index, 0x02, {0x00}); + else + results = device->hidpp_dev->callFunction(f_index, 0x02, {0x02}); + } + catch(HIDPP20::Error &e) + { + log_printf(ERROR, "Error toggling hires scroll, code %d: %s\n", e.errorCode(), e.what()); + return; + } +} + +void CycleDPIAction::press() +{ + HIDPP20::IAdjustableDPI iad(device->hidpp_dev); + + try + { + for (uint i = 0; i < iad.getSensorCount(); i++) + { + int current_dpi = std::get<0>(iad.getSensorDPI(i)); + bool found = false; + for (uint j = 0; j < dpis.size(); j++) + { + if (dpis[j] == current_dpi) + { + if (j == dpis.size() - 1) + iad.setSensorDPI(i, dpis[0]); + else + iad.setSensorDPI(i, dpis[j + 1]); + found = true; + break; + } + } + if (found) break; + if (dpis.empty()) iad.setSensorDPI(i, std::get<1>(iad.getSensorDPI(i))); + else iad.setSensorDPI(i, dpis[0]); + } + } + catch(HIDPP20::Error &e) { log_printf(ERROR, "Error while cycling DPI: %s", e.what()); } +} + +void ChangeDPIAction::press() +{ + HIDPP20::IAdjustableDPI iad(device->hidpp_dev); + + try + { + for(uint i = 0; i < iad.getSensorCount(); i++) + { + int current_dpi = std::get<0>(iad.getSensorDPI(i)); + iad.setSensorDPI(i, current_dpi + dpi_inc); + } + } + catch(HIDPP20::Error &e) + { + log_printf(ERROR, "Error while incrementing DPI: %s", e.what()); + } +} \ No newline at end of file diff --git a/src/logid/Actions.h b/src/logid/Actions.h new file mode 100644 index 0000000..06f3b1c --- /dev/null +++ b/src/logid/Actions.h @@ -0,0 +1,122 @@ +#ifndef LOGIOPS_ACTIONS_H +#define LOGIOPS_ACTIONS_H + +#include +#include +#include "Device.h" + +enum class Action +{ + None, + Keypress, + Gestures, + CycleDPI, + ChangeDPI, + ToggleSmartshift, + ToggleHiresScroll +}; +enum class Direction +{ + None, + Up, + Down, + Left, + Right +}; +enum class GestureMode +{ + NoPress, + OnRelease, + OnFewPixels +}; + +class Device; + +class ButtonAction +{ +public: + Action type; + virtual void press() = 0; + virtual void release() = 0; +// ButtonAction(const ButtonAction &a, Device* d) : type (a.type), device (d) {} +// ButtonAction* create_instance(Device* d); +protected: + ButtonAction(Action a) : type (a) {}; + Device* device; +}; +class NoAction : public ButtonAction +{ +public: + NoAction() : ButtonAction(Action::None) {} + virtual void press() {} + virtual void release() {} +}; +class KeyAction : public ButtonAction +{ +public: + explicit KeyAction(std::vector k) : ButtonAction(Action::Keypress), keys (std::move(k)) {}; + KeyAction(const KeyAction &a, Device* d); + //virtual KeyAction* create_instance(Device* d) { return new KeyAction(*this, d); }; + virtual void press(); + virtual void release(); +private: + std::vector keys; +}; +class Gesture +{ +public: + Gesture(ButtonAction* ba, GestureMode m, int pp=0) : action (ba), mode (m), per_pixel (pp) {}; + Gesture(const Gesture &g) : action (g.action), mode (g.mode), per_pixel (g.per_pixel) {}; + + ButtonAction* action; + + GestureMode mode; + int per_pixel; +}; +class GestureAction : public ButtonAction +{ +public: + GestureAction(std::map g) : ButtonAction(Action::Gestures), gestures (std::move(g)) {}; + std::map gestures; + virtual void press(); + void move(HIDPP20::IReprogControlsV4::Move m); + virtual void release(); +private: + bool held; + int x; + int y; +}; +class SmartshiftAction : public ButtonAction +{ +public: + SmartshiftAction() : ButtonAction(Action::ToggleSmartshift) {}; + virtual void press(); + virtual void release() {} +}; +class HiresScrollAction : public ButtonAction +{ +public: + HiresScrollAction() : ButtonAction(Action::ToggleHiresScroll) {}; + virtual void press(); + virtual void release() {} +}; +class CycleDPIAction : public ButtonAction +{ +public: + CycleDPIAction(std::vector d) : ButtonAction(Action::CycleDPI), dpis (d) {}; + virtual void press(); + virtual void release() {} +private: + const std::vector dpis; +}; +class ChangeDPIAction : public ButtonAction +{ +public: + ChangeDPIAction(int i) : ButtonAction(Action::ChangeDPI), dpi_inc (i) {}; + virtual void press(); + virtual void release() {} +private: + int dpi_inc; +}; + +#endif //LOGIOPS_ACTIONS_H diff --git a/src/logid/CMakeLists.txt b/src/logid/CMakeLists.txt new file mode 100644 index 0000000..6b8a5ab --- /dev/null +++ b/src/logid/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.14) +project(logid) + +set(CMAKE_CXX_STANDARD 14) + +set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/../CMake") + +find_package(Threads REQUIRED) +find_package(PkgConfig REQUIRED) + +add_executable(logid + logid.cpp + util.cpp + util.h + Configuration.cpp + Configuration.h + Actions.cpp + Actions.h + Device.cpp + Device.h + DeviceFinder.cpp + DeviceFinder.h + EvdevDevice.cpp + EvdevDevice.h) +set_target_properties(logid PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +pkg_check_modules(PC_EVDEV libevdev) +pkg_check_modules(libhidpp REQUIRED) +pkg_check_modules(libconfig++ REQUIRED) + +find_path(HIDPP_INCLUDE_DIR hidpp) +find_library(HIDPP_LIBRARY libhidpp.so) + +find_path(EVDEV_INCLUDE_DIR libevdev/libevdev.h + HINTS ${PC_EVDEV_INCLUDE_DIRS} ${PC_EVDEV_INCLUDEDIR}) +find_library(EVDEV_LIBRARY + NAMES evdev libevdev) + +include_directories(${HIDPP_INCLUDE_DIR}/hidpp ${EVDEV_INCLUDE_DIR}) + +target_link_libraries(logid ${CMAKE_THREAD_LIBS_INIT} ${EVDEV_LIBRARY} config++ ${HIDPP_LIBRARY}) diff --git a/src/logid/Configuration.cpp b/src/logid/Configuration.cpp new file mode 100644 index 0000000..e7e8900 --- /dev/null +++ b/src/logid/Configuration.cpp @@ -0,0 +1,386 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "Configuration.h" +#include "util.h" + +using namespace libconfig; + +Configuration::Configuration(const char *config_file) +{ + //Read config file + try + { + cfg.readFile(config_file); + } + catch(const FileIOException &e) + { + log_printf(ERROR, "%s", "I/O Error while reading configuration file!"); + throw e; + } + catch(const ParseException &e) + { + log_printf(ERROR, "Parse error in %s, line %d: %s", e.getFile(), e.getLine(), e.getError()); + throw e; + } + const Setting &root = cfg.getRoot(); + Setting* _devices; + + try { _devices = &root["devices"]; } + catch(const SettingNotFoundException &e) + { + log_printf(WARN, "No devices listed in config file."); + return; + } + + for(int i = 0; i < _devices->getLength(); i++) + { + const Setting &device = (*_devices)[i]; + std::string name; + try + { + if(!device.lookupValue("name", name)) + { + log_printf(WARN, "Line %d: 'name' must be a string, skipping device.", device["name"].getSourceLine()); + continue; + } + } + catch(SettingNotFoundException &e) + { + log_printf(WARN, "Line %d: Missing 'name' field, skipping device.", device.getSourceLine()); + continue; + } + devices.insert({name, new DeviceConfig(device)}); + } +} + +DeviceConfig::DeviceConfig(const libconfig::Setting &root) +{ + try + { + int d; + if(!root.lookupValue("dpi", d)) + throw SettingTypeException(root["dpi"]); + dpi = new int(d); + } + catch(const SettingNotFoundException &e) + { + log_printf(INFO, "Missing dpi option, not setting."); + } + catch(const SettingTypeException &e) + { + log_printf(WARN, "Line %d: DPI must me an integer; not setting.", root["dpi"].getSourceLine()); + } + + try + { + bool b; + if(!root.lookupValue("hiresscroll", b)) + throw SettingTypeException(root["hiresscroll"]); + hiresscroll = new bool(b); + } + catch(const SettingNotFoundException &e) + { + log_printf(INFO, "Missing hiresscroll option, not setting."); + } + catch(const SettingTypeException &e) + { + log_printf(WARN, "Line %d: DPI must me an integer; not setting.", root["hiresscroll"].getSourceLine()); + } + + try + { + const Setting& ss = root["smartshift"]; + smartshift = new smartshift_options; + bool on; + int threshold; + try + { + if (ss.lookupValue("on", on)) smartshift->on = new bool(on); + else log_printf(WARN, "Line %d: on field must be a boolean", ss["on"].getSourceLine()); + } + catch(const SettingNotFoundException &e) { } + + try + { + if (ss.lookupValue("threshold", threshold)) + { + if(threshold < 0) + { + threshold = 1; + log_printf(INFO, "Smartshift threshold must be > 0 or < 100, setting to 0."); + } + if(threshold >= 100) + { + threshold = 99; + log_printf(INFO, "Smartshift threshold must be > 0 or < 100, setting to 99."); + } + smartshift->threshold = new uint8_t(threshold); + } + else log_printf(WARN, "Line %d: threshold must be an integer", ss["threshold"].getSourceLine()); + } + catch(const SettingNotFoundException &e) + { + log_printf(INFO, "Missing threshold for smartshift, not setting."); + } + } + catch(const SettingNotFoundException &e) { } + catch(const SettingTypeException &e) + { + log_printf(WARN, "Line %d: smartshift field must be an object", root["hiressscroll"].getSourceLine()); + } + + Setting* buttons; + try + { + buttons = &root["buttons"]; + } + catch(const SettingNotFoundException &e) + { + log_printf(WARN, "No button configuration found, reverting to null config."); + new std::map(); + return; + } + + for(int i = 0; i < buttons->getLength(); i++) + { + const Setting &button = (*buttons)[i]; + + int cid; + try { button.lookupValue("cid", cid); } + catch(SettingNotFoundException &e) + { + log_printf(WARN, "Entry on line %d is missing a cid", button.getSourceLine()); + continue; + } + + if(actions.find(cid) != actions.end()) + { + log_printf(WARN, "Duplicate entries for cid 0x%x, skipping entry on line %d", cid, button.getSourceLine()); + continue; + } + + Setting* action_config; + try { action_config = &button["action"]; } + catch(SettingNotFoundException &e) + { + log_printf(WARN, "cid 0x%x is missing an action, not diverting!", cid); + continue; + } + + Action action_type; + try + { + std::string action_type_str; + action_config->lookupValue("type", action_type_str); + action_type = string_to_action(action_type_str); + } + catch(SettingNotFoundException &e) + { + log_printf(WARN, "cid 0x%x is missing an action type, not diverting!", cid); + continue; + } + catch(std::invalid_argument &e) + { + log_printf(WARN, "Line %d: %s", (*action_config)["type"].getSourceLine(), e.what()); + continue; + } + + try { actions.insert({cid, parse_action(action_type, action_config)}); } + catch(std::exception &e) { log_printf(ERROR, "%s", e.what()); } + } +} + +ButtonAction* parse_action(Action type, const Setting* action_config, bool is_gesture) +{ + if(type == Action::None) return new NoAction(); + if(type == Action::Keypress) + { + std::vector keys; + try + { + const Setting &keys_config = (*action_config)["keys"]; + for (int i = 0; i < keys_config.getLength(); i++) + { + int keycode = libevdev_event_code_from_name(EV_KEY, keys_config[i]); + if(keycode == -1) + { + const char* keyname = keys_config[i]; + log_printf(WARN, "%s is not a valid keycode, skipping", keyname); + } + else keys.push_back(keycode); + } + } + catch(SettingNotFoundException &e) + { + log_printf(WARN, "Expected keys parameter on line %d", action_config->getSourceLine()); + } + + return new KeyAction(keys); + } + else if(type == Action::Gestures) + { + if(is_gesture) + { + log_printf(WARN, "Line %d: Recursive gesture, defaulting to no action.", action_config->getSourceLine()); + return new NoAction(); + } + std::map gestures; + + Setting* gestures_config; + + try { gestures_config = &(*action_config)["gestures"]; } + catch(SettingNotFoundException &e) + { + log_printf(WARN, "No gestures parameter for line %d, skipping", action_config->getSourceLine()); + throw e; + } + + for(int i = 0; i < gestures_config->getLength(); i++) + { + const Setting &gesture_config = (*gestures_config)[i]; + + std::string direction_str; + Direction direction; + try + { + gesture_config.lookupValue("direction", direction_str); + direction = string_to_direction(direction_str); + } + catch(SettingNotFoundException &e) + { + log_printf(WARN, "No direction set on line %d", gesture_config.getSourceLine()); + continue; + } + catch(std::invalid_argument &e) + { + log_printf(WARN, "Line %d: %s", gesture_config["direction"].getSourceLine(), e.what()); + continue; + } + + if(gestures.find(direction) != gestures.end()) + { + log_printf(WARN, "Entry on line %d is a duplicate, skipping...", gesture_config["direction"].getSourceLine()); + continue; + } + + GestureMode mode; + try + { + std::string mode_str; + gesture_config.lookupValue("mode", mode_str); + mode = string_to_gesturemode(mode_str); + } + catch (SettingNotFoundException &e) + { + log_printf(INFO, "Gesture mode on line %d not found, defaulting to OnRelease", gesture_config.getSourceLine()); + mode = GestureMode::OnRelease; + } + + if(mode == GestureMode::NoPress) + { + gestures.insert({direction, new Gesture(new NoAction(), mode)}); + continue; + } + + Setting* g_action; + try { g_action = &gesture_config["action"]; } + catch(SettingNotFoundException &e) + { + log_printf(WARN, "No action set for %s", direction_str.c_str()); + continue; + } + + Action g_type; + try + { + std::string type_str; + g_action->lookupValue("type", type_str); + g_type = string_to_action(type_str); + } + catch(SettingNotFoundException &e) + { + log_printf(INFO, "Missing an action type on line %d, skipping", g_action->getSourceLine()); + continue; + } + catch(std::invalid_argument &e) + { + log_printf(WARN, "Line %d: %s", (*g_action)["type"].getSourceLine(), e.what()); + continue; + } + + ButtonAction* ba; + + try { ba = parse_action(g_type, g_action, true); } + catch(std::exception &e) { continue; } + + if(mode == GestureMode::OnFewPixels) + { + try + { + int pp; + gesture_config.lookupValue("pixels", pp); + gestures.insert({direction, new Gesture(ba, mode, pp)}); + } + catch(SettingNotFoundException &e) + { + log_printf(WARN, "Line %d: OnFewPixels requires a 'pixels' field.", gesture_config.getSourceLine()); + } + } + else gestures.insert({direction, new Gesture(ba, mode)}); + } + + return new GestureAction(gestures); + } + else if(type == Action::ToggleSmartshift) return new SmartshiftAction(); + else if(type == Action::ToggleHiresScroll) return new HiresScrollAction(); + else if(type == Action::CycleDPI) + { + std::vector dpis; + try + { + const Setting &keys_config = (*action_config)["dpis"]; + for (int i = 0; i < keys_config.getLength(); i++) + { + dpis.push_back((int)keys_config[i]); + } + } + catch(SettingNotFoundException &e) + { + log_printf(ERROR, "Line %d: CycleDPI action is missing 'dpis' field, defaulting to NoAction.", action_config->getSourceLine()); + } + + return new CycleDPIAction(dpis); + } + else if(type == Action::ChangeDPI) + { + int inc; + try + { + action_config->lookupValue("inc", inc); + } + catch(SettingNotFoundException &e) + { + log_printf(ERROR, "Line %d: ChangeDPI action is missing an 'inc' field, defaulting to NoAction.",action_config->getSourceLine()); + return new NoAction(); + } + + return new ChangeDPIAction(inc); + } + + log_printf(ERROR, "This shouldn't have happened. Unhandled action type? Defaulting to NoAction"); + return new NoAction(); +} + +DeviceConfig::DeviceConfig() +{ + dpi = nullptr; + hiresscroll = nullptr; + smartshift = nullptr; + actions = {}; +} \ No newline at end of file diff --git a/src/logid/Configuration.h b/src/logid/Configuration.h new file mode 100644 index 0000000..5e1adc4 --- /dev/null +++ b/src/logid/Configuration.h @@ -0,0 +1,41 @@ +#ifndef MASTEROPTIONS_CONFIGURATION_H +#define MASTEROPTIONS_CONFIGURATION_H + +#include +#include + +struct smartshift_options +{ + bool* on = nullptr; + uint8_t* threshold = nullptr; +}; + +class DeviceConfig; +class ButtonAction; +enum class Action; + +class DeviceConfig +{ +public: + DeviceConfig(); + DeviceConfig(const libconfig::Setting& root); + const int* dpi = nullptr; + struct smartshift_options* smartshift = nullptr; + const bool* hiresscroll = nullptr; + std::map actions; +}; + +class Configuration +{ +public: + Configuration(const char* config_file); + std::map devices; +private: + libconfig::Config cfg; +}; + +ButtonAction* parse_action(Action action, const libconfig::Setting* action_config, bool is_gesture=false); + +extern Configuration* global_config; + +#endif //MASTEROPTIONS_CONFIGURATION_H diff --git a/src/logid/Device.cpp b/src/logid/Device.cpp new file mode 100644 index 0000000..a460672 --- /dev/null +++ b/src/logid/Device.cpp @@ -0,0 +1,292 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Device.h" +#include "Actions.h" +#include "Configuration.h" +#include "util.h" +#include "EvdevDevice.h" + +using namespace std::chrono_literals; + +Device::Device(std::string p, const HIDPP::DeviceIndex i) : path(p), index (i) +{ + DeviceRemoved = false; + dispatcher = new HIDPP::SimpleDispatcher(path.c_str()); + hidpp_dev = new HIDPP20::Device(dispatcher, index); + features = get_features(); + + if(global_config->devices.find(hidpp_dev->name()) == global_config->devices.end()) + { + log_printf(INFO, "Device %s not configured, using default.", hidpp_dev->name().c_str()); + config = new DeviceConfig(); + } + else config = global_config->devices.find(hidpp_dev->name())->second; +} + +void Device::configure(bool scanning) +{ + if(config->dpi != nullptr) + set_dpi(*config->dpi, scanning); + if(config->smartshift != nullptr) + set_smartshift(*config->smartshift, scanning); + if(config->hiresscroll != nullptr) + set_hiresscroll(*config->hiresscroll, scanning); + divert_buttons(); +} + +void Device::divert_buttons(bool scanning) +{ + HIDPP20::IReprogControlsV4 irc4(hidpp_dev); + for(auto it = config->actions.begin(); it != config->actions.end(); ++it) + { + uint8_t flags = 0; + flags |= HIDPP20::IReprogControlsV4::ChangeTemporaryDivert; + flags |= HIDPP20::IReprogControlsV4::TemporaryDiverted; + if(it->second->type == Action::Gestures) + { + flags |= HIDPP20::IReprogControlsV4::ChangeRawXYDivert; + flags |= HIDPP20::IReprogControlsV4::RawXYDiverted; + } + it->first; + irc4.setControlReporting(it->first, flags, it->first); + } +} + +void Device::set_smartshift(smartshift_options ops, bool scanning) +{ + std::vector parameters; + parameters.push_back(ops.on == nullptr ? 0 : (*ops.on)? 2 : 1); + if(ops.threshold != nullptr) + { + parameters.push_back(*ops.threshold); + parameters.push_back(*ops.threshold); + } + + if(features.find(0x2110) == features.end()) + { + log_printf(DEBUG, "Error toggling smart shift, feature is non-existent."); + return; + } + const uint8_t f_index = features.find(0x2110)->second; + + try { hidpp_dev->callFunction(f_index, 0x01, parameters); } + catch (HIDPP20::Error &e) + { + if(scanning) + throw e; + log_printf(ERROR, "Error setting smartshift options, code %d: %s\n", e.errorCode(), e.what()); + } +} + +void Device::set_hiresscroll(bool b, bool scanning) +{ + +} + +void Device::set_dpi(int dpi, bool scanning) +{ + HIDPP20::IAdjustableDPI iad(hidpp_dev); + + try { for(int i = 0; i < iad.getSensorCount(); i++) iad.setSensorDPI(i, dpi); } + catch (HIDPP20::Error &e) + { + if(scanning) + throw e; + log_printf(ERROR, "Error while setting DPI: %s", e.what()); + } +} + +void Device::start() +{ + configure(); + + auto *d = new HIDPP::SimpleDispatcher(path.c_str()); + listener = new SimpleListener(d, index); + listener->addEventHandler( std::make_unique(hidpp_dev, this) ); + auto listener_thread = std::thread { [=]() { listener->start(); } }; + listener_thread.detach(); + while(!DeviceRemoved) + { + std::mutex m; + std::condition_variable cv; + std::vector results; + + std::thread t([&cv, &results, dev=hidpp_dev, removed=&DeviceRemoved]() + { + try { results = dev->callFunction(0x00, 0x00); } + catch(HIDPP10::Error &e) { usleep(500000); } + catch(std::system_error &e) + { + cv.notify_one(); + if(*removed) printf("REMOVED!\n"); + *removed = true; + } + cv.notify_one(); + }); + t.detach(); + + std::unique_lock l(m); + if(cv.wait_for(l, 500ms) == std::cv_status::timeout) + { + while(!DeviceRemoved) + { + try + { + configure(true); + break; + } + catch(std::exception &e) {} // Retry infinitely on failure + } + } + usleep(200000); + } + + listener->stop(); + listener_thread.join(); +} + +void Device::ButtonHandler::handleEvent (const HIDPP::Report &event) +{ + switch (event.function()) + { + case HIDPP20::IReprogControlsV4::Event::DivertedButtonEvent: + { + new_states = HIDPP20::IReprogControlsV4::divertedButtonEvent(event); + if (states.empty()) + { + for (uint16_t i : new_states) + std::thread{[=]() { dev->press_button(i); }}.detach(); + states = new_states; + break; + } + std::vector::iterator it; + std::vector cids(states.size() + new_states.size()); + it = std::set_union(states.begin(), states.end(), new_states.begin(), new_states.end(), cids.begin()); + cids.resize(it - cids.begin()); + for (uint16_t i : cids) + { + if (std::find(new_states.begin(), new_states.end(), i) != new_states.end()) + { + if (std::find(states.begin(), states.end(), i) == states.end()) + std::thread{[=]() { dev->press_button(i); }}.detach(); + } else + std::thread{[=]() { dev->release_button(i); }}.detach(); + } + states = new_states; + break; + } + case HIDPP20::IReprogControlsV4::Event::DivertedRawXYEvent: + { + auto raw_xy = HIDPP20::IReprogControlsV4::divertedRawXYEvent(event); + + for(uint16_t i : states) + std::thread{[=]() { dev->move_diverted(i, raw_xy); }}.detach(); + break; + } + } +} + +void Device::EventListener::removeEventHandlers () +{ + for (const auto &p: iterators) + dispatcher->unregisterEventHandler(p.second); + handlers.clear(); + iterators.clear(); +} + +Device::EventListener::~EventListener() +{ + removeEventHandlers(); +} + +void Device::EventListener::addEventHandler(std::unique_ptr &&handler) +{ + uint8_t feature = handler->feature()->index(); + EventHandler *ptr = handler.get(); + handlers.emplace(feature, std::move(handler)); + dispatcher->registerEventHandler(index, feature, [ptr](const HIDPP::Report &report) + { + ptr->handleEvent(report); + return true; + }); +} + +void Device::SimpleListener::start() +{ + try { dispatcher->listen(); } + catch(std::system_error &e) { } +} + +void Device::SimpleListener::stop() +{ + dispatcher->stop(); +} + +bool Device::SimpleListener::event (EventHandler *handler, const HIDPP::Report &report) +{ + handler->handleEvent (report); + return true; +} + +void Device::stop() +{ + DeviceRemoved = true; +} + +void Device::press_button(uint16_t cid) +{ + if(config->actions.find(cid) == config->actions.end()) + { + log_printf(WARN, "0x%x was pressed but no action was found.", cid); + return; + } + config->actions.find(cid)->second->press(); +} + +void Device::release_button(uint16_t cid) +{ + if(config->actions.find(cid) == config->actions.end()) + { + log_printf(WARN, "0x%x was released but no action was found.", cid); + return; + } + config->actions.find(cid)->second->release(); +} + +void Device::move_diverted(uint16_t cid, HIDPP20::IReprogControlsV4::Move m) +{ + auto action = config->actions.find(cid)->second; + switch(action->type) + { + case Action::Gestures: + ((GestureAction*)action)->move(m); + break; + default: + break; + } +} + +std::map Device::get_features() +{ + std::map features; + + HIDPP20::IFeatureSet ifs (hidpp_dev); + + unsigned int feature_count = ifs.getCount(); + + for(unsigned int i = 1; i <= feature_count; i++) + features.insert({ifs.getFeatureID(i), i}); + + return features; +} \ No newline at end of file diff --git a/src/logid/Device.h b/src/logid/Device.h new file mode 100644 index 0000000..9014620 --- /dev/null +++ b/src/logid/Device.h @@ -0,0 +1,109 @@ +#ifndef LOGIOPS_DEVICE_H +#define LOGIOPS_DEVICE_H + +#include +#include +#include +#include + +#include "Actions.h" +#include "Configuration.h" + +class Device +{ +public: + Device(std::string p, const HIDPP::DeviceIndex i); + + void configure(bool scanning=false); + + void press_button(uint16_t cid); + void release_button(uint16_t cid); + void move_diverted(uint16_t cid, HIDPP20::IReprogControlsV4::Move move); + + void start(); + void stop(); + + std::map get_features(); + + std::map features; + + std::string path; + const HIDPP::DeviceIndex index; + HIDPP::Dispatcher* dispatcher; + HIDPP20::Device* hidpp_dev; + + class EventHandler + { + public: + virtual const HIDPP20::FeatureInterface *feature() const = 0; + virtual void handleEvent (const HIDPP::Report &event) = 0; + }; + class ButtonHandler : public EventHandler + { + public: + ButtonHandler (HIDPP20::Device *hidppdev, Device *d): + _irc4 (hidppdev), + dev (d), + states (0) {} + const HIDPP20::FeatureInterface *feature () const + { + return &_irc4; + } + void handleEvent (const HIDPP::Report &event); + protected: + HIDPP20::IReprogControlsV4 _irc4; + Device* dev; + std::vector states; + std::vector new_states; + }; + + class EventListener + { + HIDPP::Dispatcher *dispatcher; + HIDPP::DeviceIndex index; + std::map> handlers; + std::map iterators; + public: + EventListener (HIDPP::Dispatcher *dispatcher, HIDPP::DeviceIndex index): dispatcher (dispatcher), index (index) {} + + virtual void removeEventHandlers (); + virtual ~EventListener(); + virtual void addEventHandler (std::unique_ptr &&handler); + + virtual void start () = 0; + virtual void stop () = 0; + + protected: + virtual bool event (EventHandler* handler, const HIDPP::Report &report) = 0; + }; + class SimpleListener : public EventListener + { + HIDPP::SimpleDispatcher *dispatcher; + + public: + SimpleListener (HIDPP::SimpleDispatcher* dispatcher, HIDPP::DeviceIndex index): + EventListener (dispatcher, index), + dispatcher (dispatcher) + { + } + + virtual void start(); + virtual void stop(); + + protected: + virtual bool event (EventHandler* handler, const HIDPP::Report &report); + }; + +protected: + DeviceConfig* config; + bool DeviceRemoved; + EventListener* listener; + + void divert_buttons(bool scanning=false); + void set_smartshift(struct smartshift_options ops, bool scanning=false); + void set_hiresscroll(bool b, bool scanning=false); + void set_dpi(int dpi, bool scanning=false); +}; + + +#endif //LOGIOPS_DEVICE_H diff --git a/src/logid/DeviceFinder.cpp b/src/logid/DeviceFinder.cpp new file mode 100644 index 0000000..bea9cf6 --- /dev/null +++ b/src/logid/DeviceFinder.cpp @@ -0,0 +1,143 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DeviceFinder.h" +#include "util.h" +#include "Device.h" + +void find_device() +{ + auto df = new DeviceFinder(); + df->run(); +} + +void DeviceFinder::addDevice(const char *path) +{ + const int max_tries = 5; + const int try_delay = 50000; + std::string string_path(path); + // Asynchronously scan device + std::thread{[=]() + { + // Check /sys/class/hidraw/hidrawX/device/uevent for device details. + // Continue if HIDRAW_NAME contains 'Logitech' or is non-existent/ + // Otherwise, skip. + std::string hidraw_name; + std::size_t spos = string_path.rfind('/'); + if(spos != std::string::npos) hidraw_name = string_path.substr(spos+1, string_path.size()-1); + else + { + log_printf(ERROR, "Expected file but got directory: %s", path); + return; + } + std::ifstream info_file("/sys/class/hidraw/" + hidraw_name + "/device/uevent"); + std::map hidraw_info; + std::string line; + while(std::getline(info_file, line)) + { + if(line.find('=') == std::string::npos) continue; + hidraw_info.insert({line.substr(0, line.find('=')), line.substr(line.find('=') + 1, line.size()-1)}); + } + + if(hidraw_info.find("HID_NAME") != hidraw_info.end()) + if (hidraw_info.find("HID_NAME")->second.find("Logitech") == std::string::npos) return; + + //Check if device is an HID++ device and handle it accordingly + try + { + HIDPP::SimpleDispatcher dispatcher(string_path.c_str()); + bool has_receiver_index = false; + for(HIDPP::DeviceIndex index: { + HIDPP::DefaultDevice, HIDPP::CordedDevice, + HIDPP::WirelessDevice1, HIDPP::WirelessDevice2, + HIDPP::WirelessDevice3, HIDPP::WirelessDevice4, + HIDPP::WirelessDevice5, HIDPP::WirelessDevice6}) + { + /// TODO: CONTINUOUSLY SCAN ALL DEVICES ON RECEIVER + //Skip wireless devices if default device (receiver) has failed + if(!has_receiver_index && index == HIDPP::WirelessDevice1) + break; + for(int i = 0; i < max_tries; i++) + { + try + { + HIDPP::Device d(&dispatcher, index); + auto version = d.protocolVersion(); + if(index == HIDPP::DefaultDevice && version == std::make_tuple(1, 0)) + has_receiver_index = true; + uint major, minor; + std::tie(major, minor) = d.protocolVersion(); + if(major > 1) // HID++ 2.0 devices only + { + auto dev = new Device(string_path.c_str(), index); + handlers.insert({ + dev, std::async(std::launch::async, &Device::start, *dev) + }); + log_printf(INFO, "%s detected: device %d on %s", d.name().c_str(), index, string_path.c_str()); + } + break; + } + catch(HIDPP10::Error &e) + { + if(e.errorCode() != HIDPP10::Error::UnknownDevice) + { + if(i == max_tries - 1) + log_printf(WARN, "Error while querying %s, wireless device %d: %s", string_path.c_str(), index, e.what()); + else usleep(try_delay); + } + } + catch(HIDPP20::Error &e) + { + if(e.errorCode() != HIDPP20::Error::UnknownDevice) + { + if(i == max_tries - 1) + log_printf(WARN, "Error while querying %s, device %d: %s", string_path.c_str(), index, e.what()); + else usleep(try_delay); + } + } + catch(HIDPP::Dispatcher::TimeoutError &e) + { + if(i == max_tries - 1) + log_printf(WARN, "Device %s (index %d) timed out.", string_path.c_str(), index); + else usleep(try_delay); + } + catch(std::runtime_error &e) + { + if(i == max_tries - 1) + log_printf(WARN, "Runtime error on device %d on %s: %s", index, string_path.c_str(), e.what()); + else usleep(try_delay); + } + } + } + } + catch(HIDPP::Dispatcher::NoHIDPPReportException &e) {} + catch(std::system_error &e) { log_printf(WARN, "Failed to open %s: %s", string_path.c_str(), e.what()); } + }}.detach(); +} + +void DeviceFinder::removeDevice(const char* path) +{ + // Iterate through Devices, stop all in path + auto it = handlers.begin(); + while (it != handlers.end()) + { + if(it->first->path == path) + { + log_printf(INFO, "%s on %s disconnected.", it->first->hidpp_dev->name(), path); + it->first->stop(); + it->second.wait(); + //handlers.erase(it); + it++; + } + else + it++; + } +} \ No newline at end of file diff --git a/src/logid/DeviceFinder.h b/src/logid/DeviceFinder.h new file mode 100644 index 0000000..f5cd478 --- /dev/null +++ b/src/logid/DeviceFinder.h @@ -0,0 +1,18 @@ +#ifndef MASTEROPTIONS_DEVICEFINDER_H +#define MASTEROPTIONS_DEVICEFINDER_H + +#include "Device.h" + +struct handler_pair; + +class DeviceFinder : public HID::DeviceMonitor +{ +protected: + void addDevice(const char* path); + void removeDevice(const char* path); + std::map> handlers; +}; + +void find_device(); + +#endif //MASTEROPTIONS_DEVICEFINDER_H diff --git a/src/logid/EvdevDevice.cpp b/src/logid/EvdevDevice.cpp new file mode 100644 index 0000000..7abeaa9 --- /dev/null +++ b/src/logid/EvdevDevice.cpp @@ -0,0 +1,35 @@ +#include +#include +#include + +#include "EvdevDevice.h" + +EvdevDevice::EvdevDevice(const char* name) +{ + device = libevdev_new(); + libevdev_set_name(device, name); + + libevdev_enable_event_type(device, EV_KEY); + for(int i = 0; i < KEY_CNT; i++) + libevdev_enable_event_code(device, EV_KEY, i, nullptr); + libevdev_enable_event_type(device, EV_REL); + for(int i = 0; i < REL_CNT; i++) + libevdev_enable_event_code(device, EV_REL, i, nullptr); + + int err = libevdev_uinput_create_from_device(device, LIBEVDEV_UINPUT_OPEN_MANAGED, &ui_device); + + if(err != 0) + throw std::system_error(-err, std::generic_category()); +} + +void EvdevDevice::send_event(unsigned int type, unsigned int code, int value) +{ + libevdev_uinput_write_event(ui_device, type, code, value); + libevdev_uinput_write_event(ui_device, EV_SYN, SYN_REPORT, 0); +} + +EvdevDevice::~EvdevDevice() +{ + libevdev_uinput_destroy(ui_device); + libevdev_free(device); +} diff --git a/src/logid/EvdevDevice.h b/src/logid/EvdevDevice.h new file mode 100644 index 0000000..57ded21 --- /dev/null +++ b/src/logid/EvdevDevice.h @@ -0,0 +1,19 @@ +#ifndef MASTEROPTIONS_EVDEVDEVICE_H +#define MASTEROPTIONS_EVDEVDEVICE_H + +#include +#include + +class EvdevDevice +{ +public: + EvdevDevice(const char* name); + ~EvdevDevice(); + void send_event(unsigned int type, unsigned int code, int value); + libevdev* device; + libevdev_uinput* ui_device; +}; + +extern EvdevDevice* global_evdev; + +#endif //MASTEROPTIONS_EVDEVDEVICE_H diff --git a/src/logid/logid.cpp b/src/logid/logid.cpp new file mode 100644 index 0000000..a7ad129 --- /dev/null +++ b/src/logid/logid.cpp @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" +#include "Device.h" +#include "Actions.h" +#include "Configuration.h" +#include "EvdevDevice.h" +#include "DeviceFinder.h" + +#define evdev_name "logid" + +LogLevel global_verbosity = DEBUG; +Configuration* global_config; +EvdevDevice* global_evdev; + +int main(int argc, char** argv) +{ + // Read config + try { global_config = new Configuration("logid.cfg"); } + catch (std::exception &e) { return EXIT_FAILURE; } + + //Create an evdev device called 'logid' + try { global_evdev = new EvdevDevice(evdev_name); } + catch(std::system_error& e) + { + log_printf(ERROR, "Could not create evdev device: %s", e.what()); + return EXIT_FAILURE; + } + + find_device(); // Scan devices, create listeners, handlers, etc. + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/src/logid/util.cpp b/src/logid/util.cpp new file mode 100644 index 0000000..88876d0 --- /dev/null +++ b/src/logid/util.cpp @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include +#include + +#include "util.h" +#include "Actions.h" + +void log_printf(LogLevel level, const char* format, ...) +{ + if(global_verbosity > level) return; + + va_list vargs; + va_start(vargs, format); + + FILE* stream = stdout; + if(level == ERROR || level == WARN) stream = stderr; + + fprintf(stream, "[%s] ", level_prefix(level)); + vfprintf(stream, format, vargs); + fprintf(stream, "\n"); +} + +const char* level_prefix(LogLevel level) +{ + if(level == DEBUG) return "DEBUG"; + if(level == INFO) return "INFO" ; + if(level == WARN) return "WARN"; + if(level == ERROR) return "ERROR"; + + return "DEBUG"; +} + +Direction get_direction(int x, int y) +{ + if(abs(x) < 50 && abs(y) < 50) return Direction::None; + + double angle; + + if(x == 0 && y > 0) angle = 90; // Y+ + else if(x == 0 && y < 0) angle = 270; // Y- + else if(x > 0 && y == 0) angle = 0; // X+ + else if(x < 0 && y == 0) angle = 180; // X+ + else + { + angle = fabs(atan((double)y/(double)x) * 180.0 / M_PI); + + if(x < 0 && y > 0) angle = 180.0 - angle; //Q2 + else if(x < 0 && y < 0) angle += 180; // Q3 + else if(x > 0 && y < 0) angle = 360.0 - angle; // Q4 + } + + if(315 < angle || angle <= 45) return Direction::Right; + else if(45 < angle && angle <= 135) return Direction::Down; + else if(135 < angle && angle <= 225) return Direction::Left; + else if(225 < angle && angle <= 315) return Direction::Up; + + return Direction::None; +} + +Direction string_to_direction(std::string s) +{ + const char* original_str = s.c_str(); + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + + if(s == "none") return Direction::None; + if(s == "up") return Direction::Up; + if(s == "down") return Direction::Down; + if(s == "left") return Direction::Left; + if(s == "right") return Direction::Right; + + s = original_str; + + throw std::invalid_argument(s + " is an invalid direction."); +} + +GestureMode string_to_gesturemode(std::string s) +{ + const char* original_str = s.c_str(); + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + + if(s == "nopress") return GestureMode::NoPress; + if(s == "onrelease") return GestureMode::OnRelease; + if(s == "onfewpixels") return GestureMode::OnFewPixels; + + s = original_str; + + log_printf(INFO, "%s is an invalid gesture mode. Defaulting to OnRelease", original_str); + + return GestureMode::OnRelease; +} + +Action string_to_action(std::string s) +{ + std::string original_str = s; + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + + if(s == "none") return Action::None; + if(s == "keypress") return Action::Keypress; + if(s == "gestures") return Action::Gestures; + if(s == "togglesmartshift") return Action::ToggleSmartshift; + if(s == "togglehiresscroll") return Action::ToggleHiresScroll; + if(s == "cycledpi") return Action::CycleDPI; + if(s == "changedpi") return Action::ChangeDPI; + + throw std::invalid_argument(original_str + " is an invalid action."); +} \ No newline at end of file diff --git a/src/logid/util.h b/src/logid/util.h new file mode 100644 index 0000000..be6e491 --- /dev/null +++ b/src/logid/util.h @@ -0,0 +1,25 @@ +#ifndef MASTEROPTIONS_LOGGER_H +#define MASTEROPTIONS_LOGGER_H + +#include "Actions.h" + +enum LogLevel +{ + DEBUG, + INFO, + WARN, + ERROR +}; + +extern LogLevel global_verbosity; + +void log_printf(LogLevel level, const char* format, ...); + +const char* level_prefix(LogLevel level); + +Direction get_direction(int x, int y); +Direction string_to_direction(std::string s); +GestureMode string_to_gesturemode(std::string s); +Action string_to_action(std::string s); + +#endif //MASTEROPTIONS_LOGGER_H