diff --git a/Source/Core/Core/HW/Triforce/FZeroAX.cpp b/Source/Core/Core/HW/Triforce/FZeroAX.cpp index 0431d50a8c..e588450462 100644 --- a/Source/Core/Core/HW/Triforce/FZeroAX.cpp +++ b/Source/Core/Core/HW/Triforce/FZeroAX.cpp @@ -187,6 +187,28 @@ void FZeroAXDeluxe_IOAdapter::DoState(PointerWrap& p) p.Do(m_rx_reply); } +FZeroAXSteeringWheel::FZeroAXSteeringWheel() + : m_config_changed_callback_id{CPUThreadConfigCallback::AddConfigChangedCallback( + [this] { HandleConfigChange(); })}, + // TODO: Is this safe ? + m_devices_changed_hook{ + g_controller_interface.RegisterDevicesChangedCallback([this] { HandleConfigChange(); })} +{ +} + +FZeroAXSteeringWheel::~FZeroAXSteeringWheel() +{ + CPUThreadConfigCallback::RemoveConfigChangedCallback(m_config_changed_callback_id); +} + +void FZeroAXSteeringWheel::HandleConfigChange() +{ + auto* const controller = Pad::GetConfig()->GetController(0); + const auto wheel_device = g_controller_interface.FindDevice(controller->GetDefaultDevice()); + + m_spring_effect = wheel_device->CreateSpringEffect(); +} + void FZeroAXSteeringWheel::Update() { constexpr std::size_t REQUEST_SIZE = 4; @@ -242,19 +264,17 @@ void FZeroAXSteeringWheel::ProcessRequest(std::span request) INFO_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: servo_position: {}", m_servo_position); - constexpr auto force_strength = 1.0; + if (m_spring_effect != nullptr) + { + constexpr auto force_strength = 1.0; - // TODO: Is this a sensible calculation ? - const auto center_position = ControllerEmu::MapToFloat(s8(m_servo_position * 2), {}); + // TODO: Is this a sensible calculation ? + const auto center_position = ControllerEmu::MapToFloat(s8(m_servo_position * 2), {}); - // TODO: Acquire this object in the constructor and update it on config change ! - // TODO: Also turn off the force appropriately on shutdown ! - auto* const controller = Pad::GetConfig()->GetController(0); - const auto wheel_device = g_controller_interface.FindDevice(controller->GetDefaultDevice()); + // TODO: Should we also set a friction force ? - // TODO: Should we also set a friction force ? - - wheel_device->SetCenteringForce(force_strength, center_position); + m_spring_effect->SetForce(force_strength, center_position); + } break; } diff --git a/Source/Core/Core/HW/Triforce/FZeroAX.h b/Source/Core/Core/HW/Triforce/FZeroAX.h index 18688336e2..8d1f36f8d7 100644 --- a/Source/Core/Core/HW/Triforce/FZeroAX.h +++ b/Source/Core/Core/HW/Triforce/FZeroAX.h @@ -3,9 +3,16 @@ #pragma once +#include "Common/HookableEvent.h" +#include "Core/CPUThreadConfigCallback.h" #include "Core/HW/Triforce/IOPorts.h" #include "Core/HW/Triforce/SerialDevice.h" +namespace ciface::Core +{ +class SpringEffect; +} + namespace Triforce { @@ -13,6 +20,9 @@ namespace Triforce class FZeroAXSteeringWheel final : public SerialDevice { public: + FZeroAXSteeringWheel(); + ~FZeroAXSteeringWheel() override; + void Update() override; bool IsInitializing() const; @@ -22,11 +32,18 @@ public: void DoState(PointerWrap&) override; private: + void HandleConfigChange(); + void ProcessRequest(std::span); u8 m_init_state = 0; s8 m_servo_position = 0; + + std::unique_ptr m_spring_effect; + + const CPUThreadConfigCallback::ConfigChangedCallbackID m_config_changed_callback_id; + const Common::EventHook m_devices_changed_hook; }; // Used for both FZeroAX and FZeroAXMonster. diff --git a/Source/Core/Core/HW/Triforce/MarioKartGP.cpp b/Source/Core/Core/HW/Triforce/MarioKartGP.cpp index 174eca91ad..80d1d6c98e 100644 --- a/Source/Core/Core/HW/Triforce/MarioKartGP.cpp +++ b/Source/Core/Core/HW/Triforce/MarioKartGP.cpp @@ -80,6 +80,28 @@ void MarioKartGPCommon_IOAdapter::HandleGenericOutputsChanged(std::spanGetController(0); + const auto wheel_device = g_controller_interface.FindDevice(controller->GetDefaultDevice()); + + m_spring_effect = wheel_device->CreateSpringEffect(); + m_friction_effect = wheel_device->CreateFrictionEffect(); +} + void MarioKartGPSteeringWheel::Update() { constexpr std::size_t REQUEST_SIZE = 10; @@ -123,18 +145,23 @@ void MarioKartGPSteeringWheel::ProcessRequest(std::span request) const bool should_set_force = centering_force != 0; if (should_set_force) { - // TODO: Are these sensible calculations ? - const auto force_strength = ControllerEmu::MapToFloat(s16(centering_force), {}); - const auto center_position = ControllerEmu::MapToFloat(s16(roll), {}); + if (m_spring_effect != nullptr) + { + // TODO: Are these sensible calculations ? + const auto force_strength = ControllerEmu::MapToFloat(s16(centering_force), {}); + const auto center_position = 0.0 - ControllerEmu::MapToFloat(s16(roll), {}); - // TODO: Acquire this object in the constructor and update it on config change ! - // TODO: Also turn off the force appropriately on shutdown ! - auto* const controller = Pad::GetConfig()->GetController(0); - const auto wheel_device = g_controller_interface.FindDevice(controller->GetDefaultDevice()); + m_spring_effect->SetForce(force_strength, center_position); + } - wheel_device->SetCenteringForce(force_strength, center_position); + if (m_friction_effect != nullptr) + { + // TODO: sensible calculation ? + const auto force_strength = ControllerEmu::MapToFloat(s16(friction_force), {}); + + m_friction_effect->SetForce(force_strength); + } } - switch (m_init_state) { case 0: diff --git a/Source/Core/Core/HW/Triforce/MarioKartGP.h b/Source/Core/Core/HW/Triforce/MarioKartGP.h index dc72a3b769..92e0c03fae 100644 --- a/Source/Core/Core/HW/Triforce/MarioKartGP.h +++ b/Source/Core/Core/HW/Triforce/MarioKartGP.h @@ -3,13 +3,16 @@ #pragma once +#include "Common/HookableEvent.h" +#include "Core/CPUThreadConfigCallback.h" #include "Core/HW/Triforce/IOPorts.h" #include "Core/HW/Triforce/SerialDevice.h" namespace ciface::Core { -class Device; -} +class SpringEffect; +class FrictionEffect; +} // namespace ciface::Core namespace Triforce { @@ -18,6 +21,9 @@ namespace Triforce class MarioKartGPSteeringWheel final : public SerialDevice { public: + MarioKartGPSteeringWheel(); + ~MarioKartGPSteeringWheel() override; + void Update() override; void Reset(); @@ -25,9 +31,17 @@ public: void DoState(PointerWrap&) override; private: + void HandleConfigChange(); + void ProcessRequest(std::span); u8 m_init_state = 0; + + std::unique_ptr m_spring_effect; + std::unique_ptr m_friction_effect; + + const CPUThreadConfigCallback::ConfigChangedCallbackID m_config_changed_callback_id; + const Common::EventHook m_devices_changed_hook; }; // Used for both MarioKartGP and MarioKartGP2. diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp index d2d284af80..5c2c370189 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp @@ -121,8 +121,14 @@ Device::Output* Device::FindOutput(std::string_view name) const return nullptr; } -void Device::SetCenteringForce(double gain, double center_position) +std::unique_ptr Device::CreateSpringEffect() { + return nullptr; +} + +std::unique_ptr Device::CreateFrictionEffect() +{ + return nullptr; } bool Device::Control::IsMatchingName(std::string_view name) const @@ -508,4 +514,10 @@ auto InputDetector::TakeResults() -> Results return std::move(m_detections); } +SpringEffect::SpringEffect() = default; +SpringEffect::~SpringEffect() = default; + +FrictionEffect::FrictionEffect() = default; +FrictionEffect::~FrictionEffect() = default; + } // namespace ciface::Core diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h index 52b10ab246..2741bc0f8b 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h @@ -44,6 +44,35 @@ enum class DeviceRemoval Keep, }; +// A "spring" force feedback effect used for wheel centering. +class SpringEffect +{ +public: + SpringEffect(); + virtual ~SpringEffect(); + + SpringEffect(const SpringEffect&) = delete; + SpringEffect(SpringEffect&&) = delete; + SpringEffect& operator=(const SpringEffect&) = delete; + SpringEffect& operator=(SpringEffect&&) = delete; + + virtual void SetForce(double gain, double center_position) = 0; +}; + +class FrictionEffect +{ +public: + FrictionEffect(); + virtual ~FrictionEffect(); + + FrictionEffect(const FrictionEffect&) = delete; + FrictionEffect(FrictionEffect&&) = delete; + FrictionEffect& operator=(const FrictionEffect&) = delete; + FrictionEffect& operator=(FrictionEffect&&) = delete; + + virtual void SetForce(double gain) = 0; +}; + class Device { public: @@ -160,7 +189,8 @@ public: Input* FindInput(std::string_view name) const; Output* FindOutput(std::string_view name) const; - virtual void SetCenteringForce(double gain, double center_position); + virtual std::unique_ptr CreateSpringEffect(); + virtual std::unique_ptr CreateFrictionEffect(); protected: void AddInput(Input* const i); diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp index 0da36e619d..cabe154688 100644 --- a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp +++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp @@ -221,9 +221,6 @@ Gamepad::Gamepad(SDL_Gamepad* const gamepad, SDL_Joystick* const joystick) AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Strong)); AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Weak)); } - - // TODO: Remove test hacks - // SetCenteringForce(0.8, 0.7); } } @@ -276,15 +273,27 @@ SDL_JoystickID Gamepad::GetSDLInstanceID() const return SDL_GetJoystickID(m_joystick); } -void Gamepad::SetCenteringForce(double gain, double center_position) +class SpringEffect final : public Core::SpringEffect { - INFO_LOG_FMT(CONTROLLERINTERFACE, "SetCenteringForce: {:.2} {:.2}", gain, center_position); +public: + explicit SpringEffect(SDL_Haptic* haptic) : m_haptic{haptic} {} - if (m_haptic == nullptr) - return; + ~SpringEffect() override { SetForce(0.0, 0.0); } - if (gain != 0.0) + void SetForce(double gain, double center_position) override { + INFO_LOG_FMT(CONTROLLERINTERFACE, "SpringEffect: {:.2} {:.2}", gain, center_position); + + if (gain == 0.0) + { + if (m_effect_id != -1) + { + SDL_DestroyHapticEffect(m_haptic, std::exchange(m_effect_id, -1)); + } + + return; + } + SDL_HapticEffect effect{ .type = SDL_HAPTIC_SPRING, }; @@ -293,9 +302,8 @@ void Gamepad::SetCenteringForce(double gain, double center_position) // TODO: Why does only SDL_HAPTIC_POLAR do the correct thing for my Sidewinder joystick? // I would expect SDL_HAPTIC_STEERING_AXIS to be more reliable.. - condition.direction.type = SDL_HAPTIC_POLAR; - // condition.direction.type = SDL_HAPTIC_STEERING_AXIS; - // condition.direction.dir[0] = 1; + // condition.direction.type = SDL_HAPTIC_POLAR; + condition.direction.type = SDL_HAPTIC_STEERING_AXIS; // Is "infinity" always supported ? condition.length = SDL_HAPTIC_INFINITY; @@ -311,30 +319,116 @@ void Gamepad::SetCenteringForce(double gain, double center_position) condition.center[0] = ControllerEmu::MapFloat(center_position, 0); - if (m_centering_haptic_effect == -1) + if (m_effect_id == -1) { // Create and start a new effect. - m_centering_haptic_effect = SDL_CreateHapticEffect(m_haptic, &effect); - if (m_centering_haptic_effect == -1) + m_effect_id = SDL_CreateHapticEffect(m_haptic, &effect); + if (m_effect_id == -1) { ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_CreateHapticEffect: {}", SDL_GetError()); return; } - if (!SDL_RunHapticEffect(m_haptic, m_centering_haptic_effect, 1)) + if (!SDL_RunHapticEffect(m_haptic, m_effect_id, 1)) ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_RunHapticEffect: {}", SDL_GetError()); } else { // Update an already running effect. - if (!SDL_UpdateHapticEffect(m_haptic, m_centering_haptic_effect, &effect)) + if (!SDL_UpdateHapticEffect(m_haptic, m_effect_id, &effect)) ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_UpdateHapticEffect: {}", SDL_GetError()); } } - else if (m_centering_haptic_effect != -1) + +private: + SDL_Haptic* const m_haptic; + int m_effect_id = -1; +}; + +class FrictionEffect final : public Core::FrictionEffect +{ +public: + explicit FrictionEffect(SDL_Haptic* haptic) : m_haptic{haptic} {} + + ~FrictionEffect() override { SetForce(0.0); } + + void SetForce(double gain) override { - SDL_DestroyHapticEffect(m_haptic, std::exchange(m_centering_haptic_effect, -1)); + INFO_LOG_FMT(CONTROLLERINTERFACE, "FrictionEffect: {:.2}", gain); + + if (gain == 0.0) + { + if (m_effect_id != -1) + { + SDL_DestroyHapticEffect(m_haptic, std::exchange(m_effect_id, -1)); + } + + return; + } + + SDL_HapticEffect effect{ + .type = SDL_HAPTIC_FRICTION, + }; + + auto& condition = effect.condition; + + // TODO: Why does only SDL_HAPTIC_POLAR do the correct thing for my Sidewinder joystick? + // I would expect SDL_HAPTIC_STEERING_AXIS to be more reliable.. + // condition.direction.type = SDL_HAPTIC_POLAR; + condition.direction.type = SDL_HAPTIC_STEERING_AXIS; + + // Is "infinity" always supported ? + condition.length = SDL_HAPTIC_INFINITY; + + const auto unsigned_gain = ControllerEmu::MapFloat(gain, 0); + condition.right_sat[0] = unsigned_gain; + condition.left_sat[0] = unsigned_gain; + + // TODO: Is this a sensible coeff value ? + constexpr auto coeff = std::numeric_limits::max(); + condition.right_coeff[0] = coeff; + condition.left_coeff[0] = coeff; + + if (m_effect_id == -1) + { + // Create and start a new effect. + m_effect_id = SDL_CreateHapticEffect(m_haptic, &effect); + if (m_effect_id == -1) + { + ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_CreateHapticEffect: {}", SDL_GetError()); + return; + } + + if (!SDL_RunHapticEffect(m_haptic, m_effect_id, 1)) + ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_RunHapticEffect: {}", SDL_GetError()); + } + else + { + // Update an already running effect. + if (!SDL_UpdateHapticEffect(m_haptic, m_effect_id, &effect)) + ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_UpdateHapticEffect: {}", SDL_GetError()); + } } + +private: + SDL_Haptic* const m_haptic; + int m_effect_id = -1; +}; + +std::unique_ptr Gamepad::CreateSpringEffect() +{ + if (m_haptic == nullptr) + return nullptr; + + return std::make_unique(m_haptic); +} + +std::unique_ptr Gamepad::CreateFrictionEffect() +{ + if (m_haptic == nullptr) + return nullptr; + + return std::make_unique(m_haptic); } std::string Gamepad::Button::GetName() const diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h index 34090f4a60..ccfa781591 100644 --- a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h +++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h @@ -342,7 +342,8 @@ public: return Core::DeviceRemoval::Keep; } - void SetCenteringForce(double gain, double center_position) override; + std::unique_ptr CreateSpringEffect() override; + std::unique_ptr CreateFrictionEffect() override; private: void UpdateRumble() @@ -371,8 +372,6 @@ private: float m_touchpad_x = 0.f; float m_touchpad_y = 0.f; float m_touchpad_pressure = 0.f; - - int m_centering_haptic_effect = -1; }; struct SDLMotionAxis diff --git a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp index 87482f4e0f..f3347645c5 100644 --- a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp +++ b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp @@ -589,7 +589,7 @@ bool evdevDevice::AddNode(std::string devnode, int fd, libevdev* dev) static_cast(!write(fd, &ie, sizeof(ie))); - m_ffb_wheel_fd = fd; + m_ffb_fd = fd; } // Constant FF effect @@ -729,18 +729,36 @@ bool evdevDevice::IsValid() const return true; } -void evdevDevice::SetCenteringForce(double gain, double center_position) +class SpringEffect final : public Core::SpringEffect { - INFO_LOG_FMT(CONTROLLERINTERFACE, "SetCenteringForce: {:.2} {:.2}", gain, center_position); +public: + explicit SpringEffect(int ffb_fd) : m_ffb_fd{ffb_fd} {} - if (m_ffb_wheel_fd == -1) - return; + ~SpringEffect() override { SetForce(0.0, 0.0); } - if (gain != 0.0) + void SetForce(double gain, double center_position) override { + INFO_LOG_FMT(CONTROLLERINTERFACE, "SpringEffect: {:.2} {:.2}", gain, center_position); + + if (gain == 0.0) + { + if (m_effect_id != -1) + { + if (ioctl(m_ffb_fd, EVIOCRMFF, m_effect_id) < 0) + ERROR_LOG_FMT(COMMON, "EVIOCRMFF: {}", Common::LastStrerrorString()); + + // TODO: Is it correct to not remove this effect on destruction. + // Is it enough to close the parent fd ? + + m_effect_id = -1; + } + + return; + } + ff_effect effect{ .type = FF_SPRING, - .id = m_centering_effect_id, + .id = m_effect_id, }; // @@ -760,36 +778,119 @@ void evdevDevice::SetCenteringForce(double gain, double center_position) condition[0].center = ControllerEmu::MapFloat(center_position, 0); - if (ioctl(m_ffb_wheel_fd, EVIOCSFF, &effect) < 0) + if (ioctl(m_ffb_fd, EVIOCSFF, &effect) < 0) { ERROR_LOG_FMT(COMMON, "EVIOCSFF FF_SPRING: {}", Common::LastStrerrorString()); return; } - if (m_centering_effect_id == -1) + if (m_effect_id == -1) { - m_centering_effect_id = effect.id; + m_effect_id = effect.id; const input_event play{ .type = EV_FF, - .code = u16(m_centering_effect_id), + .code = u16(m_effect_id), .value = std::numeric_limits::max(), }; - if (write(m_ffb_wheel_fd, &play, sizeof(play)) < 0) + if (write(m_ffb_fd, &play, sizeof(play)) < 0) ERROR_LOG_FMT(COMMON, "EV_FF: {}", Common::LastStrerrorString()); } } - else if (m_centering_effect_id != -1) + +private: + const int m_ffb_fd; + s16 m_effect_id = -1; +}; + +class FrictionEffect final : public Core::FrictionEffect +{ +public: + explicit FrictionEffect(int ffb_fd) : m_ffb_fd{ffb_fd} {} + + ~FrictionEffect() override { SetForce(0.0); } + + void SetForce(double gain) override { - if (ioctl(m_ffb_wheel_fd, EVIOCRMFF, m_centering_effect_id) < 0) - ERROR_LOG_FMT(COMMON, "EVIOCRMFF: {}", Common::LastStrerrorString()); + INFO_LOG_FMT(CONTROLLERINTERFACE, "FrictionEffect: {:.2}", gain); - // TODO: Is it correct to not remove this effect on destruction. - // Is it enough to close the parent fd ? + if (gain == 0.0) + { + if (m_effect_id != -1) + { + if (ioctl(m_ffb_fd, EVIOCRMFF, m_effect_id) < 0) + ERROR_LOG_FMT(COMMON, "EVIOCRMFF: {}", Common::LastStrerrorString()); - m_centering_effect_id = -1; + // TODO: Is it correct to not remove this effect on destruction. + // Is it enough to close the parent fd ? + + m_effect_id = -1; + } + + return; + } + + ff_effect effect{ + .type = FF_FRICTION, + .id = m_effect_id, + }; + + // + // "Values above 32767 ms (0x7fff) should not be used and have unspecified results." + effect.replay.length = 0x7fff; + + auto& condition = effect.u.condition; + + const auto unsigned_gain = ControllerEmu::MapFloat(gain, 0); + condition[0].right_saturation = unsigned_gain; + condition[0].left_saturation = unsigned_gain; + + // TODO: Is this a sensible coeff value ? + constexpr auto coeff = std::numeric_limits::max(); + condition[0].right_coeff = coeff; + condition[0].left_coeff = coeff; + + if (ioctl(m_ffb_fd, EVIOCSFF, &effect) < 0) + { + ERROR_LOG_FMT(COMMON, "EVIOCSFF FF_SPRING: {}", Common::LastStrerrorString()); + return; + } + + if (m_effect_id == -1) + { + m_effect_id = effect.id; + + const input_event play{ + .type = EV_FF, + .code = u16(m_effect_id), + .value = std::numeric_limits::max(), + }; + + if (write(m_ffb_fd, &play, sizeof(play)) < 0) + ERROR_LOG_FMT(COMMON, "EV_FF: {}", Common::LastStrerrorString()); + } } + +private: + const int m_ffb_fd; + s16 m_effect_id = -1; +}; + +std::unique_ptr evdevDevice::CreateSpringEffect() +{ + if (m_ffb_fd == -1) + return nullptr; + + return std::make_unique(m_ffb_fd); +} + +std::unique_ptr evdevDevice::CreateFrictionEffect() +{ + if (m_ffb_fd == -1) + return nullptr; + + return std::make_unique(m_ffb_fd); } evdevDevice::Effect::Effect(int fd) : m_fd(fd) diff --git a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.h b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.h index f569999101..f982ae9670 100644 --- a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.h +++ b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.h @@ -88,7 +88,8 @@ public: std::string GetName() const override { return m_name; } std::string GetSource() const override { return "evdev"; } - void SetCenteringForce(double gain, double center_position) override; + std::unique_ptr CreateSpringEffect() override; + std::unique_ptr CreateFrictionEffect() override; private: std::string m_name; @@ -104,7 +105,6 @@ private: InputBackend& m_input_backend; - int m_ffb_wheel_fd = -1; - s16 m_centering_effect_id = -1; + int m_ffb_fd = -1; }; } // namespace ciface::evdev