2015-06-29 00:17:35 +00:00
|
|
|
// Copyright 2015 Dolphin Emulator Project
|
|
|
|
// Licensed under GPLv2+
|
|
|
|
// Refer to the license.txt file included.
|
|
|
|
|
2017-09-09 19:52:35 +00:00
|
|
|
#include <cstring>
|
2015-07-08 18:09:08 +00:00
|
|
|
#include <fcntl.h>
|
2015-06-29 00:17:35 +00:00
|
|
|
#include <libudev.h>
|
2015-08-04 17:59:34 +00:00
|
|
|
#include <map>
|
2016-07-14 15:50:35 +00:00
|
|
|
#include <memory>
|
2017-09-09 19:52:35 +00:00
|
|
|
#include <string>
|
2015-06-29 00:17:35 +00:00
|
|
|
#include <unistd.h>
|
|
|
|
|
2016-07-14 15:50:35 +00:00
|
|
|
#include <sys/eventfd.h>
|
|
|
|
|
2015-09-26 20:39:47 +00:00
|
|
|
#include "Common/Assert.h"
|
2016-07-14 15:50:35 +00:00
|
|
|
#include "Common/Flag.h"
|
2015-06-29 00:17:35 +00:00
|
|
|
#include "Common/Logging/Log.h"
|
2016-07-11 12:00:09 +00:00
|
|
|
#include "Common/MathUtil.h"
|
2016-07-01 03:05:24 +00:00
|
|
|
#include "Common/StringUtil.h"
|
2016-07-14 15:50:35 +00:00
|
|
|
#include "Common/Thread.h"
|
2016-06-12 15:08:04 +00:00
|
|
|
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
2015-06-29 00:17:35 +00:00
|
|
|
#include "InputCommon/ControllerInterface/evdev/evdev.h"
|
|
|
|
|
|
|
|
namespace ciface
|
|
|
|
{
|
|
|
|
namespace evdev
|
|
|
|
{
|
2016-07-14 15:50:35 +00:00
|
|
|
static std::thread s_hotplug_thread;
|
|
|
|
static Common::Flag s_hotplug_thread_running;
|
|
|
|
static int s_wakeup_eventfd;
|
|
|
|
|
|
|
|
// There is no easy way to get the device name from only a dev node
|
|
|
|
// during a device removed event, since libevdev can't work on removed devices;
|
|
|
|
// sysfs is not stable, so this is probably the easiest way to get a name for a node.
|
|
|
|
static std::map<std::string, std::string> s_devnode_name_map;
|
|
|
|
|
2015-08-01 23:29:06 +00:00
|
|
|
static std::string GetName(const std::string& devnode)
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
int fd = open(devnode.c_str(), O_RDWR | O_NONBLOCK);
|
|
|
|
libevdev* dev = nullptr;
|
|
|
|
int ret = libevdev_new_from_fd(fd, &dev);
|
|
|
|
if (ret != 0)
|
|
|
|
{
|
|
|
|
close(fd);
|
|
|
|
return std::string();
|
|
|
|
}
|
2016-07-14 15:50:35 +00:00
|
|
|
std::string res = StripSpaces(libevdev_get_name(dev));
|
2016-06-24 08:43:46 +00:00
|
|
|
libevdev_free(dev);
|
|
|
|
close(fd);
|
|
|
|
return res;
|
2015-08-01 23:29:06 +00:00
|
|
|
}
|
|
|
|
|
2016-07-14 15:50:35 +00:00
|
|
|
static void HotplugThreadFunc()
|
|
|
|
{
|
|
|
|
Common::SetCurrentThreadName("evdev Hotplug Thread");
|
|
|
|
NOTICE_LOG(SERIALINTERFACE, "evdev hotplug thread started");
|
|
|
|
|
|
|
|
udev* udev = udev_new();
|
|
|
|
_assert_msg_(PAD, udev != nullptr, "Couldn't initialize libudev.");
|
|
|
|
|
|
|
|
// Set up monitoring
|
|
|
|
udev_monitor* monitor = udev_monitor_new_from_netlink(udev, "udev");
|
|
|
|
udev_monitor_filter_add_match_subsystem_devtype(monitor, "input", nullptr);
|
|
|
|
udev_monitor_enable_receiving(monitor);
|
|
|
|
const int monitor_fd = udev_monitor_get_fd(monitor);
|
|
|
|
|
|
|
|
while (s_hotplug_thread_running.IsSet())
|
|
|
|
{
|
|
|
|
fd_set fds;
|
|
|
|
|
|
|
|
FD_ZERO(&fds);
|
|
|
|
FD_SET(monitor_fd, &fds);
|
|
|
|
FD_SET(s_wakeup_eventfd, &fds);
|
|
|
|
|
2018-02-04 21:45:28 +00:00
|
|
|
int ret = select(std::max(monitor_fd, s_wakeup_eventfd) + 1, &fds, nullptr, nullptr, nullptr);
|
2016-07-14 15:50:35 +00:00
|
|
|
if (ret < 1 || !FD_ISSET(monitor_fd, &fds))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
udev_device* dev = udev_monitor_receive_device(monitor);
|
|
|
|
|
|
|
|
const char* action = udev_device_get_action(dev);
|
|
|
|
const char* devnode = udev_device_get_devnode(dev);
|
|
|
|
if (!devnode)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (strcmp(action, "remove") == 0)
|
|
|
|
{
|
|
|
|
const auto it = s_devnode_name_map.find(devnode);
|
|
|
|
if (it == s_devnode_name_map.end())
|
|
|
|
continue; // we don't know the name for this device, so it is probably not an evdev device
|
|
|
|
const std::string& name = it->second;
|
|
|
|
g_controller_interface.RemoveDevice([&name](const auto& device) {
|
|
|
|
return device->GetSource() == "evdev" && device->GetName() == name && !device->IsValid();
|
|
|
|
});
|
|
|
|
s_devnode_name_map.erase(devnode);
|
|
|
|
}
|
|
|
|
// Only react to "device added" events for evdev devices that we can access.
|
|
|
|
else if (strcmp(action, "add") == 0 && access(devnode, W_OK) == 0)
|
|
|
|
{
|
|
|
|
const std::string name = GetName(devnode);
|
|
|
|
if (name.empty())
|
|
|
|
continue; // probably not an evdev device
|
|
|
|
auto device = std::make_shared<evdevDevice>(devnode);
|
|
|
|
if (device->IsInteresting())
|
|
|
|
{
|
|
|
|
g_controller_interface.AddDevice(std::move(device));
|
|
|
|
s_devnode_name_map.insert(std::pair<std::string, std::string>(devnode, name));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
udev_device_unref(dev);
|
|
|
|
}
|
|
|
|
NOTICE_LOG(SERIALINTERFACE, "evdev hotplug thread stopped");
|
|
|
|
}
|
|
|
|
|
|
|
|
static void StartHotplugThread()
|
|
|
|
{
|
2016-12-05 00:37:36 +00:00
|
|
|
// Mark the thread as running.
|
|
|
|
if (!s_hotplug_thread_running.TestAndSet())
|
|
|
|
// It was already running.
|
2016-07-14 15:50:35 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
s_wakeup_eventfd = eventfd(0, 0);
|
|
|
|
_assert_msg_(PAD, s_wakeup_eventfd != -1, "Couldn't create eventfd.");
|
|
|
|
s_hotplug_thread = std::thread(HotplugThreadFunc);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void StopHotplugThread()
|
|
|
|
{
|
2016-12-05 00:37:36 +00:00
|
|
|
// Tell the hotplug thread to stop.
|
|
|
|
if (!s_hotplug_thread_running.TestAndClear())
|
|
|
|
// It wasn't running, we're done.
|
|
|
|
return;
|
|
|
|
// Write something to efd so that select() stops blocking.
|
|
|
|
uint64_t value = 1;
|
|
|
|
if (write(s_wakeup_eventfd, &value, sizeof(uint64_t)) < 0)
|
2016-07-14 15:50:35 +00:00
|
|
|
{
|
|
|
|
}
|
2016-12-05 00:37:36 +00:00
|
|
|
s_hotplug_thread.join();
|
2016-07-14 15:50:35 +00:00
|
|
|
}
|
|
|
|
|
2016-06-12 15:08:04 +00:00
|
|
|
void Init()
|
2015-06-29 00:17:35 +00:00
|
|
|
{
|
2016-07-14 15:50:35 +00:00
|
|
|
s_devnode_name_map.clear();
|
2016-10-16 20:39:05 +00:00
|
|
|
StartHotplugThread();
|
|
|
|
}
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2016-10-16 20:39:05 +00:00
|
|
|
void PopulateDevices()
|
|
|
|
{
|
|
|
|
// We use udev to iterate over all /dev/input/event* devices.
|
|
|
|
// Note: the Linux kernel is currently limited to just 32 event devices. If
|
|
|
|
// this ever changes, hopefully udev will take care of this.
|
2016-07-14 15:50:35 +00:00
|
|
|
|
|
|
|
udev* udev = udev_new();
|
|
|
|
_assert_msg_(PAD, udev != nullptr, "Couldn't initialize libudev.");
|
2016-06-24 08:43:46 +00:00
|
|
|
|
|
|
|
// List all input devices
|
|
|
|
udev_enumerate* enumerate = udev_enumerate_new(udev);
|
|
|
|
udev_enumerate_add_match_subsystem(enumerate, "input");
|
|
|
|
udev_enumerate_scan_devices(enumerate);
|
|
|
|
udev_list_entry* devices = udev_enumerate_get_list_entry(enumerate);
|
|
|
|
|
|
|
|
// Iterate over all input devices
|
|
|
|
udev_list_entry* dev_list_entry;
|
|
|
|
udev_list_entry_foreach(dev_list_entry, devices)
|
|
|
|
{
|
|
|
|
const char* path = udev_list_entry_get_name(dev_list_entry);
|
|
|
|
|
|
|
|
udev_device* dev = udev_device_new_from_syspath(udev, path);
|
|
|
|
|
|
|
|
const char* devnode = udev_device_get_devnode(dev);
|
|
|
|
// We only care about devices which we have read/write access to.
|
|
|
|
if (devnode && access(devnode, W_OK) == 0)
|
|
|
|
{
|
|
|
|
// Unfortunately udev gives us no way to filter out the non event device interfaces.
|
|
|
|
// So we open it and see if it works with evdev ioctls or not.
|
2016-07-14 08:25:52 +00:00
|
|
|
auto input = std::make_shared<evdevDevice>(devnode);
|
2016-06-24 08:43:46 +00:00
|
|
|
|
|
|
|
if (input->IsInteresting())
|
|
|
|
{
|
2016-06-25 19:46:39 +00:00
|
|
|
g_controller_interface.AddDevice(std::move(input));
|
2016-12-26 00:24:35 +00:00
|
|
|
std::string name = GetName(devnode);
|
2016-07-14 15:50:35 +00:00
|
|
|
s_devnode_name_map.insert(std::pair<std::string, std::string>(devnode, name));
|
2016-06-24 08:43:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
udev_device_unref(dev);
|
|
|
|
}
|
|
|
|
udev_enumerate_unref(enumerate);
|
|
|
|
udev_unref(udev);
|
2016-07-14 15:50:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Shutdown()
|
|
|
|
{
|
|
|
|
StopHotplugThread();
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
2016-07-14 08:25:52 +00:00
|
|
|
evdevDevice::evdevDevice(const std::string& devnode) : m_devfile(devnode)
|
2015-06-29 00:17:35 +00:00
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
// The device file will be read on one of the main threads, so we open in non-blocking mode.
|
|
|
|
m_fd = open(devnode.c_str(), O_RDWR | O_NONBLOCK);
|
|
|
|
int ret = libevdev_new_from_fd(m_fd, &m_dev);
|
|
|
|
|
|
|
|
if (ret != 0)
|
|
|
|
{
|
|
|
|
// This useally fails because the device node isn't an evdev device, such as /dev/input/js0
|
|
|
|
m_initialized = false;
|
|
|
|
close(m_fd);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-07-01 03:05:24 +00:00
|
|
|
m_name = StripSpaces(libevdev_get_name(m_dev));
|
2016-06-24 08:43:46 +00:00
|
|
|
|
|
|
|
// Controller buttons (and keyboard keys)
|
|
|
|
int num_buttons = 0;
|
|
|
|
for (int key = 0; key < KEY_MAX; key++)
|
|
|
|
if (libevdev_has_event_code(m_dev, EV_KEY, key))
|
|
|
|
AddInput(new Button(num_buttons++, key, m_dev));
|
|
|
|
|
|
|
|
// Absolute axis (thumbsticks)
|
|
|
|
int num_axis = 0;
|
|
|
|
for (int axis = 0; axis < 0x100; axis++)
|
|
|
|
if (libevdev_has_event_code(m_dev, EV_ABS, axis))
|
|
|
|
{
|
|
|
|
AddAnalogInputs(new Axis(num_axis, axis, false, m_dev),
|
|
|
|
new Axis(num_axis, axis, true, m_dev));
|
|
|
|
num_axis++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Force feedback
|
|
|
|
if (libevdev_has_event_code(m_dev, EV_FF, FF_PERIODIC))
|
|
|
|
{
|
|
|
|
for (auto type : {FF_SINE, FF_SQUARE, FF_TRIANGLE, FF_SAW_UP, FF_SAW_DOWN})
|
|
|
|
if (libevdev_has_event_code(m_dev, EV_FF, type))
|
|
|
|
AddOutput(new ForceFeedback(type, m_dev));
|
|
|
|
}
|
|
|
|
if (libevdev_has_event_code(m_dev, EV_FF, FF_RUMBLE))
|
|
|
|
{
|
|
|
|
AddOutput(new ForceFeedback(FF_RUMBLE, m_dev));
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Add leds as output devices
|
|
|
|
|
|
|
|
m_initialized = true;
|
|
|
|
m_interesting = num_axis >= 2 || num_buttons >= 8;
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
evdevDevice::~evdevDevice()
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
if (m_initialized)
|
|
|
|
{
|
|
|
|
libevdev_free(m_dev);
|
|
|
|
close(m_fd);
|
|
|
|
}
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void evdevDevice::UpdateInput()
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
// Run through all evdev events
|
|
|
|
// libevdev will keep track of the actual controller state internally which can be queried
|
|
|
|
// later with libevdev_fetch_event_value()
|
|
|
|
input_event ev;
|
|
|
|
int rc = LIBEVDEV_READ_STATUS_SUCCESS;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
if (rc == LIBEVDEV_READ_STATUS_SYNC)
|
|
|
|
rc = libevdev_next_event(m_dev, LIBEVDEV_READ_FLAG_SYNC, &ev);
|
|
|
|
else
|
|
|
|
rc = libevdev_next_event(m_dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
|
|
|
|
} while (rc >= 0);
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
2016-07-14 15:50:35 +00:00
|
|
|
bool evdevDevice::IsValid() const
|
|
|
|
{
|
|
|
|
int current_fd = libevdev_get_fd(m_dev);
|
|
|
|
if (current_fd == -1)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
libevdev* device;
|
|
|
|
if (libevdev_new_from_fd(current_fd, &device) != 0)
|
|
|
|
{
|
|
|
|
close(current_fd);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
libevdev_free(device);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-29 00:17:35 +00:00
|
|
|
std::string evdevDevice::Button::GetName() const
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
// Buttons below 0x100 are mostly keyboard keys, and the names make sense
|
|
|
|
if (m_code < 0x100)
|
|
|
|
{
|
|
|
|
const char* name = libevdev_event_code_get_name(EV_KEY, m_code);
|
|
|
|
if (name)
|
2016-07-01 03:05:24 +00:00
|
|
|
return StripSpaces(name);
|
2016-06-24 08:43:46 +00:00
|
|
|
}
|
|
|
|
// But controllers use codes above 0x100, and the standard label often doesn't match.
|
|
|
|
// We are better off with Button 0 and so on.
|
|
|
|
return "Button " + std::to_string(m_index);
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ControlState evdevDevice::Button::GetState() const
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
int value = 0;
|
|
|
|
libevdev_fetch_event_value(m_dev, EV_KEY, m_code, &value);
|
|
|
|
return value;
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
2016-06-24 08:43:46 +00:00
|
|
|
evdevDevice::Axis::Axis(u8 index, u16 code, bool upper, libevdev* dev)
|
|
|
|
: m_code(code), m_index(index), m_upper(upper), m_dev(dev)
|
2015-06-29 00:17:35 +00:00
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
m_min = libevdev_get_abs_minimum(m_dev, m_code);
|
evdev: Correctly calculate axis range for min values greater than 0.
Axis range was previously calculated as max + abs(min), which relies on the assumption that
min will not exceed 0. For (min, max) values like (0, 255) or (-128, 127), which I assume to
be the most common cases, the range is correctly calculated as 255. However, given (20,
235), the range is erroneously calculated as 255, leading to axis values being normalized
incorrectly.
SDL already handles this case correctly. After changing the range calculation to max - min,
the axis values received from the evdev backend are practically identical to the values
received from the SDL backend.
2017-11-11 05:58:07 +00:00
|
|
|
m_range = libevdev_get_abs_maximum(m_dev, m_code) - m_min;
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string evdevDevice::Axis::GetName() const
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
return "Axis " + std::to_string(m_index) + (m_upper ? "+" : "-");
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ControlState evdevDevice::Axis::GetState() const
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
int value = 0;
|
|
|
|
libevdev_fetch_event_value(m_dev, EV_ABS, m_code, &value);
|
2015-06-30 12:37:26 +00:00
|
|
|
|
2016-06-24 08:43:46 +00:00
|
|
|
// Value from 0.0 to 1.0
|
2016-07-11 12:00:09 +00:00
|
|
|
ControlState fvalue = MathUtil::Clamp(double(value - m_min) / double(m_range), 0.0, 1.0);
|
2015-06-30 12:37:26 +00:00
|
|
|
|
2016-06-24 08:43:46 +00:00
|
|
|
// Split into two axis, each covering half the range from 0.0 to 1.0
|
|
|
|
if (m_upper)
|
|
|
|
return std::max(0.0, fvalue - 0.5) * 2.0;
|
|
|
|
else
|
|
|
|
return (0.5 - std::min(0.5, fvalue)) * 2.0;
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string evdevDevice::ForceFeedback::GetName() const
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
// We have some default names.
|
|
|
|
switch (m_type)
|
|
|
|
{
|
|
|
|
case FF_SINE:
|
|
|
|
return "Sine";
|
|
|
|
case FF_TRIANGLE:
|
|
|
|
return "Triangle";
|
|
|
|
case FF_SQUARE:
|
|
|
|
return "Square";
|
|
|
|
case FF_RUMBLE:
|
|
|
|
return "LeftRight";
|
|
|
|
default:
|
|
|
|
{
|
|
|
|
const char* name = libevdev_event_code_get_name(EV_FF, m_type);
|
|
|
|
if (name)
|
2016-07-01 03:05:24 +00:00
|
|
|
return StripSpaces(name);
|
2016-06-24 08:43:46 +00:00
|
|
|
return "Unknown";
|
|
|
|
}
|
|
|
|
}
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void evdevDevice::ForceFeedback::SetState(ControlState state)
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
// libevdev doesn't have nice helpers for forcefeedback
|
|
|
|
// we will use the file descriptors directly.
|
|
|
|
|
|
|
|
if (m_id != -1) // delete the previous effect (which also stops it)
|
|
|
|
{
|
|
|
|
ioctl(m_fd, EVIOCRMFF, m_id);
|
|
|
|
m_id = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state > 0) // Upload and start an effect.
|
|
|
|
{
|
|
|
|
ff_effect effect;
|
|
|
|
|
|
|
|
effect.id = -1;
|
|
|
|
effect.direction = 0; // down
|
|
|
|
effect.replay.length = 500; // 500ms
|
|
|
|
effect.replay.delay = 0;
|
|
|
|
effect.trigger.button = 0; // don't trigger on button press
|
|
|
|
effect.trigger.interval = 0;
|
|
|
|
|
|
|
|
// This is the the interface that XInput uses, with 2 motors of differing sizes/frequencies that
|
|
|
|
// are controlled seperatally
|
|
|
|
if (m_type == FF_RUMBLE)
|
|
|
|
{
|
|
|
|
effect.type = FF_RUMBLE;
|
|
|
|
// max ranges tuned to 'feel' similar in magnitude to triangle/sine on xbox360 controller
|
|
|
|
effect.u.rumble.strong_magnitude = u16(state * 0x4000);
|
|
|
|
effect.u.rumble.weak_magnitude = u16(state * 0xFFFF);
|
|
|
|
}
|
|
|
|
else // FF_PERIODIC, a more generic interface.
|
|
|
|
{
|
|
|
|
effect.type = FF_PERIODIC;
|
|
|
|
effect.u.periodic.waveform = m_type;
|
|
|
|
effect.u.periodic.phase = 0x7fff; // 180 degrees
|
|
|
|
effect.u.periodic.offset = 0;
|
|
|
|
effect.u.periodic.period = 10;
|
|
|
|
effect.u.periodic.magnitude = s16(state * 0x7FFF);
|
|
|
|
effect.u.periodic.envelope.attack_length = 0; // no attack
|
|
|
|
effect.u.periodic.envelope.attack_level = 0;
|
|
|
|
effect.u.periodic.envelope.fade_length = 0;
|
|
|
|
effect.u.periodic.envelope.fade_level = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
ioctl(m_fd, EVIOCSFF, &effect);
|
|
|
|
m_id = effect.id;
|
|
|
|
|
|
|
|
input_event play;
|
|
|
|
play.type = EV_FF;
|
|
|
|
play.code = m_id;
|
|
|
|
play.value = 1;
|
|
|
|
|
2016-10-22 17:08:17 +00:00
|
|
|
if (write(m_fd, &play, sizeof(play)) < 0)
|
|
|
|
{
|
|
|
|
}
|
2016-06-24 08:43:46 +00:00
|
|
|
}
|
2015-07-05 19:57:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
evdevDevice::ForceFeedback::~ForceFeedback()
|
|
|
|
{
|
2016-06-24 08:43:46 +00:00
|
|
|
// delete the uploaded effect, so we don't leak it.
|
|
|
|
if (m_id != -1)
|
|
|
|
{
|
|
|
|
ioctl(m_fd, EVIOCRMFF, m_id);
|
|
|
|
}
|
2015-06-29 00:17:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|