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
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 */
97InputDevice *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 */
184InputDevice *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 */
213bool 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 */
262void 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
Manages a list of InputDevice objects, as returned by various InputDeviceManager methods.
size_t size() const
Returns the number of devices in the collection.
This is a structure representing a single input device.
Definition inputDevice.h:53
is_connected
Returns true if the device is still connected and able to receive data, false otherwise.
get_product_id
Returns a string containing the USB product ID of the device, if this information is known.
get_vendor_id
Returns a string containing the USB vendor ID of the device, if this information is known.
void set_connected(bool connected)
Called to indicate that the device has been disconnected or connected from its host.
void poll()
Polls the input device for new activity, to ensure it contains the latest events.
Similar to MutexHolder, but for a light mutex.
bool is_of_type(TypeHandle handle) const
Returns true if the current object is or derives from the indicated type.
Definition typedObject.I:28
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.