瀏覽代碼

Robustify Linux gamepad support

rdb 10 年之前
父節點
當前提交
ab66108661

+ 154 - 42
panda/src/device/evdevInputDevice.cxx

@@ -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);
 
-  _name = ((string)name) + "." + uniq;
+  struct input_id id;
+  if (ioctl(_fd, EVIOCGID, &id) >= 0) {
+    _vendor_id = id.vendor;
+    _product_id = id.product;
+  }
 
+  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;
 

+ 11 - 3
panda/src/device/evdevInputDevice.h

@@ -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);
 

+ 38 - 1
panda/src/device/inputDevice.I

@@ -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

+ 14 - 2
panda/src/device/inputDevice.h

@@ -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;

+ 182 - 70
panda/src/device/inputDeviceManager.cxx

@@ -39,44 +39,46 @@ InputDeviceManager *InputDeviceManager::_global_ptr = NULL;
 #ifdef PHAVE_LINUX_INPUT_H
 InputDeviceManager::
 InputDeviceManager() : _inotify_fd(-1) {
+  // Use inotify to watch /dev/input for hotplugging of devices.
+  _inotify_fd = inotify_init1(O_NONBLOCK);
+
+  if (_inotify_fd < 0) {
+    device_cat.error()
+      << "Error initializing inotify: " << strerror(errno) << "\n";
+
+  } else if (inotify_add_watch(_inotify_fd, "/dev/input", IN_CREATE | IN_ATTRIB | IN_DELETE) < 0) {
+    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) {
-      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;
-        }
+      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;
   }
-
-  // Use inotify to watch /dev/input for hotplugging of devices.
-  _inotify_fd = inotify_init1(O_NONBLOCK);
-
-  if (_inotify_fd < 0) {
-    device_cat.error()
-      << "Error initializing inotify: " << strerror(errno) << "\n";
-
-  } else if (inotify_add_watch(_inotify_fd, "/dev/input", IN_CREATE | IN_ATTRIB | IN_DELETE) < 0) {
-    device_cat.error()
-      << "Error adding inotify watch on /dev/input: " << strerror(errno) << "\n";
-  }
 }
 #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);
+  }
+
+  // Check if we can directly read the event device.
+  char path[64];
+  sprintf(path, "/dev/input/event%d", ev_index);
+
+  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";
+    }
+
+    _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];
   }
 
-  if (_devices_by_path.count(name)) {
-    // We already have this device.
-    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;
   }
 
-  // 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";
+  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();
 
-    _devices_by_path[name] = device;
-    _connected_devices.add_device(MOVE(device));
-    return true;
+    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 true;
+  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);
-
-        if (device_cat.is_info()) {
-          device_cat.info()
-            << "Removed input device " << *device << "\n";
+      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_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.

+ 5 - 3
panda/src/device/inputDeviceManager.h

@@ -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;

+ 97 - 12
panda/src/device/linuxJoystickDevice.cxx

@@ -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);
       }

+ 5 - 3
panda/src/device/linuxJoystickDevice.h

@@ -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() {