443 lines
12 KiB
C++
443 lines
12 KiB
C++
/**
|
|
* PANDA 3D SOFTWARE
|
|
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
|
*
|
|
* All use of this software is subject to the terms of the revised BSD
|
|
* license. You should have received a copy of this license along
|
|
* with this source code in a file named "LICENSE."
|
|
*
|
|
* @file linuxJoystickDevice.cxx
|
|
* @author rdb
|
|
* @date 2015-08-21
|
|
*/
|
|
|
|
#include "linuxJoystickDevice.h"
|
|
#include "evdevInputDevice.h"
|
|
|
|
#ifdef PHAVE_LINUX_INPUT_H
|
|
|
|
#include "gamepadButton.h"
|
|
#include "linuxInputDeviceManager.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <linux/joystick.h>
|
|
|
|
TypeHandle LinuxJoystickDevice::_type_handle;
|
|
|
|
/**
|
|
* Creates a new device using the Linux joystick device with the given index.
|
|
*/
|
|
LinuxJoystickDevice::
|
|
LinuxJoystickDevice(LinuxInputDeviceManager *manager, size_t index) :
|
|
_manager(manager),
|
|
_fd(-1),
|
|
_index(index),
|
|
_dpad_x_axis(-1),
|
|
_dpad_y_axis(-1),
|
|
_dpad_left_button(-1),
|
|
_dpad_up_button(-1),
|
|
_ltrigger_button(-1),
|
|
_rtrigger_button(-1)
|
|
{
|
|
LightMutexHolder holder(_lock);
|
|
if (!open_device()) {
|
|
device_cat.error()
|
|
<< "Could not open joystick device /dev/input/js" << index
|
|
<< ": " << strerror(errno) << "\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
LinuxJoystickDevice::
|
|
~LinuxJoystickDevice() {
|
|
if (_fd != -1) {
|
|
close(_fd);
|
|
_fd = -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if there are pending events.
|
|
*/
|
|
bool LinuxJoystickDevice::
|
|
check_events() const {
|
|
unsigned int avail = 0;
|
|
ioctl(_fd, FIONREAD, &avail);
|
|
return (avail != 0);
|
|
}
|
|
|
|
/**
|
|
* Polls the input device for new activity, to ensure it contains the latest
|
|
* events. This will only have any effect for some types of input devices;
|
|
* others may be updated automatically, and this method will be a no-op.
|
|
*/
|
|
void LinuxJoystickDevice::
|
|
do_poll() {
|
|
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;
|
|
if (_manager != nullptr) {
|
|
_manager->add_device(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Opens or reopens the joystick device, and reads out the button and axis
|
|
* mappings. Assumes the lock is already held.
|
|
*/
|
|
bool LinuxJoystickDevice::
|
|
open_device() {
|
|
nassertr(_lock.debug_is_locked(), false);
|
|
|
|
char path[64];
|
|
sprintf(path, "/dev/input/js%zd", _index);
|
|
|
|
_fd = open(path, O_RDONLY | O_NONBLOCK);
|
|
|
|
if (_fd == -1) {
|
|
_is_connected = false;
|
|
return false;
|
|
}
|
|
|
|
// 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);
|
|
_name = name;
|
|
|
|
bool emulate_dpad = true;
|
|
bool have_analog_triggers = false;
|
|
|
|
// Get the number of axes.
|
|
uint8_t num_axes = 0, num_buttons = 0;
|
|
ioctl(_fd, JSIOCGAXES, &num_axes);
|
|
ioctl(_fd, JSIOCGBUTTONS, &num_buttons);
|
|
|
|
_buttons.resize(num_buttons);
|
|
_axes.resize(num_axes);
|
|
|
|
if (num_buttons > 0) {
|
|
uint16_t btnmap[512];
|
|
ioctl(_fd, JSIOCGBTNMAP, btnmap);
|
|
|
|
for (uint8_t i = 0; i < num_buttons; ++i) {
|
|
ButtonHandle handle = EvdevInputDevice::map_button(btnmap[i]);
|
|
if (handle == ButtonHandle::none()) {
|
|
if (device_cat.is_debug()) {
|
|
device_cat.debug() << "Unmapped /dev/input/js" << _index
|
|
<< " button " << (int)i << ": 0x" << std::hex << btnmap[i] << "\n";
|
|
}
|
|
} else if (handle == GamepadButton::face_a()) {
|
|
_device_class = DeviceClass::GAMEPAD;
|
|
} else if (handle == GamepadButton::trigger()) {
|
|
_device_class = DeviceClass::FLIGHT_STICK;
|
|
} else if (handle == GamepadButton::dpad_left()) {
|
|
emulate_dpad = false;
|
|
} else if (handle == GamepadButton::ltrigger()) {
|
|
_ltrigger_button = i;
|
|
} else if (handle == GamepadButton::rtrigger()) {
|
|
_rtrigger_button = i;
|
|
}
|
|
_buttons[i].handle = handle;
|
|
}
|
|
}
|
|
|
|
if (num_axes > 0) {
|
|
uint8_t axmap[512];
|
|
ioctl(_fd, JSIOCGAXMAP, axmap);
|
|
|
|
for (uint8_t i = 0; i < num_axes; ++i) {
|
|
Axis axis = Axis::NONE;
|
|
|
|
switch (axmap[i]) {
|
|
case ABS_X:
|
|
if (_device_class == DeviceClass::GAMEPAD) {
|
|
axis = Axis::LEFT_X;
|
|
} else if (_device_class == DeviceClass::FLIGHT_STICK) {
|
|
axis = Axis::ROLL;
|
|
} else {
|
|
axis = Axis::X;
|
|
}
|
|
break;
|
|
|
|
case ABS_Y:
|
|
if (_device_class == DeviceClass::GAMEPAD) {
|
|
axis = Axis::LEFT_Y;
|
|
} else if (_device_class == DeviceClass::FLIGHT_STICK) {
|
|
axis = Axis::PITCH;
|
|
} else {
|
|
axis = Axis::Y;
|
|
}
|
|
break;
|
|
|
|
case ABS_Z:
|
|
if (_device_class == DeviceClass::GAMEPAD) {
|
|
axis = Axis::LEFT_TRIGGER;
|
|
} else {
|
|
//axis = Axis::TRIGGER;
|
|
}
|
|
break;
|
|
|
|
case ABS_RX:
|
|
axis = Axis::RIGHT_X;
|
|
break;
|
|
|
|
case ABS_RY:
|
|
axis = Axis::RIGHT_Y;
|
|
break;
|
|
|
|
case ABS_RZ:
|
|
if (_device_class == DeviceClass::GAMEPAD) {
|
|
axis = Axis::RIGHT_TRIGGER;
|
|
} else {
|
|
axis = Axis::YAW;
|
|
}
|
|
break;
|
|
|
|
case ABS_THROTTLE:
|
|
axis = Axis::THROTTLE;
|
|
break;
|
|
|
|
case ABS_RUDDER:
|
|
axis = Axis::RUDDER;
|
|
break;
|
|
|
|
case ABS_WHEEL:
|
|
axis = Axis::WHEEL;
|
|
break;
|
|
|
|
case ABS_GAS:
|
|
axis = Axis::ACCELERATOR;
|
|
break;
|
|
|
|
case ABS_BRAKE:
|
|
axis = Axis::BRAKE;
|
|
break;
|
|
|
|
case ABS_HAT0X:
|
|
if (emulate_dpad) {
|
|
// Emulate D-Pad or hat switch.
|
|
_dpad_x_axis = i;
|
|
_dpad_left_button = (int)_buttons.size();
|
|
if (_device_class == DeviceClass::GAMEPAD) {
|
|
add_button(GamepadButton::dpad_left());
|
|
add_button(GamepadButton::dpad_right());
|
|
} else {
|
|
add_button(GamepadButton::hat_left());
|
|
add_button(GamepadButton::hat_right());
|
|
}
|
|
_buttons[_dpad_left_button]._state = S_up;
|
|
_buttons[_dpad_left_button+1]._state = S_up;
|
|
axis = Axis::NONE;
|
|
}
|
|
break;
|
|
|
|
case ABS_HAT0Y:
|
|
if (emulate_dpad) {
|
|
// Emulate D-Pad.
|
|
_dpad_y_axis = i;
|
|
_dpad_up_button = (int)_buttons.size();
|
|
if (_device_class == DeviceClass::GAMEPAD) {
|
|
add_button(GamepadButton::dpad_up());
|
|
add_button(GamepadButton::dpad_down());
|
|
} else {
|
|
add_button(GamepadButton::hat_up());
|
|
add_button(GamepadButton::hat_down());
|
|
}
|
|
_buttons[_dpad_up_button]._state = S_up;
|
|
_buttons[_dpad_up_button+1]._state = S_up;
|
|
axis = Axis::NONE;
|
|
}
|
|
break;
|
|
|
|
case ABS_HAT2X:
|
|
if (_device_class == DeviceClass::GAMEPAD) {
|
|
axis = Axis::RIGHT_TRIGGER;
|
|
}
|
|
break;
|
|
|
|
case ABS_HAT2Y:
|
|
if (_device_class == DeviceClass::GAMEPAD) {
|
|
axis = Axis::LEFT_TRIGGER;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (device_cat.is_debug()) {
|
|
device_cat.debug() << "Unmapped /dev/input/js" << _index
|
|
<< " axis " << (int)i << ": 0x" << std::hex << (int)axmap[i] << "\n";
|
|
}
|
|
axis = Axis::NONE;
|
|
break;
|
|
}
|
|
_axes[i].axis = axis;
|
|
|
|
if (axis == Axis::LEFT_TRIGGER || axis == Axis::RIGHT_TRIGGER) {
|
|
// We'd like to use 0.0 to indicate the resting position.
|
|
_axes[i]._scale = 1.0 / 65534.0;
|
|
_axes[i]._bias = 0.5;
|
|
have_analog_triggers = true;
|
|
} else if (axis == Axis::LEFT_Y || axis == Axis::RIGHT_Y || axis == Axis::Y) {
|
|
_axes[i]._scale = 1.0 / -32767.0;
|
|
_axes[i]._bias = 0.0;
|
|
} else {
|
|
_axes[i]._scale = 1.0 / 32767.0;
|
|
_axes[i]._bias = 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_ltrigger_button >= 0 && _rtrigger_button >= 0 && !have_analog_triggers) {
|
|
// Emulate analog triggers.
|
|
_ltrigger_axis = (int)_axes.size();
|
|
add_axis(Axis::LEFT_TRIGGER, 0, 1, false);
|
|
add_axis(Axis::RIGHT_TRIGGER, 0, 1, false);
|
|
} else {
|
|
_ltrigger_button = -1;
|
|
_rtrigger_button = -1;
|
|
}
|
|
|
|
// Get additional information from sysfs.
|
|
sprintf(path, "/sys/class/input/js%zd/device/id/vendor", _index);
|
|
FILE *f = fopen(path, "r");
|
|
if (f) {
|
|
if (fscanf(f, "%hx", &_vendor_id) < 1) {
|
|
_vendor_id = 0;
|
|
}
|
|
fclose(f);
|
|
}
|
|
sprintf(path, "/sys/class/input/js%zd/device/id/product", _index);
|
|
f = fopen(path, "r");
|
|
if (f) {
|
|
if (fscanf(f, "%hx", &_product_id) < 1) {
|
|
_product_id = 0;
|
|
}
|
|
fclose(f);
|
|
}
|
|
char buffer[256];
|
|
sprintf(path, "/sys/class/input/js%zd/device/device/../product", _index);
|
|
f = fopen(path, "r");
|
|
if (f) {
|
|
if (fgets(buffer, sizeof(buffer), f) != nullptr) {
|
|
buffer[strcspn(buffer, "\r\n")] = 0;
|
|
if (buffer[0] != 0) {
|
|
_name.assign(buffer);
|
|
}
|
|
}
|
|
fclose(f);
|
|
}
|
|
sprintf(path, "/sys/class/input/js%zd/device/device/../manufacturer", _index);
|
|
f = fopen(path, "r");
|
|
if (f) {
|
|
if (fgets(buffer, sizeof(buffer), f) != nullptr) {
|
|
buffer[strcspn(buffer, "\r\n")] = 0;
|
|
_manufacturer.assign(buffer);
|
|
}
|
|
fclose(f);
|
|
}
|
|
sprintf(path, "/sys/class/input/js%zd/device/device/../serial", _index);
|
|
f = fopen(path, "r");
|
|
if (f) {
|
|
if (fgets(buffer, sizeof(buffer), f) != nullptr) {
|
|
buffer[strcspn(buffer, "\r\n")] = 0;
|
|
_serial_number.assign(buffer);
|
|
}
|
|
fclose(f);
|
|
}
|
|
|
|
// Read the init events.
|
|
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 (const auto &control : _axes) {
|
|
if (control.value != 0.0) {
|
|
_is_connected = true;
|
|
return true;
|
|
}
|
|
}
|
|
_is_connected = false;
|
|
} else {
|
|
_is_connected = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
// Read 8 events at a time.
|
|
struct js_event events[8];
|
|
|
|
int n_read = read(_fd, events, sizeof(events));
|
|
if (n_read < 0) {
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
// No data available for now.
|
|
|
|
} else if (errno == ENODEV) {
|
|
// 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;
|
|
errno = 0;
|
|
|
|
} else {
|
|
device_cat.error() << "read: " << strerror(errno) << "\n";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (n_read == 0) {
|
|
return false;
|
|
}
|
|
|
|
n_read /= sizeof(struct js_event);
|
|
|
|
for (int i = 0; i < n_read; ++i) {
|
|
int index = events[i].number;
|
|
|
|
if (events[i].type & JS_EVENT_BUTTON) {
|
|
if (index == _ltrigger_button) {
|
|
axis_changed(_ltrigger_axis, events[i].value);
|
|
} else if (index == _rtrigger_button) {
|
|
axis_changed(_ltrigger_axis + 1, events[i].value);
|
|
}
|
|
button_changed(index, (events[i].value != 0));
|
|
|
|
} else if (events[i].type & JS_EVENT_AXIS) {
|
|
if (index == _dpad_x_axis) {
|
|
button_changed(_dpad_left_button, events[i].value < -1000);
|
|
button_changed(_dpad_left_button+1, events[i].value > 1000);
|
|
} else if (index == _dpad_y_axis) {
|
|
button_changed(_dpad_up_button, events[i].value < -1000);
|
|
button_changed(_dpad_up_button+1, events[i].value > 1000);
|
|
}
|
|
|
|
axis_changed(index, events[i].value);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif
|