diff --git a/src/logid/CMakeLists.txt b/src/logid/CMakeLists.txt index b2616c84..1b330790 100644 --- a/src/logid/CMakeLists.txt +++ b/src/logid/CMakeLists.txt @@ -21,6 +21,7 @@ add_executable(logid Configuration.cpp features/DPI.cpp features/SmartShift.cpp + features/HapticFeedback.cpp features/HiresScroll.cpp features/RemapButton.cpp features/DeviceStatus.cpp @@ -62,6 +63,7 @@ add_executable(logid backend/hidpp20/features/AdjustableDPI.cpp backend/hidpp20/features/SmartShift.cpp backend/hidpp20/features/ReprogControls.cpp + backend/hidpp20/features/HapticFeedback.cpp backend/hidpp20/features/HiresScroll.cpp backend/hidpp20/features/ChangeHost.cpp backend/hidpp20/features/WirelessDeviceStatus.cpp diff --git a/src/logid/Device.cpp b/src/logid/Device.cpp index 20b679d0..6b128c9f 100644 --- a/src/logid/Device.cpp +++ b/src/logid/Device.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -151,6 +152,7 @@ void Device::_init() { _addFeature("dpi"); _addFeature("smartshift"); + _addFeature("hapticfeedback"); _addFeature("hiresscroll"); _addFeature("remapbutton"); _addFeature("devicestatus"); diff --git a/src/logid/backend/hidpp20/feature_defs.h b/src/logid/backend/hidpp20/feature_defs.h index 3eebe42c..09f73930 100644 --- a/src/logid/backend/hidpp20/feature_defs.h +++ b/src/logid/backend/hidpp20/feature_defs.h @@ -57,6 +57,7 @@ namespace logid::backend::hidpp20 { BACKLIGHT = 0x1981, BACKLIGHT_V2 = 0x1982, BACKLIGHT_V3 = 0x1983, + HAPTIC_FEEDBACK = 0x19b0, PRESENTER_CONTROL = 0x1a00, SENSOR_3D = 0x1a01, REPROG_CONTROLS = 0x1b00, @@ -126,4 +127,4 @@ namespace logid::backend::hidpp20 { } -#endif //LOGID_BACKEND_HIDPP20_FEATUREDEFS \ No newline at end of file +#endif //LOGID_BACKEND_HIDPP20_FEATUREDEFS diff --git a/src/logid/backend/hidpp20/features/HapticFeedback.cpp b/src/logid/backend/hidpp20/features/HapticFeedback.cpp new file mode 100644 index 00000000..3e9a8132 --- /dev/null +++ b/src/logid/backend/hidpp20/features/HapticFeedback.cpp @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Kristóf Marussy + * + * 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 + +using namespace logid::backend::hidpp20; + +static const uint8_t kVibration = 0x01; +static const uint8_t kBatterySaving = 0x02; + +HapticFeedback::HapticFeedback(Device* dev) : Feature(dev, ID) { +} + +void HapticFeedback::setStrength(uint8_t strength, bool enabled, bool battery_saving) { + uint8_t flags = 0; + if (enabled) { + flags |= kVibration; + } + if (battery_saving) { + flags |= kBatterySaving; + } + std::vector params = {flags, strength}; + callFunction(SetStrength, params); +} + +void HapticFeedback::playEffect(uint8_t effect) { + std::vector params = {effect}; + callFunction(PlayEffect, params); +} diff --git a/src/logid/backend/hidpp20/features/HapticFeedback.h b/src/logid/backend/hidpp20/features/HapticFeedback.h new file mode 100644 index 00000000..cdadc6c4 --- /dev/null +++ b/src/logid/backend/hidpp20/features/HapticFeedback.h @@ -0,0 +1,44 @@ +/* + * Copyright 2025 Kristóf Marussy + * + * 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_BACKEND_HIDPP20_FEATURE_HAPTICFEEDBACK_H +#define LOGID_BACKEND_HIDPP20_FEATURE_HAPTICFEEDBACK_H + +#include +#include + +namespace logid::backend::hidpp20 { + class HapticFeedback : public Feature { + public: + static const uint16_t ID = FeatureID::HAPTIC_FEEDBACK; + + [[nodiscard]] uint16_t getID() final { return ID; } + + enum Function { + SetStrength = 2, + PlayEffect = 4 + }; + + explicit HapticFeedback(Device* dev); + + void setStrength(uint8_t strength, bool enabled = true, bool battery_saving = false); + + void playEffect(uint8_t effect); + }; +} + +#endif //LOGID_BACKEND_HIDPP20_FEATURE_HAPTICFEEDBACK_H diff --git a/src/logid/config/schema.h b/src/logid/config/schema.h index 6f16a8ab..8d25733c 100644 --- a/src/logid/config/schema.h +++ b/src/logid/config/schema.h @@ -292,18 +292,29 @@ namespace logid::config { typedef map> RemapButton; + struct HapticFeedback : public group { + std::optional enabled; + std::optional strength; + std::optional battery_saving; + + HapticFeedback() : group({"enabled", "strength", "battery_saving"}, + &HapticFeedback::enabled, &HapticFeedback::strength, + &HapticFeedback::battery_saving) {} + }; + struct Profile : public group { std::optional dpi; std::optional smartshift; std::optional> hiresscroll; std::optional thumbwheel; std::optional buttons; + std::optional haptic_feedback; Profile() : group({"dpi", "smartshift", "hiresscroll", - "buttons", "thumbwheel"}, + "buttons", "thumbwheel", "haptic_feedback"}, &Profile::dpi, &Profile::smartshift, &Profile::hiresscroll, &Profile::buttons, - &Profile::thumbwheel) {} + &Profile::thumbwheel, &Profile::haptic_feedback) {} }; struct Device : public group { diff --git a/src/logid/features/HapticFeedback.cpp b/src/logid/features/HapticFeedback.cpp new file mode 100644 index 00000000..b87c09d2 --- /dev/null +++ b/src/logid/features/HapticFeedback.cpp @@ -0,0 +1,161 @@ +/* + * Copyright 2025 Kristóf Marussy + * + * 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 "config/schema.h" +#include "features/DeviceFeature.h" +#include "ipcgull/interface.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace logid::features; +using namespace logid::backend; + +static const bool kDefaultEnabled = true; +static const uint8_t kDefaultStrength = 60; +static const int kMinStrength = 1; +static const int kMaxStrength = 100; +static const bool kDefaultBatterySaving = false; +static const uint8_t kMaxEffect = 14; + +static uint8_t clampStrength(int strength) { + return std::max(kMinStrength, std::min(strength, kMaxStrength)); +} + +HapticFeedback::HapticFeedback(Device* device) : DeviceFeature(device), _config(device->activeProfile().haptic_feedback) { + try { + _haptic_feedback = std::make_shared(&device->hidpp20()); + } catch (hidpp20::UnsupportedFeature& e) { + throw UnsupportedFeature(); + } + + _ipc_interface = _device->ipcNode()->make_interface(this); +} + +void HapticFeedback::configure() { + std::shared_lock lock(_config_mutex); + + bool enabled = kDefaultEnabled; + uint8_t strength = kDefaultStrength; + int battery_saving = kDefaultBatterySaving; + if (_config.get().has_value()) { + const auto& config = _config.get().value(); + if (config.enabled.has_value()) { + enabled = config.enabled.value(); + } + if (config.strength.has_value()) { + strength = clampStrength(config.strength.value()); + } + if (config.battery_saving.has_value()) { + battery_saving = config.battery_saving.value(); + } + } + setStrength(strength, enabled, battery_saving); +} + +void HapticFeedback::listen() { +} + +void HapticFeedback::setProfile(config::Profile& profile) { + std::unique_lock lock(_config_mutex); + _config = profile.haptic_feedback; +} + +void HapticFeedback::setStrength(uint8_t strength, bool enabled, bool battery_saving) { + _haptic_feedback->setStrength(clampStrength(strength), enabled, battery_saving); +} + +void HapticFeedback::playEffect(uint8_t effect) { + if (_isEnabled() && effect <= kMaxEffect) { + _haptic_feedback->playEffect(effect); + } +} + +bool HapticFeedback::_isEnabled() const { + std::shared_lock lock(_config_mutex); + if (!_config.get().has_value()) { + return kDefaultEnabled; + } + return _config.get().value().enabled.value_or(kDefaultEnabled); +} + +HapticFeedback::IPC::IPC(HapticFeedback* parent) : ipcgull::interface( + SERVICE_ROOT_NAME ".HapticFeedback", { + {"GetEnabled", {this, &IPC::getEnabled, {"enabled"}}}, + {"SetEnabled", {this, &IPC::setEnabled, {"enabled"}}}, + {"GetStrength", {this, &IPC::getStrength, {"strength"}}}, + {"SetStrength", {this, &IPC::setStrength, {"strength"}}}, + {"GetBatterySaving", {this, &IPC::getBatterySaving, {"battery_saving"}}}, + {"SetBatterySaving", {this, &IPC::setBatterySaving, {"battery_saving"}}}, + {"PlayEffect", {this, &IPC::playEffect, {"effect"}}} + }, {}, {}), _parent(*parent) { +} + +bool HapticFeedback::IPC::getEnabled() const { + return _parent._isEnabled(); +} + +void HapticFeedback::IPC::setEnabled(bool enabled) { + std::shared_lock lock(_parent._config_mutex); + if (!_parent._config.get().has_value()) { + _parent._config.get().emplace(); + } + _parent._config.get().value().enabled.emplace(enabled); + _parent.configure(); +} + +uint8_t HapticFeedback::IPC::getStrength() const { + std::unique_lock lock(_parent._config_mutex); + if (!_parent._config.get().has_value()) { + return kDefaultStrength; + } + return _parent._config.get().value().strength.value_or(kDefaultStrength); +} + +void HapticFeedback::IPC::setStrength(uint8_t strength) { + std::shared_lock lock(_parent._config_mutex); + if (!_parent._config.get().has_value()) { + _parent._config.get().emplace(); + } + _parent._config.get().value().strength.emplace(strength); + _parent.configure(); +} + +bool HapticFeedback::IPC::getBatterySaving() const { + std::unique_lock lock(_parent._config_mutex); + if (!_parent._config.get().has_value()) { + return kDefaultBatterySaving; + } + return _parent._config.get().value().battery_saving.value_or(kDefaultBatterySaving); +} + +void HapticFeedback::IPC::setBatterySaving(bool battery_saving) { + std::shared_lock lock(_parent._config_mutex); + if (!_parent._config.get().has_value()) { + _parent._config.get().emplace(); + } + _parent._config.get().value().battery_saving.emplace(battery_saving); + _parent.configure(); +} + +void HapticFeedback::IPC::playEffect(uint8_t effect) { + _parent.playEffect(effect); +} diff --git a/src/logid/features/HapticFeedback.h b/src/logid/features/HapticFeedback.h new file mode 100644 index 00000000..c2a87fe5 --- /dev/null +++ b/src/logid/features/HapticFeedback.h @@ -0,0 +1,76 @@ +/* + * Copyright 2025 Kristóf Marussy + * + * 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_FEATURE_HAPTICFEEDBACK_H +#define LOGID_FEATURE_HAPTICFEEDBACK_H + +#include +#include +#include +#include +#include + +namespace logid::features { + class HapticFeedback : public DeviceFeature { + public: + void configure() final; + + void listen() final; + + void setProfile(config::Profile& profile) final; + + void setStrength(uint8_t strength, bool enabled = true, bool battery_saving = false); + + void playEffect(uint8_t effect); + + protected: + explicit HapticFeedback(Device* dev); + + private: + class IPC : public ipcgull::interface { + public: + explicit IPC(HapticFeedback* parent); + + [[nodiscard]] bool getEnabled() const; + + void setEnabled(bool enabled); + + [[nodiscard]] uint8_t getStrength() const; + + void setStrength(uint8_t strength); + + [[nodiscard]] bool getBatterySaving() const; + + void setBatterySaving(bool strength); + + void playEffect(uint8_t effect); + + private: + HapticFeedback& _parent; + }; + + bool _isEnabled() const; + + mutable std::shared_mutex _config_mutex; + std::reference_wrapper> _config; + std::shared_ptr _haptic_feedback; + + std::shared_ptr _ipc_interface; + }; +} + +#endif //LOGID_FEATURE_HAPTICFEEDBACK_H