From 0fbeb1e3c90b4e22cacedf81ab3725f774d6b7c2 Mon Sep 17 00:00:00 2001 From: pixl Date: Sat, 11 Jul 2020 16:31:08 -0400 Subject: [PATCH] Add Gesture support Only supports OnRelease right now. Also, some bugs were spotted while writing this: - Sometimes deadlocks on startup (cause unknown) - Sometimes valid CIDs will be unknown (bug may have been fixed?) --- src/logid/CMakeLists.txt | 3 + src/logid/Configuration.h | 2 +- src/logid/actions/Action.cpp | 3 + src/logid/actions/Action.h | 3 +- src/logid/actions/GestureAction.cpp | 269 ++++++++++++++++++ src/logid/actions/GestureAction.h | 67 +++++ src/logid/actions/gesture/Gesture.cpp | 109 +++++++ src/logid/actions/gesture/Gesture.h | 73 +++++ src/logid/actions/gesture/ReleaseGesture.cpp | 49 ++++ src/logid/actions/gesture/ReleaseGesture.h | 43 +++ .../hidpp20/features/ReprogControls.cpp | 24 +- .../backend/hidpp20/features/ReprogControls.h | 2 + src/logid/features/RemapButton.cpp | 2 +- 13 files changed, 636 insertions(+), 13 deletions(-) create mode 100644 src/logid/actions/GestureAction.cpp create mode 100644 src/logid/actions/GestureAction.h create mode 100644 src/logid/actions/gesture/Gesture.cpp create mode 100644 src/logid/actions/gesture/Gesture.h create mode 100644 src/logid/actions/gesture/ReleaseGesture.cpp create mode 100644 src/logid/actions/gesture/ReleaseGesture.h diff --git a/src/logid/CMakeLists.txt b/src/logid/CMakeLists.txt index 6d93d7b..c279ff8 100644 --- a/src/logid/CMakeLists.txt +++ b/src/logid/CMakeLists.txt @@ -24,6 +24,9 @@ add_executable(logid actions/KeypressAction.cpp actions/ToggleHiresScroll.cpp actions/ToggleSmartShift.cpp + actions/GestureAction.cpp + actions/gesture/Gesture.cpp + actions/gesture/ReleaseGesture.cpp backend/Error.cpp backend/raw/DeviceMonitor.cpp backend/raw/RawDevice.cpp diff --git a/src/logid/Configuration.h b/src/logid/Configuration.h index ed10893..b05da63 100644 --- a/src/logid/Configuration.h +++ b/src/logid/Configuration.h @@ -50,7 +50,7 @@ namespace logid int workerCount() const; private: std::map _device_paths; - std::chrono::milliseconds _io_timeout; + std::chrono::milliseconds _io_timeout = LOGID_DEFAULT_RAWDEVICE_TIMEOUT; int _worker_threads; libconfig::Config _config; }; diff --git a/src/logid/actions/Action.cpp b/src/logid/actions/Action.cpp index 6de115d..1a845ee 100644 --- a/src/logid/actions/Action.cpp +++ b/src/logid/actions/Action.cpp @@ -22,6 +22,7 @@ #include "KeypressAction.h" #include "ToggleSmartShift.h" #include "ToggleHiresScroll.h" +#include "GestureAction.h" using namespace logid; using namespace logid::actions; @@ -53,6 +54,8 @@ std::shared_ptr Action::makeAction(Device *device, libconfig::Setting return std::make_shared(device); else if(type == "togglehiresscroll") return std::make_shared(device); + else if(type == "gestures") + return std::make_shared(device, setting); else throw InvalidAction(type); diff --git a/src/logid/actions/Action.h b/src/logid/actions/Action.h index 14ff244..846abd7 100644 --- a/src/logid/actions/Action.h +++ b/src/logid/actions/Action.h @@ -65,11 +65,10 @@ namespace actions { class Config { - public: + protected: explicit Config(Device* device) : _device (device) { } - protected: Device* _device; }; protected: diff --git a/src/logid/actions/GestureAction.cpp b/src/logid/actions/GestureAction.cpp new file mode 100644 index 0000000..a9b80f4 --- /dev/null +++ b/src/logid/actions/GestureAction.cpp @@ -0,0 +1,269 @@ +/* + * Copyright 2019-2020 PixlOne + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#include +#include "GestureAction.h" +#include "../Device.h" +#include "../backend/hidpp20/features/ReprogControls.h" + +using namespace logid::actions; +using namespace logid; +using namespace logid::backend; + +GestureAction::Direction GestureAction::toDirection(std::string direction) +{ + std::transform(direction.begin(), direction.end(), direction.begin(), + ::tolower); + if(direction == "up") + return Up; + else if(direction == "down") + return Down; + else if(direction == "left") + return Left; + else if(direction == "right") + return Right; + else if(direction == "none") + return None; + else + throw std::invalid_argument("direction"); +} + +GestureAction::Direction GestureAction::toDirection(int16_t x, int16_t y) +{ + if(x >= 0 && y >= 0) + return x >= y ? Right : Down; + else if(x < 0 && y >= 0) + return -x <= y ? Down : Left; + else if(x <= 0 && y < 0) + return x <= y ? Left : Up; + else + return x <= -y ? Up : Right; +} + +GestureAction::GestureAction(Device* dev, libconfig::Setting& config) : + Action (dev), _config (dev, config) +{ +} + +void GestureAction::press() +{ + _pressed = true; + _x = 0, _y = 0; + for(auto& gesture : _config.gestures()) + gesture.second->press(); +} + +void GestureAction::release() +{ + _pressed = false; + bool threshold_met = false; + + auto d = toDirection(_x, _y); + auto primary_gesture = _config.gestures().find(d); + if(primary_gesture != _config.gestures().end()) { + threshold_met = primary_gesture->second->metThreshold(); + primary_gesture->second->release(true); + } + + for(auto& gesture : _config.gestures()) { + if(gesture.first == d) + continue; + if(!threshold_met) { + if(gesture.second->metThreshold()) { + // If the primary gesture did not meet its threshold, use the + // secondary one. + threshold_met = true; + gesture.second->release(true); + break; + } + } else { + gesture.second->release(false); + } + } + + if(!threshold_met) { + if(_config.noneAction()) { + _config.noneAction()->press(); + _config.noneAction()->release(); + } + } +} + +void GestureAction::move(int16_t x, int16_t y) +{ + auto new_x = _x + x, new_y = _y + y; + + if(abs(x) > 0) { + if(_x < 0 && new_x >= 0) { // Left -> Origin/Right + auto left = _config.gestures().find(Left); + if(left != _config.gestures().end()) + left->second->move(_x); + if(new_x) { // Ignore to origin + auto right = _config.gestures().find(Right); + if(right != _config.gestures().end()) + right->second->move(new_x); + } + } else if(_x > 0 && new_x <= 0) { // Right -> Origin/Left + auto right = _config.gestures().find(Right); + if(right != _config.gestures().end()) + right->second->move(-_x); + if(new_x) { // Ignore to origin + auto left = _config.gestures().find(Left); + if(left != _config.gestures().end()) + left->second->move(-new_x); + } + } else if(new_x < 0) { // Origin/Left to Left + auto left = _config.gestures().find(Left); + if(left != _config.gestures().end()) + left->second->move(-x); + } else if(new_x > 0) { // Origin/Right to Right + auto right = _config.gestures().find(Right); + if(right != _config.gestures().end()) + right->second->move(x); + } + } + + if(abs(y) > 0) { + if(_y > 0 && new_y <= 0) { // Up -> Origin/Down + auto up = _config.gestures().find(Up); + if(up != _config.gestures().end()) + up->second->move(_y); + if(new_y) { // Ignore to origin + auto down = _config.gestures().find(Down); + if(down != _config.gestures().end()) + down->second->move(new_y); + } + } else if(_y < 0 && new_y >= 0) { // Down -> Origin/Up + auto down = _config.gestures().find(Down); + if(down != _config.gestures().end()) + down->second->move(-_y); + if(new_y) { // Ignore to origin + auto up = _config.gestures().find(Up); + if(up != _config.gestures().end()) + up->second->move(-new_y); + } + } else if(new_y < 0) { // Origin/Up to Up + auto up = _config.gestures().find(Up); + if(up != _config.gestures().end()) + up->second->move(-y); + } else if(new_y > 0) {// Origin/Down to Down + auto down = _config.gestures().find(Down); + if(down != _config.gestures().end()) + down->second->move(y); + } + } + + _x = new_x; _y = new_y; +} + +uint8_t GestureAction::reprogFlags() const +{ + return (hidpp20::ReprogControls::TemporaryDiverted | + hidpp20::ReprogControls::RawXYDiverted); +} + +GestureAction::Config::Config(Device* device, libconfig::Setting &root) : + Action::Config(device) +{ + try { + auto& gestures = root.lookup("gestures"); + + if(!gestures.isList()) { + logPrintf(WARN, "Line %d: gestures must be a list, ignoring.", + gestures.getSourceLine()); + return; + } + + int gesture_count = gestures.getLength(); + + for(int i = 0; i < gesture_count; i++) { + if(!gestures[i].isGroup()) { + logPrintf(WARN, "Line %d: gesture must be a group, skipping.", + gestures[i].getSourceLine()); + continue; + } + + Direction d; + try { + auto& direction = gestures[i].lookup("direction"); + if(direction.getType() != libconfig::Setting::TypeString) { + logPrintf(WARN, "Line %d: direction must be a string, " + "skipping.", direction.getSourceLine()); + continue; + } + + try { + d = toDirection(direction); + } catch(std::invalid_argument& e) { + logPrintf(WARN, "Line %d: Invalid direction %s", + direction.getSourceLine(), (const char*)direction); + continue; + } + } catch(libconfig::SettingNotFoundException& e) { + logPrintf(WARN, "Line %d: direction is a required field, " + "skipping.", gestures[i].getSourceLine()); + continue; + } + + if(_gestures.find(d) != _gestures.end() || (d == None && _none_action)) { + logPrintf(WARN, "Line %d: Gesture is already defined for " + "this direction, duplicate ignored.", + gestures[i].getSourceLine()); + continue; + } + + if(d == None) { + try { + _none_action = Action::makeAction(_device, + gestures[i].lookup("action")); + } catch (InvalidAction& e) { + logPrintf(WARN, "Line %d: %s is not a valid action, " + "skipping.", gestures[i].lookup("action") + .getSourceLine(), e.what()); + } catch (libconfig::SettingNotFoundException& e) { + logPrintf(WARN, "Line %d: action is a required field, " + "skipping.", gestures[i].getSourceLine(), + e.what()); + } + continue; + } + + try { + _gestures.emplace(d, Gesture::makeGesture(_device, + gestures[i])); + } catch(InvalidGesture& e) { + logPrintf(WARN, "Line %d: Invalid gesture: %s", + gestures[i].getSourceLine(), e.what()); + } + } + + } catch(libconfig::SettingNotFoundException& e) { + logPrintf(WARN, "Line %d: gestures is a required field, ignoring.", + root.getSourceLine()); + } +} + +std::map>& + GestureAction::Config::gestures() +{ + return _gestures; +} + +std::shared_ptr GestureAction::Config::noneAction() +{ + return _none_action; +} \ No newline at end of file diff --git a/src/logid/actions/GestureAction.h b/src/logid/actions/GestureAction.h new file mode 100644 index 0000000..3928f75 --- /dev/null +++ b/src/logid/actions/GestureAction.h @@ -0,0 +1,67 @@ +/* + * Copyright 2019-2020 PixlOne + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#ifndef LOGID_ACTION_GESTUREACTION_H +#define LOGID_ACTION_GESTUREACTION_H + +#include +#include +#include "Action.h" +#include "gesture/Gesture.h" + +namespace logid { +namespace actions { + class GestureAction : public Action + { + public: + enum Direction + { + None, + Up, + Down, + Left, + Right + }; + static Direction toDirection(std::string direction); + static Direction toDirection(int16_t x, int16_t y); + + GestureAction(Device* dev, libconfig::Setting& config); + + virtual void press(); + virtual void release(); + virtual void move(int16_t x, int16_t y); + + virtual uint8_t reprogFlags() const; + + class Config : public Action::Config + { + public: + Config(Device* device, libconfig::Setting& root); + std::map>& gestures(); + std::shared_ptr noneAction(); + protected: + std::map> _gestures; + std::shared_ptr _none_action; + }; + + protected: + int16_t _x, _y; + Config _config; + }; +}} + +#endif //LOGID_ACTION_GESTUREACTION_H diff --git a/src/logid/actions/gesture/Gesture.cpp b/src/logid/actions/gesture/Gesture.cpp new file mode 100644 index 0000000..4691d4e --- /dev/null +++ b/src/logid/actions/gesture/Gesture.cpp @@ -0,0 +1,109 @@ +/* + * Copyright 2019-2020 PixlOne + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include +#include "Gesture.h" +#include "../../util/log.h" +#include "ReleaseGesture.h" +#include "../../backend/hidpp20/features/ReprogControls.h" + +using namespace logid::actions; + +Gesture::Gesture(Device *device) : _device (device) +{ +} + +Gesture::Config::Config(Device* device, libconfig::Setting& root, + bool action_required) : _device (device) +{ + if(action_required) { + try { + _action = Action::makeAction(_device, + root.lookup("action")); + } catch (libconfig::SettingNotFoundException &e) { + throw InvalidGesture("action is missing"); + } + + if(_action->reprogFlags() & backend::hidpp20::ReprogControls::RawXYDiverted) + throw InvalidGesture("gesture cannot require RawXY"); + } + + _threshold = LOGID_GESTURE_DEFAULT_THRESHOLD; + try { + auto& threshold = root.lookup("threshold"); + if(threshold.getType() == libconfig::Setting::TypeInt) { + _threshold = (int)threshold; + if(_threshold <= 0) { + _threshold = LOGID_GESTURE_DEFAULT_THRESHOLD; + logPrintf(WARN, "Line %d: threshold must be positive, setting" + "to default (%d)", threshold.getSourceLine(), + _threshold); + } + } else + logPrintf(WARN, "Line %d: threshold must be an integer, setting " + "to default (%d).", threshold.getSourceLine()); + } catch(libconfig::SettingNotFoundException& e) { + // Ignore + } +} + +std::shared_ptr Gesture::makeGesture(Device *device, + libconfig::Setting &setting) +{ + if(!setting.isGroup()) { + logPrintf(WARN, "Line %d: Gesture is not a group, ignoring.", + setting.getSourceLine()); + throw InvalidGesture(); + } + + try { + auto& gesture_mode = setting.lookup("mode"); + + if(gesture_mode.getType() != libconfig::Setting::TypeString) { + logPrintf(WARN, "Line %d: Gesture mode must be a string," + "defaulting to OnRelease.", + gesture_mode.getSourceLine()); + return std::make_shared(device, setting); + } + + std::string type = gesture_mode; + std::transform(type.begin(), type.end(), type.begin(), ::tolower); + + if(type == "onrelease") + return std::make_shared(device, setting); + else { + logPrintf(WARN, "Line %d: Unknown gesture mode %s, defaulting to " + "OnRelease.", gesture_mode.getSourceLine(), + (const char*)gesture_mode); + return std::make_shared(device, setting); + } + + } catch(libconfig::SettingNotFoundException& e) { + return std::make_shared(device, setting); + } +} + +int16_t Gesture::Config::threshold() const +{ + return _threshold; +} + +std::shared_ptr Gesture::Config::action() +{ + return _action; +} \ No newline at end of file diff --git a/src/logid/actions/gesture/Gesture.h b/src/logid/actions/gesture/Gesture.h new file mode 100644 index 0000000..731c50f --- /dev/null +++ b/src/logid/actions/gesture/Gesture.h @@ -0,0 +1,73 @@ +/* + * Copyright 2019-2020 PixlOne + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#ifndef LOGID_ACTION_GESTURE_H +#define LOGID_ACTION_GESTURE_H + +#include "../Action.h" + +#define LOGID_GESTURE_DEFAULT_THRESHOLD 50 + +namespace logid { +namespace actions +{ + class InvalidGesture : public std::exception + { + public: + explicit InvalidGesture(std::string what="") : _what (what) + { + } + virtual const char* what() + { + return _what.c_str(); + } + private: + std::string _what; + }; + + class Gesture + { + public: + virtual void press() = 0; + virtual void release(bool primary=false) = 0; + virtual void move(int16_t axis) = 0; + + virtual bool metThreshold() const = 0; + + class Config + { + public: + Config(Device* device, libconfig::Setting& root, + bool action_required=true); + virtual int16_t threshold() const; + virtual std::shared_ptr action(); + protected: + Device* _device; + std::shared_ptr _action; + int16_t _threshold; + }; + + static std::shared_ptr makeGesture(Device* device, + libconfig::Setting& setting); + + protected: + explicit Gesture(Device* device); + Device* _device; + }; +}} + +#endif //LOGID_ACTION_GESTURE_H diff --git a/src/logid/actions/gesture/ReleaseGesture.cpp b/src/logid/actions/gesture/ReleaseGesture.cpp new file mode 100644 index 0000000..1e14891 --- /dev/null +++ b/src/logid/actions/gesture/ReleaseGesture.cpp @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2020 PixlOne + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#include "ReleaseGesture.h" +#include "../../util/log.h" + +using namespace logid::actions; + +ReleaseGesture::ReleaseGesture(Device *device, libconfig::Setting &root) : + Gesture (device), _config (device, root) +{ +} + +void ReleaseGesture::press() +{ + _axis = 0; +} + +void ReleaseGesture::release(bool primary) +{ + if(metThreshold() && primary) { + _config.action()->press(); + _config.action()->release(); + } +} + +void ReleaseGesture::move(int16_t axis) +{ + _axis += axis; +} + +bool ReleaseGesture::metThreshold() const +{ + return _axis >= _config.threshold(); +} \ No newline at end of file diff --git a/src/logid/actions/gesture/ReleaseGesture.h b/src/logid/actions/gesture/ReleaseGesture.h new file mode 100644 index 0000000..b35fda2 --- /dev/null +++ b/src/logid/actions/gesture/ReleaseGesture.h @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 PixlOne + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#ifndef LOGID_ACTION_RELEASEGESTURE_H +#define LOGID_ACTION_RELEASEGESTURE_H + +#include "Gesture.h" + +namespace logid { +namespace actions +{ + class ReleaseGesture : public Gesture + { + public: + ReleaseGesture(Device* device, libconfig::Setting& root); + + virtual void press(); + virtual void release(bool primary=false); + virtual void move(int16_t axis); + + virtual bool metThreshold() const; + + protected: + int16_t _axis; + Gesture::Config _config; + }; +}} + +#endif //LOGID_ACTION_RELEASEGESTURE_H diff --git a/src/logid/backend/hidpp20/features/ReprogControls.cpp b/src/logid/backend/hidpp20/features/ReprogControls.cpp index 444096f..82af1d4 100644 --- a/src/logid/backend/hidpp20/features/ReprogControls.cpp +++ b/src/logid/backend/hidpp20/features/ReprogControls.cpp @@ -36,11 +36,11 @@ try { \ } // Define all of the ReprogControls versions -DEFINE_REPROG(ReprogControls, Feature); -DEFINE_REPROG(ReprogControlsV2, ReprogControls); -DEFINE_REPROG(ReprogControlsV2_2, ReprogControlsV2); -DEFINE_REPROG(ReprogControlsV3, ReprogControlsV2_2); -DEFINE_REPROG(ReprogControlsV4, ReprogControlsV3); +DEFINE_REPROG(ReprogControls, Feature) +DEFINE_REPROG(ReprogControlsV2, ReprogControls) +DEFINE_REPROG(ReprogControlsV2_2, ReprogControlsV2) +DEFINE_REPROG(ReprogControlsV3, ReprogControlsV2_2) +DEFINE_REPROG(ReprogControlsV4, ReprogControlsV3) std::shared_ptr ReprogControls::autoVersion(Device *dev) { @@ -81,10 +81,16 @@ ReprogControls::ControlInfo ReprogControls::getControlInfo(uint8_t index) ReprogControls::ControlInfo ReprogControls::getControlIdInfo(uint16_t cid) { - if(_cids.empty()) { - for(uint8_t i = 0; i < getControlCount(); i++) { - auto info = getControlInfo(i); - _cids.emplace(info.controlID, info); + if(!_cids_initialized) { + std::unique_lock lock(_cids_populating); + if(!_cids_initialized) { + uint8_t controls = getControlCount(); + for(uint8_t i = 0; i < controls; i++) { + auto info = getControlInfo(i); + _cids.emplace(info.controlID, info); + } + _cids_populating.unlock(); + _cids_initialized = true; } } diff --git a/src/logid/backend/hidpp20/features/ReprogControls.h b/src/logid/backend/hidpp20/features/ReprogControls.h index 143dafd..07567f8 100644 --- a/src/logid/backend/hidpp20/features/ReprogControls.h +++ b/src/logid/backend/hidpp20/features/ReprogControls.h @@ -110,6 +110,8 @@ namespace hidpp20 protected: ReprogControls(Device* dev, uint16_t _id); std::map _cids; + bool _cids_initialized = false; + std::mutex _cids_populating; }; class ReprogControlsV2 : public ReprogControls diff --git a/src/logid/features/RemapButton.cpp b/src/logid/features/RemapButton.cpp index b28d091..32fdd6e 100644 --- a/src/logid/features/RemapButton.cpp +++ b/src/logid/features/RemapButton.cpp @@ -56,7 +56,7 @@ void RemapButton::configure() } if((i.second->reprogFlags() & hidpp20::ReprogControls::RawXYDiverted) && - (!_reprog_controls->supportsRawXY() || (info.additionalFlags & + (!_reprog_controls->supportsRawXY() || !(info.additionalFlags & hidpp20::ReprogControls::RawXY))) logPrintf(WARN, "%s: Cannot divert raw XY movements for CID " "0x%02x", _device->name().c_str(), i.first);