450 lines
11 KiB
C++
450 lines
11 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"
|
|
|
|
#ifdef PHAVE_LINUX_INPUT_H
|
|
|
|
#include "gamepadButton.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(int index) :
|
|
_fd(-1),
|
|
_index(index),
|
|
_dpad_x_axis(-1),
|
|
_dpad_y_axis(-1),
|
|
_dpad_left_button(-1),
|
|
_dpad_up_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;
|
|
InputDeviceManager *mgr = InputDeviceManager::get_global_ptr();
|
|
mgr->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%d", _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;
|
|
|
|
// 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);
|
|
_controls.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 = ButtonHandle::none();
|
|
switch (btnmap[i]) {
|
|
case BTN_A:
|
|
handle = GamepadButton::action_a();
|
|
_device_class = DC_gamepad;
|
|
break;
|
|
|
|
case BTN_B:
|
|
handle = GamepadButton::action_b();
|
|
break;
|
|
|
|
case BTN_C:
|
|
handle = GamepadButton::action_c();
|
|
break;
|
|
|
|
case BTN_X:
|
|
handle = GamepadButton::action_x();
|
|
break;
|
|
|
|
case BTN_Y:
|
|
handle = GamepadButton::action_y();
|
|
break;
|
|
|
|
case BTN_Z:
|
|
handle = GamepadButton::action_z();
|
|
break;
|
|
|
|
case BTN_TL:
|
|
handle = GamepadButton::lshoulder();
|
|
break;
|
|
|
|
case BTN_TR:
|
|
handle = GamepadButton::rshoulder();
|
|
break;
|
|
|
|
case BTN_TL2:
|
|
handle = GamepadButton::ltrigger();
|
|
break;
|
|
|
|
case BTN_TR2:
|
|
handle = GamepadButton::rtrigger();
|
|
break;
|
|
|
|
case BTN_SELECT:
|
|
handle = GamepadButton::back();
|
|
break;
|
|
|
|
case BTN_START:
|
|
handle = GamepadButton::start();
|
|
break;
|
|
|
|
case BTN_MODE:
|
|
handle = GamepadButton::guide();
|
|
break;
|
|
|
|
case BTN_THUMBL:
|
|
handle = GamepadButton::lstick();
|
|
break;
|
|
|
|
case BTN_THUMBR:
|
|
handle = GamepadButton::rstick();
|
|
break;
|
|
|
|
case BTN_TRIGGER_HAPPY1:
|
|
handle = GamepadButton::dpad_left();
|
|
_dpad_left_button = i;
|
|
break;
|
|
|
|
case BTN_TRIGGER_HAPPY2:
|
|
handle = GamepadButton::dpad_right();
|
|
break;
|
|
|
|
case BTN_TRIGGER_HAPPY3:
|
|
handle = GamepadButton::dpad_up();
|
|
_dpad_up_button = i;
|
|
break;
|
|
|
|
case BTN_TRIGGER_HAPPY4:
|
|
handle = GamepadButton::dpad_down();
|
|
break;
|
|
|
|
default:
|
|
if (device_cat.is_debug()) {
|
|
device_cat.debug() << "Unmapped /dev/input/js" << _index
|
|
<< " button " << (int)i << ": 0x" << hex << btnmap[i] << "\n";
|
|
}
|
|
handle = ButtonHandle::none();
|
|
break;
|
|
}
|
|
_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) {
|
|
ControlAxis axis = C_none;
|
|
|
|
switch (axmap[i]) {
|
|
case ABS_X:
|
|
if (_device_class == DC_gamepad) {
|
|
axis = C_left_x;
|
|
} else {
|
|
axis = C_x;
|
|
}
|
|
break;
|
|
|
|
case ABS_Y:
|
|
if (_device_class == DC_gamepad) {
|
|
axis = C_left_y;
|
|
} else {
|
|
axis = C_y;
|
|
}
|
|
break;
|
|
|
|
case ABS_Z:
|
|
if (_device_class == DC_gamepad) {
|
|
axis = C_left_trigger;
|
|
} else {
|
|
axis = C_trigger;
|
|
}
|
|
break;
|
|
|
|
case ABS_RX:
|
|
axis = C_right_x;
|
|
break;
|
|
|
|
case ABS_RY:
|
|
axis = C_right_y;
|
|
break;
|
|
|
|
case ABS_RZ:
|
|
axis = C_right_trigger;
|
|
break;
|
|
|
|
case ABS_THROTTLE:
|
|
axis = InputDevice::C_throttle;
|
|
break;
|
|
|
|
case ABS_RUDDER:
|
|
axis = InputDevice::C_rudder;
|
|
break;
|
|
|
|
case ABS_WHEEL:
|
|
axis = InputDevice::C_wheel;
|
|
break;
|
|
|
|
case ABS_GAS:
|
|
axis = InputDevice::C_accelerator;
|
|
break;
|
|
|
|
case ABS_BRAKE:
|
|
axis = InputDevice::C_brake;
|
|
break;
|
|
|
|
case ABS_HAT0X:
|
|
if (_dpad_left_button == -1 && _device_class == DC_gamepad) {
|
|
// Emulate D-Pad.
|
|
_dpad_x_axis = i;
|
|
_dpad_left_button = (int)_buttons.size();
|
|
_buttons.push_back(ButtonState(GamepadButton::dpad_left()));
|
|
_buttons.push_back(ButtonState(GamepadButton::dpad_right()));
|
|
axis = C_none;
|
|
} else {
|
|
axis = C_hat_x;
|
|
}
|
|
break;
|
|
|
|
case ABS_HAT0Y:
|
|
if (_dpad_up_button == -1 && _device_class == DC_gamepad) {
|
|
// Emulate D-Pad.
|
|
_dpad_y_axis = i;
|
|
_dpad_up_button = (int)_buttons.size();
|
|
_buttons.push_back(ButtonState(GamepadButton::dpad_up()));
|
|
_buttons.push_back(ButtonState(GamepadButton::dpad_down()));
|
|
axis = C_none;
|
|
} else {
|
|
axis = C_hat_y;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (device_cat.is_debug()) {
|
|
device_cat.debug() << "Unmapped /dev/input/js" << _index
|
|
<< " axis " << (int)i << ": 0x" << hex << (int)axmap[i] << "\n";
|
|
}
|
|
axis = C_none;
|
|
break;
|
|
}
|
|
_controls[i]._axis = axis;
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
set_button_state(index, (events[i].value != 0));
|
|
|
|
} else if (events[i].type & JS_EVENT_AXIS) {
|
|
if (index == _dpad_x_axis) {
|
|
set_button_state(_dpad_left_button, events[i].value < -1000);
|
|
set_button_state(_dpad_left_button+1, events[i].value > 1000);
|
|
} else if (index == _dpad_y_axis) {
|
|
set_button_state(_dpad_up_button, events[i].value < -1000);
|
|
set_button_state(_dpad_up_button+1, events[i].value > 1000);
|
|
}
|
|
|
|
ControlAxis axis = _controls[index]._axis;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif
|