|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 /* |
|
6 * LinuxGamepadService: A Linux backend for the GamepadService. |
|
7 * Derived from the kernel documentation at |
|
8 * http://www.kernel.org/doc/Documentation/input/joystick-api.txt |
|
9 */ |
|
10 #include <algorithm> |
|
11 #include <cstddef> |
|
12 |
|
13 #include <glib.h> |
|
14 #include <linux/joystick.h> |
|
15 #include <stdio.h> |
|
16 #include <stdint.h> |
|
17 #include <sys/ioctl.h> |
|
18 #include <unistd.h> |
|
19 |
|
20 #include "nscore.h" |
|
21 #include "mozilla/dom/GamepadService.h" |
|
22 #include "udev.h" |
|
23 |
|
24 namespace { |
|
25 |
|
26 using mozilla::dom::GamepadService; |
|
27 using mozilla::udev_lib; |
|
28 using mozilla::udev_device; |
|
29 using mozilla::udev_list_entry; |
|
30 using mozilla::udev_enumerate; |
|
31 using mozilla::udev_monitor; |
|
32 |
|
33 static const float kMaxAxisValue = 32767.0; |
|
34 static const char kJoystickPath[] = "/dev/input/js"; |
|
35 |
|
36 //TODO: should find a USB identifier for each device so we can |
|
37 // provide something that persists across connect/disconnect cycles. |
|
38 typedef struct { |
|
39 int index; |
|
40 guint source_id; |
|
41 int numAxes; |
|
42 int numButtons; |
|
43 char idstring[128]; |
|
44 char devpath[PATH_MAX]; |
|
45 } Gamepad; |
|
46 |
|
47 class LinuxGamepadService { |
|
48 public: |
|
49 LinuxGamepadService() : mMonitor(nullptr), |
|
50 mMonitorSourceID(0) { |
|
51 } |
|
52 |
|
53 void Startup(); |
|
54 void Shutdown(); |
|
55 |
|
56 private: |
|
57 void AddDevice(struct udev_device* dev); |
|
58 void RemoveDevice(struct udev_device* dev); |
|
59 void ScanForDevices(); |
|
60 void AddMonitor(); |
|
61 void RemoveMonitor(); |
|
62 bool is_gamepad(struct udev_device* dev); |
|
63 void ReadUdevChange(); |
|
64 |
|
65 // handler for data from /dev/input/jsN |
|
66 static gboolean OnGamepadData(GIOChannel *source, |
|
67 GIOCondition condition, |
|
68 gpointer data); |
|
69 |
|
70 // handler for data from udev monitor |
|
71 static gboolean OnUdevMonitor(GIOChannel *source, |
|
72 GIOCondition condition, |
|
73 gpointer data); |
|
74 |
|
75 udev_lib mUdev; |
|
76 struct udev_monitor* mMonitor; |
|
77 guint mMonitorSourceID; |
|
78 // Information about currently connected gamepads. |
|
79 nsAutoTArray<Gamepad,4> mGamepads; |
|
80 }; |
|
81 |
|
82 // singleton instance |
|
83 LinuxGamepadService* gService = nullptr; |
|
84 |
|
85 void |
|
86 LinuxGamepadService::AddDevice(struct udev_device* dev) |
|
87 { |
|
88 const char* devpath = mUdev.udev_device_get_devnode(dev); |
|
89 if (!devpath) { |
|
90 return; |
|
91 } |
|
92 |
|
93 // Ensure that this device hasn't already been added. |
|
94 for (unsigned int i = 0; i < mGamepads.Length(); i++) { |
|
95 if (strcmp(mGamepads[i].devpath, devpath) == 0) { |
|
96 return; |
|
97 } |
|
98 } |
|
99 |
|
100 Gamepad gamepad; |
|
101 snprintf(gamepad.devpath, sizeof(gamepad.devpath), "%s", devpath); |
|
102 GIOChannel* channel = g_io_channel_new_file(devpath, "r", nullptr); |
|
103 if (!channel) { |
|
104 return; |
|
105 } |
|
106 |
|
107 g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, nullptr); |
|
108 g_io_channel_set_encoding(channel, nullptr, nullptr); |
|
109 g_io_channel_set_buffered(channel, FALSE); |
|
110 int fd = g_io_channel_unix_get_fd(channel); |
|
111 char name[128]; |
|
112 if (ioctl(fd, JSIOCGNAME(sizeof(name)), &name) == -1) { |
|
113 strcpy(name, "unknown"); |
|
114 } |
|
115 const char* vendor_id = |
|
116 mUdev.udev_device_get_property_value(dev, "ID_VENDOR_ID"); |
|
117 const char* model_id = |
|
118 mUdev.udev_device_get_property_value(dev, "ID_MODEL_ID"); |
|
119 if (!vendor_id || !model_id) { |
|
120 struct udev_device* parent = |
|
121 mUdev.udev_device_get_parent_with_subsystem_devtype(dev, |
|
122 "input", |
|
123 nullptr); |
|
124 if (parent) { |
|
125 vendor_id = mUdev.udev_device_get_sysattr_value(parent, "id/vendor"); |
|
126 model_id = mUdev.udev_device_get_sysattr_value(parent, "id/product"); |
|
127 } |
|
128 } |
|
129 snprintf(gamepad.idstring, sizeof(gamepad.idstring), |
|
130 "%s-%s-%s", |
|
131 vendor_id ? vendor_id : "unknown", |
|
132 model_id ? model_id : "unknown", |
|
133 name); |
|
134 |
|
135 char numAxes = 0, numButtons = 0; |
|
136 ioctl(fd, JSIOCGAXES, &numAxes); |
|
137 gamepad.numAxes = numAxes; |
|
138 ioctl(fd, JSIOCGBUTTONS, &numButtons); |
|
139 gamepad.numButtons = numButtons; |
|
140 |
|
141 nsRefPtr<GamepadService> service(GamepadService::GetService()); |
|
142 gamepad.index = service->AddGamepad(gamepad.idstring, |
|
143 mozilla::dom::NoMapping, |
|
144 gamepad.numButtons, |
|
145 gamepad.numAxes); |
|
146 |
|
147 gamepad.source_id = |
|
148 g_io_add_watch(channel, |
|
149 GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), |
|
150 OnGamepadData, |
|
151 GINT_TO_POINTER(gamepad.index)); |
|
152 g_io_channel_unref(channel); |
|
153 |
|
154 mGamepads.AppendElement(gamepad); |
|
155 } |
|
156 |
|
157 void |
|
158 LinuxGamepadService::RemoveDevice(struct udev_device* dev) |
|
159 { |
|
160 const char* devpath = mUdev.udev_device_get_devnode(dev); |
|
161 if (!devpath) { |
|
162 return; |
|
163 } |
|
164 |
|
165 nsRefPtr<GamepadService> service(GamepadService::GetService()); |
|
166 for (unsigned int i = 0; i < mGamepads.Length(); i++) { |
|
167 if (strcmp(mGamepads[i].devpath, devpath) == 0) { |
|
168 g_source_remove(mGamepads[i].source_id); |
|
169 service->RemoveGamepad(mGamepads[i].index); |
|
170 mGamepads.RemoveElementAt(i); |
|
171 break; |
|
172 } |
|
173 } |
|
174 } |
|
175 |
|
176 void |
|
177 LinuxGamepadService::ScanForDevices() |
|
178 { |
|
179 struct udev_enumerate* en = mUdev.udev_enumerate_new(mUdev.udev); |
|
180 mUdev.udev_enumerate_add_match_subsystem(en, "input"); |
|
181 mUdev.udev_enumerate_scan_devices(en); |
|
182 |
|
183 struct udev_list_entry* dev_list_entry; |
|
184 for (dev_list_entry = mUdev.udev_enumerate_get_list_entry(en); |
|
185 dev_list_entry != nullptr; |
|
186 dev_list_entry = mUdev.udev_list_entry_get_next(dev_list_entry)) { |
|
187 const char* path = mUdev.udev_list_entry_get_name(dev_list_entry); |
|
188 struct udev_device* dev = mUdev.udev_device_new_from_syspath(mUdev.udev, |
|
189 path); |
|
190 if (is_gamepad(dev)) { |
|
191 AddDevice(dev); |
|
192 } |
|
193 |
|
194 mUdev.udev_device_unref(dev); |
|
195 } |
|
196 |
|
197 mUdev.udev_enumerate_unref(en); |
|
198 } |
|
199 |
|
200 void |
|
201 LinuxGamepadService::AddMonitor() |
|
202 { |
|
203 // Add a monitor to watch for device changes |
|
204 mMonitor = |
|
205 mUdev.udev_monitor_new_from_netlink(mUdev.udev, "udev"); |
|
206 if (!mMonitor) { |
|
207 // Not much we can do here. |
|
208 return; |
|
209 } |
|
210 mUdev.udev_monitor_filter_add_match_subsystem_devtype(mMonitor, |
|
211 "input", |
|
212 nullptr); |
|
213 |
|
214 int monitor_fd = mUdev.udev_monitor_get_fd(mMonitor); |
|
215 GIOChannel* monitor_channel = g_io_channel_unix_new(monitor_fd); |
|
216 mMonitorSourceID = |
|
217 g_io_add_watch(monitor_channel, |
|
218 GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), |
|
219 OnUdevMonitor, |
|
220 nullptr); |
|
221 g_io_channel_unref(monitor_channel); |
|
222 |
|
223 mUdev.udev_monitor_enable_receiving(mMonitor); |
|
224 } |
|
225 |
|
226 void |
|
227 LinuxGamepadService::RemoveMonitor() |
|
228 { |
|
229 if (mMonitorSourceID) { |
|
230 g_source_remove(mMonitorSourceID); |
|
231 mMonitorSourceID = 0; |
|
232 } |
|
233 if (mMonitor) { |
|
234 mUdev.udev_monitor_unref(mMonitor); |
|
235 mMonitor = nullptr; |
|
236 } |
|
237 } |
|
238 |
|
239 void |
|
240 LinuxGamepadService::Startup() |
|
241 { |
|
242 // Don't bother starting up if libudev couldn't be loaded or initialized. |
|
243 if (!mUdev) |
|
244 return; |
|
245 |
|
246 AddMonitor(); |
|
247 ScanForDevices(); |
|
248 } |
|
249 |
|
250 void |
|
251 LinuxGamepadService::Shutdown() |
|
252 { |
|
253 for (unsigned int i = 0; i < mGamepads.Length(); i++) { |
|
254 g_source_remove(mGamepads[i].source_id); |
|
255 } |
|
256 mGamepads.Clear(); |
|
257 RemoveMonitor(); |
|
258 } |
|
259 |
|
260 bool |
|
261 LinuxGamepadService::is_gamepad(struct udev_device* dev) |
|
262 { |
|
263 if (!mUdev.udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK")) |
|
264 return false; |
|
265 |
|
266 const char* devpath = mUdev.udev_device_get_devnode(dev); |
|
267 if (!devpath) { |
|
268 return false; |
|
269 } |
|
270 if (strncmp(kJoystickPath, devpath, sizeof(kJoystickPath) - 1) != 0) { |
|
271 return false; |
|
272 } |
|
273 |
|
274 return true; |
|
275 } |
|
276 |
|
277 void |
|
278 LinuxGamepadService::ReadUdevChange() |
|
279 { |
|
280 struct udev_device* dev = |
|
281 mUdev.udev_monitor_receive_device(mMonitor); |
|
282 const char* action = mUdev.udev_device_get_action(dev); |
|
283 if (is_gamepad(dev)) { |
|
284 if (strcmp(action, "add") == 0) { |
|
285 AddDevice(dev); |
|
286 } else if (strcmp(action, "remove") == 0) { |
|
287 RemoveDevice(dev); |
|
288 } |
|
289 } |
|
290 mUdev.udev_device_unref(dev); |
|
291 } |
|
292 |
|
293 // static |
|
294 gboolean |
|
295 LinuxGamepadService::OnGamepadData(GIOChannel* source, |
|
296 GIOCondition condition, |
|
297 gpointer data) |
|
298 { |
|
299 int index = GPOINTER_TO_INT(data); |
|
300 //TODO: remove gamepad? |
|
301 if (condition & G_IO_ERR || condition & G_IO_HUP) |
|
302 return FALSE; |
|
303 |
|
304 while (true) { |
|
305 struct js_event event; |
|
306 gsize count; |
|
307 GError* err = nullptr; |
|
308 if (g_io_channel_read_chars(source, |
|
309 (gchar*)&event, |
|
310 sizeof(event), |
|
311 &count, |
|
312 &err) != G_IO_STATUS_NORMAL || |
|
313 count == 0) { |
|
314 break; |
|
315 } |
|
316 |
|
317 //TODO: store device state? |
|
318 if (event.type & JS_EVENT_INIT) { |
|
319 continue; |
|
320 } |
|
321 |
|
322 nsRefPtr<GamepadService> service(GamepadService::GetService()); |
|
323 switch (event.type) { |
|
324 case JS_EVENT_BUTTON: |
|
325 service->NewButtonEvent(index, event.number, !!event.value); |
|
326 break; |
|
327 case JS_EVENT_AXIS: |
|
328 service->NewAxisMoveEvent(index, event.number, |
|
329 ((float)event.value) / kMaxAxisValue); |
|
330 break; |
|
331 } |
|
332 } |
|
333 |
|
334 return TRUE; |
|
335 } |
|
336 |
|
337 // static |
|
338 gboolean |
|
339 LinuxGamepadService::OnUdevMonitor(GIOChannel* source, |
|
340 GIOCondition condition, |
|
341 gpointer data) |
|
342 { |
|
343 if (condition & G_IO_ERR || condition & G_IO_HUP) |
|
344 return FALSE; |
|
345 |
|
346 gService->ReadUdevChange(); |
|
347 return TRUE; |
|
348 } |
|
349 |
|
350 } // namespace |
|
351 |
|
352 namespace mozilla { |
|
353 namespace hal_impl { |
|
354 |
|
355 void StartMonitoringGamepadStatus() |
|
356 { |
|
357 if (!gService) { |
|
358 gService = new LinuxGamepadService(); |
|
359 gService->Startup(); |
|
360 } |
|
361 } |
|
362 |
|
363 void StopMonitoringGamepadStatus() |
|
364 { |
|
365 if (gService) { |
|
366 gService->Shutdown(); |
|
367 delete gService; |
|
368 gService = nullptr; |
|
369 } |
|
370 } |
|
371 |
|
372 } // namespace hal_impl |
|
373 } // namespace mozilla |