Robustify Linux gamepad support

This commit is contained in:
rdb 2015-12-19 16:03:28 +01:00
parent 70370999c9
commit ab66108661
8 changed files with 510 additions and 140 deletions

View File

@ -19,6 +19,7 @@
#include "gamepadButton.h"
#include "keyboardButton.h"
#include "mouseButton.h"
#include "inputDeviceManager.h"
#include <fcntl.h>
#include <linux/input.h>
@ -32,19 +33,6 @@ static InputDevice::ControlAxis axis_map[] = {
TypeHandle EvdevInputDevice::_type_handle;
////////////////////////////////////////////////////////////////////
// Function: EvdevInputDevice::Constructor
// Access: Published
// Description: Creates a new device using the Linux joystick
// device using the given file descriptor. It will
// be closed when this object destructs.
////////////////////////////////////////////////////////////////////
EvdevInputDevice::
EvdevInputDevice(int fd) :
_fd(fd) {
init_device();
}
////////////////////////////////////////////////////////////////////
// Function: EvdevInputDevice::Constructor
// Access: Published
@ -52,15 +40,18 @@ EvdevInputDevice(int fd) :
// device using the given device filename.
////////////////////////////////////////////////////////////////////
EvdevInputDevice::
EvdevInputDevice(const string &fn) {
_fd = open(fn.c_str(), O_RDONLY | O_NONBLOCK);
EvdevInputDevice(int index) : _index(index) {
char path[64];
sprintf(path, "/dev/input/event%d", index);
_fd = open(path, O_RDONLY | O_NONBLOCK);
if (_fd >= 0) {
init_device();
} else {
_is_connected = false;
device_cat.error()
<< "Opening raw input device: " << strerror(errno) << " " << fn << "\n";
<< "Opening raw input device: " << strerror(errno) << " " << path << "\n";
}
}
@ -77,6 +68,18 @@ EvdevInputDevice::
}
}
////////////////////////////////////////////////////////////////////
// Function: EvdevInputDevice::check_events
// Access: Public
// Description: Returns true if there are pending events.
////////////////////////////////////////////////////////////////////
bool EvdevInputDevice::
check_events() const {
unsigned int avail = 0;
ioctl(_fd, FIONREAD, &avail);
return (avail != 0);
}
////////////////////////////////////////////////////////////////////
// Function: EvdevInputDevice::do_poll
// Access: Private, Virtual
@ -88,8 +91,15 @@ EvdevInputDevice::
////////////////////////////////////////////////////////////////////
void EvdevInputDevice::
do_poll() {
if (_fd != -1) {
if (_fd != -1 && process_events()) {
while (process_events()) {}
// If we got events, we are obviously connected. Mark us so.
if (!_is_connected) {
_is_connected = true;
InputDeviceManager *mgr = InputDeviceManager::get_global_ptr();
mgr->add_device(this);
}
}
}
@ -104,10 +114,8 @@ init_device() {
uint8_t evtypes[(EV_CNT + 7) >> 3];
memset(evtypes, 0, sizeof(evtypes));
char name[256];
char uniq[256];
char name[128];
if (ioctl(_fd, EVIOCGNAME(sizeof(name)), name) < 0 ||
ioctl(_fd, EVIOCGPHYS(sizeof(uniq)), uniq) < 0 ||
ioctl(_fd, EVIOCGBIT(0, sizeof(evtypes)), evtypes) < 0) {
close(_fd);
_fd = -1;
@ -116,29 +124,59 @@ init_device() {
return false;
}
for (char *p=name; *p; p++) {
if (((*p<'a')||(*p>'z')) && ((*p<'A')||(*p>'Z')) && ((*p<'0')||(*p>'9'))) {
*p = '_';
}
}
for (char *p=uniq; *p; p++) {
if (((*p<'a')||(*p>'z')) && ((*p<'A')||(*p>'Z')) && ((*p<'0')||(*p>'9'))) {
*p = '_';
}
_name.assign(name);
struct input_id id;
if (ioctl(_fd, EVIOCGID, &id) >= 0) {
_vendor_id = id.vendor;
_product_id = id.product;
}
_name = ((string)name) + "." + uniq;
bool all_values_zero = true;
if (test_bit(EV_ABS, evtypes)) {
// Check which axes are on the device.
uint8_t axes[(ABS_CNT + 7) >> 3];
memset(axes, 0, sizeof(axes));
AxisRange range;
range._scale = 1.0;
range._bias = 0.0;
_axis_ranges.resize(ABS_CNT, range);
int num_bits = ioctl(_fd, EVIOCGBIT(EV_ABS, sizeof(axes)), axes) << 3;
for (int i = 0; i < num_bits; ++i) {
if (test_bit(i, axes)) {
if (i >= 0 && i < 6) {
set_control_map(i, axis_map[i]);
// Check the initial value and ranges.
struct input_absinfo absinfo;
if (ioctl(_fd, EVIOCGABS(i), &absinfo) >= 0) {
double factor, bias;
if (absinfo.minimum < 0) {
// Centered, eg. for sticks.
factor = 2.0 / (absinfo.maximum - absinfo.minimum);
bias = (absinfo.maximum + absinfo.minimum) / (double)(absinfo.minimum - absinfo.maximum);
} else {
// 0-based, eg. for triggers.
factor = 1.0 / absinfo.maximum;
bias = 0.0;
}
// Flip Y axis to match Windows implementation.
if (i == ABS_Y || i == ABS_RY) {
factor = -factor;
bias = -bias;
}
_axis_ranges[i]._scale = factor;
_axis_ranges[i]._bias = bias;
_controls[i]._state = fma(absinfo.value, factor, bias);
if (absinfo.value != 0) {
all_values_zero = false;
}
}
}
}
}
@ -148,14 +186,40 @@ init_device() {
// Check which buttons are on the device.
uint8_t keys[(KEY_CNT + 7) >> 3];
memset(keys, 0, sizeof(keys));
ioctl(_fd, EVIOCGBIT(EV_KEY, sizeof(keys)), keys);
// Also check whether the buttons are currently pressed.
uint8_t states[(KEY_CNT + 7) >> 3];
memset(states, 0, sizeof(states));
ioctl(_fd, EVIOCGKEY(sizeof(states)), states);
int num_bits = ioctl(_fd, EVIOCGBIT(EV_KEY, sizeof(keys)), keys) << 3;
int bi = 0;
for (int i = 0; i < num_bits; ++i) {
for (int i = 0; i < KEY_CNT; ++i) {
if (test_bit(i, keys)) {
set_button_map(bi++, map_button(i));
set_button_map(bi, map_button(i));
if (test_bit(i, states)) {
_buttons[bi]._state = S_down;
all_values_zero = false;
} else {
_buttons[bi]._state = S_up;
}
++bi;
}
}
// Check device type.
if (test_bit(BTN_GAMEPAD, keys)) {
_device_class = DC_gamepad;
} else if (test_bit(BTN_JOYSTICK, keys)) {
_device_class = DC_flight_stick;
} else if (test_bit(BTN_MOUSE, keys)) {
_device_class = DC_mouse;
} else if (test_bit(BTN_WHEEL, keys)) {
_device_class = DC_steering_wheel;
}
}
if (test_bit(EV_REL, evtypes)) {
@ -166,14 +230,52 @@ init_device() {
_flags |= IDF_has_vibration;
}
_is_connected = true;
char path[64];
char buffer[256];
sprintf(path, "/sys/class/input/event%d/device/device/../product", _index);
FILE *f = fopen(path, "r");
if (f) {
fgets(buffer, sizeof(buffer), f);
buffer[strcspn(buffer, "\r\n")] = 0;
if (buffer[0] != 0) {
_name.assign(buffer);
}
fclose(f);
}
sprintf(path, "/sys/class/input/event%d/device/device/../manufacturer", _index);
f = fopen(path, "r");
if (f) {
fgets(buffer, sizeof(buffer), f);
buffer[strcspn(buffer, "\r\n")] = 0;
_manufacturer.assign(buffer);
fclose(f);
}
sprintf(path, "/sys/class/input/event%d/device/device/../serial", _index);
f = fopen(path, "r");
if (f) {
fgets(buffer, sizeof(buffer), f);
buffer[strcspn(buffer, "\r\n")] = 0;
_serial_number.assign(buffer);
fclose(f);
}
// Special-case fix for Xbox 360 Wireless Receiver: the Linux kernel
// driver always reports 4 connected gamepads, regardless of the number
// of gamepads actually present. This hack partially remedies this.
if (all_values_zero && _vendor_id == 0x045e && _product_id == 0x0719) {
_is_connected = false;
} else {
_is_connected = true;
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: EvdevInputDevice::process_events
// Access: Private
// Description: Reads a number of events from the device.
// Description: Reads a number of events from the device. Returns
// true if events were read, meaning this function
// should keep being called until it returns false.
////////////////////////////////////////////////////////////////////
bool EvdevInputDevice::
process_events() {
@ -186,10 +288,12 @@ process_events() {
// No data available for now.
} else if (errno == ENODEV || errno == EINVAL) {
// The device ceased to exist, so we better close it.
// The device ceased to exist, so we better close it. No need
// to worry about removing it from the InputDeviceManager, as it
// will get an inotify event sooner or later about this.
close(_fd);
_fd = -1;
_is_connected = false;
//_is_connected = false;
errno = 0;
} else {
@ -210,23 +314,31 @@ process_events() {
double time = ClockObject::get_global_clock()->get_frame_time();
ButtonHandle button;
// It seems that some devices send a single EV_SYN event when being
// unplugged. Boo. Ignore it.
if (n_read == 1 && events[0].code == EV_SYN) {
return false;
}
for (int i = 0; i < n_read; ++i) {
int code = events[i].code;
switch (events[i].type) {
case EV_SYN:
break;
case EV_REL:
if (events[i].code == REL_X) x += events[i].value;
if (events[i].code == REL_Y) y += events[i].value;
if (code == REL_X) x += events[i].value;
if (code == REL_Y) y += events[i].value;
have_pointer = true;
break;
case EV_ABS:
set_control_state(events[i].code, events[i].value / 32767.0);
set_control_state(code, fma(events[i].value, _axis_ranges[code]._scale, _axis_ranges[code]._bias));
break;
case EV_KEY:
button = map_button(events[i].code);
button = map_button(code);
_button_events->add_event(ButtonEvent(button, events[i].value ? ButtonEvent::T_down : ButtonEvent::T_up, time));
break;

View File

@ -25,11 +25,12 @@
// /dev/input/event# API to read data from a raw mouse.
////////////////////////////////////////////////////////////////////
class EXPCL_PANDA_DEVICE EvdevInputDevice : public InputDevice {
PUBLISHED:
EvdevInputDevice(int fd);
EvdevInputDevice(const string &device);
public:
EvdevInputDevice(int index);
virtual ~EvdevInputDevice();
bool check_events() const;
private:
virtual void do_poll();
@ -37,8 +38,15 @@ private:
bool process_events();
private:
int _index;
int _fd;
struct AxisRange {
double _scale;
double _bias;
};
pvector<AxisRange> _axis_ranges;
public:
static ButtonHandle map_button(int code);

View File

@ -35,7 +35,8 @@ InputDevice() :
////////////////////////////////////////////////////////////////////
// Function: InputDevice::get_name
// Access: Public
// Description:
// Description: Returns a human-readable name for the device. May
// not necessarily be unique.
////////////////////////////////////////////////////////////////////
INLINE string InputDevice::
get_name() const {
@ -43,6 +44,42 @@ get_name() const {
return _name;
}
////////////////////////////////////////////////////////////////////
// Function: InputDevice::get_manufacturer
// Access: Public
// Description: Returns a string containing the manufacturer of
// the device, if this information is known.
////////////////////////////////////////////////////////////////////
INLINE string InputDevice::
get_manufacturer() const {
LightMutexHolder holder(_lock);
return _manufacturer;
}
////////////////////////////////////////////////////////////////////
// Function: InputDevice::get_vendor_id
// Access: Public
// Description: Returns a string containing the USB vendor ID of
// the device, if this information is known.
////////////////////////////////////////////////////////////////////
INLINE unsigned short InputDevice::
get_vendor_id() const {
LightMutexHolder holder(_lock);
return _vendor_id;
}
////////////////////////////////////////////////////////////////////
// Function: InputDevice::get_product_id
// Access: Public
// Description: Returns a string containing the USB product ID of
// the device, if this information is known.
////////////////////////////////////////////////////////////////////
INLINE unsigned short InputDevice::
get_product_id() const {
LightMutexHolder holder(_lock);
return _product_id;
}
////////////////////////////////////////////////////////////////////
// Function: InputDevice::is_connected
// Access: Public

View File

@ -111,12 +111,24 @@ PUBLISHED:
};
INLINE string get_name() const;
INLINE string get_manufacturer() const;
INLINE unsigned short get_vendor_id() const;
INLINE unsigned short get_product_id() const;
INLINE bool is_connected() const;
INLINE DeviceClass get_device_class() const;
// The human-readable name of this input device.
MAKE_PROPERTY(name, get_name);
// The device's manufacturer, or the empty string if not known.
MAKE_PROPERTY(manufacturer, get_manufacturer);
// USB vendor ID of the device, or 0 if not known.
MAKE_PROPERTY(vendor_id, get_vendor_id);
// USB product ID of the device, or 0 if not known.
MAKE_PROPERTY(product_id, get_product_id);
// This is false if we know that the device is not currently connected.
// May report false positives if we can't know this with certainty.
MAKE_PROPERTY(connected, is_connected);
@ -212,8 +224,8 @@ protected:
DeviceClass _device_class;
int _flags;
int _event_sequence;
short _vendor_id;
short _product_id;
unsigned short _vendor_id;
unsigned short _product_id;
bool _is_connected;
bool _enable_pointer_events;
PointerData _pointer_data;

View File

@ -39,33 +39,6 @@ InputDeviceManager *InputDeviceManager::_global_ptr = NULL;
#ifdef PHAVE_LINUX_INPUT_H
InputDeviceManager::
InputDeviceManager() : _inotify_fd(-1) {
// Scan /dev/input for a list of input devices.
DIR *dir = opendir("/dev/input");
if (dir) {
dirent *entry = readdir(dir);
while (entry != NULL) {
if (entry->d_type == DT_CHR) {
string name(entry->d_name);
if (!consider_add_linux_device(name)) {
// We can't access it. That's pretty normal for most devices.
if (device_cat.is_debug()) {
device_cat.debug()
<< "Ignoring input device /dev/input/" << name << ": "
<< strerror(errno) << "\n";
}
errno = 0;
}
}
entry = readdir(dir);
}
closedir(dir);
} else {
device_cat.error()
<< "Error opening directory /dev/input: " << strerror(errno) << "\n";
return;
}
// Use inotify to watch /dev/input for hotplugging of devices.
_inotify_fd = inotify_init1(O_NONBLOCK);
@ -77,6 +50,35 @@ InputDeviceManager() : _inotify_fd(-1) {
device_cat.error()
<< "Error adding inotify watch on /dev/input: " << strerror(errno) << "\n";
}
// Scan /dev/input for a list of input devices.
DIR *dir = opendir("/dev/input");
if (dir) {
vector_int indices;
dirent *entry = readdir(dir);
while (entry != NULL) {
int index = -1;
if (entry->d_type == DT_CHR && sscanf(entry->d_name, "event%d", &index) == 1) {
indices.push_back(index);
}
entry = readdir(dir);
}
closedir(dir);
// We'll want to sort the devices by index, since the order may be
// meaningful (eg. for the Xbox wireless receiver).
std::sort(indices.begin(), indices.end());
_evdev_devices.resize(indices.back() + 1, NULL);
vector_int::const_iterator it;
for (it = indices.begin(); it != indices.end(); ++it) {
consider_add_evdev_device(*it);
}
} else {
device_cat.error()
<< "Error opening directory /dev/input: " << strerror(errno) << "\n";
return;
}
}
#elif defined(_WIN32)
InputDeviceManager::
@ -129,41 +131,124 @@ InputDeviceManager::
#ifdef PHAVE_LINUX_INPUT_H
////////////////////////////////////////////////////////////////////
// Function: InputDeviceManager::consider_add_linux_device
// Function: InputDeviceManager::consider_add_evdev_device
// Access: Private
// Description: Checks whether the given device is accessible, and
// if so, adds it. Returns false on error.
// Description: Checks whether the event device with the given index
// is accessible, and if so, adds it. Returns the
// device if it was newly connected.
////////////////////////////////////////////////////////////////////
bool InputDeviceManager::
consider_add_linux_device(const string &name) {
// Get the full path name first.
string path = "/dev/input/";
path += name;
if (access(path.c_str(), R_OK) < 0) {
return false;
InputDevice *InputDeviceManager::
consider_add_evdev_device(int ev_index) {
if (ev_index < _evdev_devices.size()) {
if (_evdev_devices[ev_index] != NULL) {
// We already have this device. FIXME: probe it and add it to the
// list of connected devices?
return NULL;
}
} else {
// Make room to store this index.
_evdev_devices.resize(ev_index + 1, NULL);
}
if (_devices_by_path.count(name)) {
// We already have this device.
return true;
}
// Check if we can directly read the event device.
char path[64];
sprintf(path, "/dev/input/event%d", ev_index);
// Check if it's a joystick or game controller device.
if (name.size() > 2 && name[0] == 'j' && name[1] == 's' && isdigit(name[2])) {
PT(InputDevice) device = new LinuxJoystickDevice(path);
if (device_cat.is_info()) {
device_cat.info()
<< "Discovered input device " << *device << "\n";
if (access(path, R_OK) == 0) {
PT(InputDevice) device = new EvdevInputDevice(ev_index);
if (device_cat.is_debug()) {
device_cat.debug()
<< "Discovered evdev input device " << *device << "\n";
}
_devices_by_path[name] = device;
_connected_devices.add_device(MOVE(device));
return true;
_evdev_devices[ev_index] = device;
if (device->is_connected()) {
_connected_devices.add_device(MOVE(device));
} else {
// Wait for activity on the device before it is considered connected.
_inactive_devices.add_device(MOVE(device));
}
return _evdev_devices[ev_index];
}
return true;
// Nope. The permissions haven't been configured to allow it.
// Check if this corresponds to a /dev/input/jsX interface, which has
// a better chance of having read permissions set, but doesn't export
// all of the features (notably, force feedback).
// We do this by checking for a js# directory inside the sysfs
// device directory.
sprintf(path, "/sys/class/input/event%d/device", ev_index);
DIR *dir = opendir(path);
if (dir == NULL) {
if (device_cat.is_debug()) {
device_cat.debug()
<< "Error opening directory " << path << ": " << strerror(errno) << "\n";
}
return NULL;
}
dirent *entry = readdir(dir);
while (entry != NULL) {
int js_index = -1;
if (sscanf(entry->d_name, "js%d", &js_index) == 1) {
// Yes, we fonud a corresponding js device. Try adding it.
closedir(dir);
InputDevice *device = consider_add_js_device(js_index);
if (device != NULL && device_cat.is_warning()) {
// This seemed to work. Display a warning to the user indicating
// that they might want to configure udev properly.
device_cat.warning()
<< "Some features of " << device->get_device_class()
<< " device " << device->get_name() << " are not available due"
" to lack of read permissions on /dev/input/event" << ev_index
<< ".\n";
}
_evdev_devices[ev_index] = device;
return device;
}
entry = readdir(dir);
}
closedir(dir);
return NULL;
}
////////////////////////////////////////////////////////////////////
// Function: InputDeviceManager::consider_add_evdev_device
// Access: Private
// Description: Checks whether the joystick device with the given
// index is accessible, and if so, adds it. Returns
// the device if it was newly connected.
////////////////////////////////////////////////////////////////////
InputDevice *InputDeviceManager::
consider_add_js_device(int js_index) {
char path[64];
sprintf(path, "/dev/input/js%d", js_index);
if (access(path, R_OK) == 0) {
PT(LinuxJoystickDevice) device = new LinuxJoystickDevice(js_index);
if (device_cat.is_debug()) {
device_cat.debug()
<< "Discovered joydev input device " << *device << "\n";
}
InputDevice *device_p = device.p();
if (device->is_connected()) {
_connected_devices.add_device(MOVE(device));
} else {
// Wait for activity on the device before it is considered connected.
_inactive_devices.add_device(MOVE(device));
}
return device_p;
}
return NULL;
}
#endif
////////////////////////////////////////////////////////////////////
@ -199,6 +284,12 @@ add_device(InputDevice *device) {
{
LightMutexHolder holder(_lock);
_connected_devices.add_device(device);
#ifdef PHAVE_LINUX_INPUT_H
// If we had added it pending activity on the device, remove it
// from the list of inactive devices.
_inactive_devices.remove_device(device);
#endif
}
throw_event("connect-device", device);
}
@ -229,9 +320,23 @@ remove_device(InputDevice *device) {
void InputDeviceManager::
update() {
#ifdef PHAVE_LINUX_INPUT_H
// Check for any devices that may be disconnected and need to be probed
// in order to see whether they have been reconnected.
InputDeviceSet inactive_devices;
{
LightMutexHolder holder(_lock);
inactive_devices = _inactive_devices;
}
for (size_t i = 0; i < inactive_devices.size(); ++i) {
InputDevice *device = inactive_devices[i];
if (device != NULL && !device->is_connected()) {
device->poll();
}
}
// We use inotify to tell us whether a device was added, removed,
// or has changed permissions to allow us to access it.
unsigned int avail;
unsigned int avail = 0;
ioctl(_inotify_fd, FIONREAD, &avail);
if (avail == 0) {
return;
@ -261,34 +366,39 @@ update() {
if (event->mask & IN_DELETE) {
// The device was deleted. If we have it, remove it.
DevicesByPath::iterator it = _devices_by_path.find(name);
if (it != _devices_by_path.end()) {
PT(InputDevice) device = it->second;
device->set_connected(false);
_devices_by_path.erase(it);
_connected_devices.remove_device(device);
int index = -1;
if (sscanf(event->name, "event%d", &index) == 1) {
// Check if we have this evdev device. If so, disconnect it.
if (index < _evdev_devices.size()) {
PT(InputDevice) device = _evdev_devices[index];
if (device != NULL) {
_evdev_devices[index] = NULL;
_inactive_devices.remove_device(device);
if (_connected_devices.remove_device(device)) {
throw_event("disconnect-device", device.p());
}
if (device_cat.is_info()) {
device_cat.info()
<< "Removed input device " << *device << "\n";
if (device_cat.is_debug()) {
device_cat.debug()
<< "Removed input device " << *device << "\n";
}
}
}
throw_event("disconnect-device", device.p());
}
} else if (event->mask & (IN_CREATE | IN_ATTRIB)) {
// The device was created, or it was chmodded to be accessible.
DevicesByPath::iterator it = _devices_by_path.find(name);
if (it == _devices_by_path.end()) {
// We don't know about this device yet.
if (!consider_add_linux_device(name) && (event->mask & IN_CREATE) != 0) {
if (device_cat.is_debug()) {
device_cat.debug()
<< "Ignoring input device /dev/input/" << name << ": "
<< strerror(errno) << "\n";
}
// The device was created, or it was chmodded to be accessible. We
// need to check for the latter since it seems that the device may
// get the IN_CREATE event before the driver gets the permissions
// set properly.
int index = -1;
if (sscanf(event->name, "event%d", &index) == 1) {
InputDevice *device = consider_add_evdev_device(index);
if (device != NULL && device->is_connected()) {
throw_event("connect-device", device);
}
errno = 0;
}
}
@ -301,6 +411,8 @@ update() {
// check if it's connected every so often. Perhaps we can switch to
// using RegisterDeviceNotification in the future.
double time_now = ClockObject::get_global_clock()->get_real_time();
LightMutexHolder holder(_update_lock);
if (time_now - _last_detection > xinput_detection_delay.get_value()) {
// I've heard this can be quite slow if no device is detected. We
// should probably move it to a thread.

View File

@ -36,7 +36,8 @@ private:
~InputDeviceManager();
#ifdef PHAVE_LINUX_INPUT_H
bool consider_add_linux_device(const string &name);
InputDevice *consider_add_evdev_device(int index);
InputDevice *consider_add_js_device(int index);
#endif
public:
@ -59,12 +60,13 @@ private:
#ifdef PHAVE_LINUX_INPUT_H
int _inotify_fd;
typedef pmap<string, InputDevice*> DevicesByPath;
DevicesByPath _devices_by_path;
pvector<InputDevice *> _evdev_devices;
InputDeviceSet _inactive_devices;
#endif
#ifdef _WIN32
// There are always exactly four of these in existence.
LightMutex _update_lock;
XInputDevice _xinput_device0;
XInputDevice _xinput_device1;
XInputDevice _xinput_device2;

View File

@ -30,14 +30,14 @@ TypeHandle LinuxJoystickDevice::_type_handle;
// device using the given device filename.
////////////////////////////////////////////////////////////////////
LinuxJoystickDevice::
LinuxJoystickDevice(const string &device) :
LinuxJoystickDevice(int index) :
_fd(-1),
_device(device)
_index(index)
{
LightMutexHolder holder(_lock);
if (!open_device()) {
device_cat.error()
<< "Could not open joystick device " << _device
<< "Could not open joystick device /dev/input/js" << index
<< ": " << strerror(errno) << "\n";
}
}
@ -55,6 +55,18 @@ LinuxJoystickDevice::
}
}
////////////////////////////////////////////////////////////////////
// Function: LinuxJoystickDevice::check_events
// Access: Public
// Description: Returns true if there are pending events.
////////////////////////////////////////////////////////////////////
bool LinuxJoystickDevice::
check_events() const {
unsigned int avail = 0;
ioctl(_fd, FIONREAD, &avail);
return (avail != 0);
}
////////////////////////////////////////////////////////////////////
// Function: LinuxJoystickDevice::do_poll
// Access: Private, Virtual
@ -66,10 +78,15 @@ LinuxJoystickDevice::
////////////////////////////////////////////////////////////////////
void LinuxJoystickDevice::
do_poll() {
if (_fd != -1) {
if (_fd != -1 && process_events()) {
while (process_events()) {}
} else {
open_device();
// If we got events, we are obviously connected. Mark us so.
if (!_is_connected) {
_is_connected = true;
InputDeviceManager *mgr = InputDeviceManager::get_global_ptr();
mgr->add_device(this);
}
}
}
@ -84,14 +101,18 @@ bool LinuxJoystickDevice::
open_device() {
nassertr(_lock.debug_is_locked(), false);
_fd = open(_device.c_str(), O_RDONLY | O_NONBLOCK);
char path[64];
sprintf(path, "/dev/input/js%d", _index);
_fd = open(path, O_RDONLY | O_NONBLOCK);
if (_fd == -1) {
_is_connected = false;
return false;
}
// Read the name from the device.
// Read the name from the device. We'll later try to use sysfs to read
// the proper product name from the device, but this is a good fallback.
char name[128];
name[0] = 0;
ioctl(_fd, JSIOCGNAME(sizeof(name)), name);
@ -249,16 +270,76 @@ open_device() {
}
}
// Get additional information from sysfs.
sprintf(path, "/sys/class/input/js%d/device/id/vendor", _index);
FILE *f = fopen(path, "r");
if (f) {
fscanf(f, "%hx", &_vendor_id);
fclose(f);
}
sprintf(path, "/sys/class/input/js%d/device/id/product", _index);
f = fopen(path, "r");
if (f) {
fscanf(f, "%hx", &_product_id);
fclose(f);
}
char buffer[256];
sprintf(path, "/sys/class/input/js%d/device/device/../product", _index);
f = fopen(path, "r");
if (f) {
fgets(buffer, sizeof(buffer), f);
buffer[strcspn(buffer, "\r\n")] = 0;
if (buffer[0] != 0) {
_name.assign(buffer);
}
fclose(f);
}
sprintf(path, "/sys/class/input/js%d/device/device/../manufacturer", _index);
f = fopen(path, "r");
if (f) {
fgets(buffer, sizeof(buffer), f);
buffer[strcspn(buffer, "\r\n")] = 0;
_manufacturer.assign(buffer);
fclose(f);
}
sprintf(path, "/sys/class/input/js%d/device/device/../serial", _index);
f = fopen(path, "r");
if (f) {
fgets(buffer, sizeof(buffer), f);
buffer[strcspn(buffer, "\r\n")] = 0;
_serial_number.assign(buffer);
fclose(f);
}
// Read the init events.
_is_connected = true;
while (process_events()) {};
// Special case handling for the wireless Xbox receiver - the Linux
// joystick API doesn't seem to have a way to report whether the device
// is actually turned on. The best we can do is check whether the axes
// are all 0, which indicates that the driver hasn't received any data for
// this gamepad yet (which means it hasn't been plugged in for this session)
if (strncmp(name, "Xbox 360 Wireless Receiver", 26) == 0) {
for (int i = 0; i < _controls.size(); ++i) {
if (_controls[i]._state != 0.0) {
_is_connected = true;
return true;
}
}
_is_connected = false;
} else {
_is_connected = true;
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: LinuxJoystickDevice::process_events
// Access: Private
// Description: Reads a number of events from the joystick.
// Description: Reads a number of events from the device. Returns
// true if events were read, meaning this function
// should keep being called until it returns false.
////////////////////////////////////////////////////////////////////
bool LinuxJoystickDevice::
process_events() {
@ -271,10 +352,12 @@ process_events() {
// No data available for now.
} else if (errno == ENODEV) {
// The device ceased to exist, so we better close it.
// The device ceased to exist, so we better close it. No need
// to worry about removing it from the InputDeviceManager, as it
// will get an inotify event sooner or later about this.
close(_fd);
_fd = -1;
_is_connected = false;
//_is_connected = false;
errno = 0;
} else {
@ -301,6 +384,8 @@ process_events() {
if (axis == C_left_trigger || axis == C_right_trigger || axis == C_trigger) {
// We'd like to use 0.0 to indicate the resting position.
set_control_state(index, (events[i].value + 32767) / 65534.0);
} else if (axis == C_left_y || axis == C_right_y || axis == C_y) {
set_control_state(index, events[i].value / -32767.0);
} else {
set_control_state(index, events[i].value / 32767.0);
}

View File

@ -25,10 +25,12 @@
// /dev/input/js# API to read data from a game controller.
////////////////////////////////////////////////////////////////////
class EXPCL_PANDA_DEVICE LinuxJoystickDevice : public InputDevice {
PUBLISHED:
LinuxJoystickDevice(const string &device);
public:
LinuxJoystickDevice(int index);
virtual ~LinuxJoystickDevice();
bool check_events() const;
private:
virtual void do_poll();
@ -37,7 +39,7 @@ private:
private:
int _fd;
string _device;
int _index;
public:
static TypeHandle get_class_type() {