Panda3D

Loading...
Searching...
No Matches
linuxInputDeviceManager.cxx
Go to the documentation of this file.
1/**
2 * PANDA 3D SOFTWARE
3 * Copyright (c) Carnegie Mellon University. All rights reserved.
4 *
5 * All use of this software is subject to the terms of the revised BSD
6 * license. You should have received a copy of this license along
7 * with this source code in a file named "LICENSE."
8 *
9 * @file linuxInputDeviceManager.cxx
10 * @author rdb
11 * @date 2018-01-25
12 */
13
14#include "linuxInputDeviceManager.h"
15#include "throw_event.h"
16
17#ifdef PHAVE_LINUX_INPUT_H
18
19#include "evdevInputDevice.h"
20#include "linuxJoystickDevice.h"
21
22#include <dirent.h>
23#include <fcntl.h>
24#include <sys/inotify.h>
25#include <sys/ioctl.h>
26
27/**
28 * Initializes the device manager by scanning which devices are currently
29 * connected and setting up any platform-dependent structures necessary for
30 * listening for future device connect events.
31 */
32LinuxInputDeviceManager::
33LinuxInputDeviceManager() {
34 // Use inotify to watch /dev/input for hotplugging of devices.
35 _inotify_fd = inotify_init();
36 fcntl(_inotify_fd, F_SETFL, O_NONBLOCK);
37 fcntl(_inotify_fd, F_SETFD, FD_CLOEXEC);
38
39 if (_inotify_fd < 0) {
40 device_cat.error()
41 << "Error initializing inotify: " << strerror(errno) << "\n";
42
43 } else if (inotify_add_watch(_inotify_fd, "/dev/input", IN_CREATE | IN_ATTRIB | IN_DELETE) < 0) {
44 device_cat.error()
45 << "Error adding inotify watch on /dev/input: " << strerror(errno) << "\n";
46 }
47
48 // Scan /dev/input for a list of input devices.
49 DIR *dir = opendir("/dev/input");
50 if (dir) {
51 std::vector<size_t> indices;
52 dirent *entry = readdir(dir);
53 while (entry != nullptr) {
54 size_t index;
55 if (entry->d_type == DT_CHR && sscanf(entry->d_name, "event%zd", &index) == 1) {
56 indices.push_back(index);
57 }
58 entry = readdir(dir);
59 }
60 closedir(dir);
61
62 // We'll want to sort the devices by index, since the order may be
63 // meaningful (eg. for the Xbox wireless receiver).
64 if (indices.empty()) {
65 return;
66 }
67 std::sort(indices.begin(), indices.end());
68 _evdev_devices.resize(indices.back() + 1, nullptr);
69
70 for (size_t index : indices) {
71 consider_add_evdev_device(index);
72 }
73 } else {
74 device_cat.error()
75 << "Error opening directory /dev/input: " << strerror(errno) << "\n";
76 return;
77 }
78}
79
80/**
81 * Closes any resources that the device manager was using to listen for events.
82 */
83LinuxInputDeviceManager::
84~LinuxInputDeviceManager() {
85 if (_inotify_fd >= 0) {
86 close(_inotify_fd);
87 _inotify_fd = -1;
88 }
89}
90
91/**
92 * Checks whether the event device with the given index is accessible, and if
93 * so, adds it. Returns the device if it was newly connected.
94 *
95 * This is the preferred interface for input devices on Linux.
96 */
97 InputDevice *LinuxInputDeviceManager::
98consider_add_evdev_device(size_t ev_index) {
99 if (ev_index < _evdev_devices.size()) {
100 if (_evdev_devices[ev_index] != nullptr) {
101 // We already have this device. FIXME: probe it and add it to the
102 // list of connected devices?
103 return nullptr;
104 }
105 } else {
106 // Make room to store this index.
107 _evdev_devices.resize(ev_index + 1, nullptr);
108 }
109
110 // Check if we can directly read the event device.
111 char path[64];
112 sprintf(path, "/dev/input/event%zd", ev_index);
113
114 if (access(path, R_OK) == 0) {
115 PT(InputDevice) device = new EvdevInputDevice(this, ev_index);
116 if (device_cat.is_debug()) {
117 device_cat.debug()
118 << "Discovered evdev input device " << *device << "\n";
119 }
120
121 _evdev_devices[ev_index] = device;
122
123 if (device->is_connected()) {
124 _connected_devices.add_device(std::move(device));
125 } else {
126 // Wait for activity on the device before it is considered connected.
127 _inactive_devices.add_device(std::move(device));
128 }
129 return _evdev_devices[ev_index];
130 }
131
132 // Nope. The permissions haven't been configured to allow it. Check if this
133 // corresponds to a /dev/input/jsX interface, which has a better chance of
134 // having read permissions set, but doesn't export all of the features
135 // (notably, force feedback).
136
137#ifndef __FreeBSD__
138 // We do this by checking for a js# directory inside the sysfs directory.
139 sprintf(path, "/sys/class/input/event%zd/device", ev_index);
140
141 DIR *dir = opendir(path);
142 if (dir == nullptr) {
143 if (device_cat.is_debug()) {
144 device_cat.debug()
145 << "Error opening directory " << path << ": " << strerror(errno) << "\n";
146 }
147 return nullptr;
148 }
149
150 dirent *entry = readdir(dir);
151 while (entry != nullptr) {
152 size_t js_index;
153 if (sscanf(entry->d_name, "js%zd", &js_index) == 1) {
154 // Yes, we fonud a corresponding js device. Try adding it.
155 closedir(dir);
156
157 InputDevice *device = consider_add_js_device(js_index);
158 if (device != nullptr && device_cat.is_warning()) {
159 // This seemed to work. Display a warning to the user indicating
160 // that they might want to configure udev properly.
161 device_cat.warning()
162 << "/dev/input/event" << ev_index << " is not readable, some "
163 "features will be unavailable.\n";
164 }
165 _evdev_devices[ev_index] = device;
166 return device;
167 }
168 entry = readdir(dir);
169 }
170
171 closedir(dir);
172#endif
173
174 return nullptr;
175}
176
177/**
178 * Checks whether the joystick device with the given index is accessible, and
179 * if so, adds it. Returns the device if it was newly connected.
180 *
181 * This is only used on Linux as a fallback interface for when an evdev device
182 * cannot be accessed.
183 */
184 InputDevice *LinuxInputDeviceManager::
185consider_add_js_device(size_t js_index) {
186 char path[64];
187 sprintf(path, "/dev/input/js%zd", js_index);
188
189 if (access(path, R_OK) == 0) {
190 PT(LinuxJoystickDevice) device = new LinuxJoystickDevice(this, js_index);
191 if (device_cat.is_debug()) {
192 device_cat.debug()
193 << "Discovered joydev input device " << *device << "\n";
194 }
195 InputDevice *device_p = device.p();
196
197 if (device->is_connected()) {
198 _connected_devices.add_device(std::move(device));
199 } else {
200 // Wait for activity on the device before it is considered connected.
201 _inactive_devices.add_device(std::move(device));
202 }
203 return device_p;
204 }
205
206 return nullptr;
207}
208
209/**
210 * Scans the "virtual" input devices on the system to check whether one with
211 * the given vendor and product ID exists.
212 */
213 bool LinuxInputDeviceManager::
214has_virtual_device(unsigned short vendor_id, unsigned short product_id) const {
215 char path[294];
216 sprintf(path, "/sys/devices/virtual/input");
217
218 DIR *dir = opendir(path);
219 if (dir != nullptr) {
220 dirent *entry = readdir(dir);
221 while (entry != nullptr) {
222 if (entry->d_name[0] != 'i') {
223 entry = readdir(dir);
224 continue;
225 }
226 FILE *f;
227
228 char vendor[5] = {0};
229 sprintf(path, "/sys/devices/virtual/input/%s/id/vendor", entry->d_name);
230 f = fopen(path, "r");
231 if (f) {
232 fgets(vendor, sizeof(vendor), f);
233 fclose(f);
234 }
235
236 char product[5] = {0};
237 sprintf(path, "/sys/devices/virtual/input/%s/id/product", entry->d_name);
238 f = fopen(path, "r");
239 if (f) {
240 fgets(product, sizeof(product), f);
241 fclose(f);
242 }
243
244 if (vendor[0] && std::stoi(std::string(vendor), nullptr, 16) == (int)vendor_id &&
245 product[0] && std::stoi(std::string(product), nullptr, 16) == (int)product_id) {
246 closedir(dir);
247 return true;
248 }
249
250 entry = readdir(dir);
251 }
252 closedir(dir);
253 }
254
255 return false;
256}
257
258/**
259 * Polls the system to see if there are any new devices. In some
260 * implementations this is a no-op.
261 */
262 void LinuxInputDeviceManager::
263update() {
264 // Check for any devices that may be disconnected and need to be probed in
265 // order to see whether they have been reconnected.
266 InputDeviceSet inactive_devices;
267 {
268 LightMutexHolder holder(_lock);
269 inactive_devices = _inactive_devices;
270 }
271 for (size_t i = 0; i < inactive_devices.size(); ++i) {
272 InputDevice *device = inactive_devices[i];
273 if (device != nullptr && !device->is_connected()) {
274 device->poll();
275 }
276 }
277
278 // We use inotify to tell us whether a device was added, removed, or has
279 // changed permissions to allow us to access it.
280 unsigned int avail = 0;
281 ioctl(_inotify_fd, FIONREAD, &avail);
282 if (avail == 0) {
283 return;
284 }
285
286 char buffer[avail];
287 int n_read = read(_inotify_fd, buffer, avail);
288 if (n_read < 0) {
289 if (errno == EAGAIN || errno == EWOULDBLOCK) {
290 // No data available for now.
291
292 } else {
293 device_cat.error() << "read: " << strerror(errno) << "\n";
294 }
295 return;
296 }
297
298 LightMutexHolder holder(_lock);
299
300 // Iterate over the events in the buffer.
301 bool removed_steam_virtual_device = false;
302 char *ptr = buffer;
303 char *end = buffer + avail;
304 while (ptr < end) {
305 inotify_event *event = (inotify_event *)ptr;
306
307 std::string name(event->name);
308
309 if (event->mask & IN_DELETE) {
310 // The device was deleted. If we have it, remove it.
311
312 size_t index;
313 if (sscanf(event->name, "event%zd", &index) == 1) {
314 // Check if we have this evdev device. If so, disconnect it.
315 if (index < _evdev_devices.size()) {
316 PT(InputDevice) device = _evdev_devices[index];
317 if (device != nullptr) {
318 device->set_connected(false);
319 _evdev_devices[index] = nullptr;
320 _inactive_devices.remove_device(device);
321 if (_connected_devices.remove_device(device)) {
322 throw_event("disconnect-device", device.p());
323 }
324
325 if (device_cat.is_debug()) {
326 device_cat.debug()
327 << "Removed input device " << *device << "\n";
328 }
329
330 // Check for Steam virtual device; see comment below.
331 if (device->get_vendor_id() == 0x28de &&
332 device->get_product_id() == 0x11ff) {
333 removed_steam_virtual_device = true;
334 }
335 }
336 }
337 }
338
339 } else if (event->mask & (IN_CREATE | IN_ATTRIB)) {
340 // The device was created, or it was chmodded to be accessible. We need
341 // to check for the latter since it seems that the device may get the
342 // IN_CREATE event before the driver gets the permissions set properly.
343
344 size_t index;
345 if (sscanf(event->name, "event%zd", &index) == 1) {
346 InputDevice *device = consider_add_evdev_device(index);
347 if (device != nullptr && device->is_connected()) {
348 throw_event("connect-device", device);
349 }
350 }
351 }
352
353 ptr += sizeof(inotify_event) + event->len;
354 }
355
356 // If the Steam virtual device was just disconnected, the user may have just
357 // shut down Steam, and we need to reactivate the real Steam Controller
358 // device that was previously suppressed by Steam.
359 if (removed_steam_virtual_device) {
360 inactive_devices = _inactive_devices;
361
362 for (size_t i = 0; i < inactive_devices.size(); ++i) {
363 InputDevice *device = inactive_devices[i];
364 if (device != nullptr && device->is_of_type(EvdevInputDevice::get_class_type())) {
365 PT(EvdevInputDevice) evdev_device = (EvdevInputDevice *)device;
366 if (evdev_device->reactivate_steam_controller()) {
367 _inactive_devices.remove_device(device);
368 _connected_devices.add_device(device);
369 throw_event("connect-device", device);
370 }
371 }
372 }
373 }
374}
375
376#endif // PHAVE_LINUX_INPUT_H
InputDeviceSet
Manages a list of InputDevice objects, as returned by various InputDeviceManager methods.
Definition inputDeviceSet.h:26
InputDeviceSet::size
size_t size() const
Returns the number of devices in the collection.
Definition inputDeviceSet.I:34
InputDevice
This is a structure representing a single input device.
Definition inputDevice.h:53
InputDevice::is_connected
is_connected
Returns true if the device is still connected and able to receive data, false otherwise.
Definition inputDevice.h:244
InputDevice::get_product_id
get_product_id
Returns a string containing the USB product ID of the device, if this information is known.
Definition inputDevice.h:240
InputDevice::get_vendor_id
get_vendor_id
Returns a string containing the USB vendor ID of the device, if this information is known.
Definition inputDevice.h:237
InputDevice::set_connected
void set_connected(bool connected)
Called to indicate that the device has been disconnected or connected from its host.
Definition inputDevice.I:383
InputDevice::poll
void poll()
Polls the input device for new activity, to ensure it contains the latest events.
Definition inputDevice.cxx:48
LightMutexHolder
Similar to MutexHolder, but for a light mutex.
Definition lightMutexHolder.h:25
TypedObject::is_of_type
bool is_of_type(TypeHandle handle) const
Returns true if the current object is or derives from the indicated type.
Definition typedObject.I:28
evdevInputDevice.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
linuxInputDeviceManager.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
linuxJoystickDevice.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
throw_event.h
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.